Godotでプレイヤーのリスポーン処理を組むとき、ありがちなのが「Playerシーンに全部詰め込む」パターンですね。

  • Player.gd の中に「死亡処理」「チェックポイント処理」「ステージ遷移処理」などが全部入り
  • チェックポイントの見た目を変えたいだけなのに、Playerのコードをいじる羽目になる
  • 敵や動く床など「プレイヤー以外のリスポーン」が必要になった瞬間、設計が破綻する

Godot標準のサンプルだと、つい「プレイヤーが自分でリスポーン位置を管理する」実装になりがちですが、それだと継承と巨大スクリプトに依存した設計になってしまいます。

そこで今回は、「リスポーン地点そのものをコンポーネント化」してしまいましょう。
プレイヤーや敵は「死んだらどこから復活するか」を自分で管理せず、RespawnAnchor コンポーネントに任せる設計です。

【Godot 4】どこでもチェックポイント化!「RespawnAnchor」コンポーネント

今回の RespawnAnchor は、ざっくり言うとこんなコンポーネントです:

  • 触れたオブジェクトの「リスポーン位置」をこのアンカーに更新する
  • シーン上に好きなだけ配置できるチェックポイント
  • プレイヤーだけでなく、敵や動く床などにも使い回し可能
  • 「死亡時にどこから復活するか」をコンポーネント間のシグナルでやり取りする

設計のキモは以下の2つです。

  1. Respawnable というインターフェース的コンポーネントを用意し、「復活できるオブジェクト」を統一的に扱う
  2. RespawnAnchor は「どこから復活するか」だけを知っている。誰が死ぬかは知らない

つまり、プレイヤー側は「自分は Respawnable です」と名乗るだけ、チェックポイント側は「触れた Respawnable に自分を登録する」だけ、という疎結合な構造にします。


コンポーネント1: Respawnable.gd(復活可能コンポーネント)


extends Node
class_name Respawnable
## 「復活できるもの」にアタッチするコンポーネント。
## Player や Enemy などの「本体ノード」に子としてぶら下げて使います。
##
## - 現在のリスポーン位置を保持
## - 死亡時に復活処理を行う
## - RespawnAnchor から呼び出されるための API を提供

## 復活対象の「本体ノード」。
## 通常は Player (CharacterBody2D / 3D) や Enemy などを指します。
@export var target_node: Node3D

## 最初のリスポーン地点。
## 未設定の場合は、_ready 時点の target_node の位置を初期値とします。
@export var initial_respawn_position: Vector3

## 死亡時に呼ばれるシグナル(UI やエフェクトと連携したいとき用)
signal died
## 復活完了時に呼ばれるシグナル
signal respawned

## 現在のリスポーン位置
var _current_respawn_position: Vector3

func _ready() -> void:
    if target_node == null:
        push_warning("Respawnable: target_node が設定されていません。このノードの親を自動で使います。")
        if owner is Node3D:
            target_node = owner
        elif get_parent() is Node3D:
            target_node = get_parent()
        else:
            push_error("Respawnable: Node3D を見つけられません。target_node を明示的に設定してください。")
            return

    # 初期リスポーン位置の決定
    if initial_respawn_position == Vector3.ZERO:
        initial_respawn_position = target_node.global_position

    _current_respawn_position = initial_respawn_position


## RespawnAnchor から呼ばれるメソッド。
## 「次に死んだとき、このアンカーから復活してね」という意味。
func set_respawn_anchor(anchor_global_position: Vector3) -> void:
    _current_respawn_position = anchor_global_position


## 外部から「死亡した」ときに呼び出す想定のメソッド。
## 例: HP が 0 になったとき、落下したときなど。
func die() -> void:
    if target_node == null:
        push_error("Respawnable: target_node が設定されていないため、die() を処理できません。")
        return

    emit_signal("died")
    _do_respawn()


## 実際のリスポーン処理。
## ここをオーバーライド/上書きして、アニメーションやフェードを挟んでもOK。
func _do_respawn() -> void:
    if target_node == null:
        return

    # 速度などを持つノードの場合は、ここでリセットすると良いです。
    if "velocity" in target_node:
        target_node.velocity = Vector3.ZERO

    target_node.global_position = _current_respawn_position
    emit_signal("respawned")


## 現在のリスポーン位置を外部から参照したいとき用
func get_respawn_position() -> Vector3:
    return _current_respawn_position

コンポーネント2: RespawnAnchor.gd(復活地点コンポーネント)


extends Area3D
class_name RespawnAnchor
## 「ここから復活してね」という地点を表すコンポーネント。
## Area3D を継承しているので、CollisionShape3D を子に持たせて「触れたら発動」させます。
##
## - Body が入ってきたら、その Body(または親)に付いている Respawnable を探す
## - 見つかったら、その Respawnable に自分の位置を登録する
## - 任意で「一度だけ有効」「自動で見た目を変える」などの演出も可能

## このアンカーが有効かどうか。
## false にすると、触れてもリスポーン地点として登録されません。
@export var enabled: bool = true

## 一度触れたら無効化するかどうか(チェックポイントを一回きりにしたい場合など)
@export var one_shot: bool = false

## プレイヤー専用にしたい場合は、対象グループ名を指定。
## 空文字列なら誰でも OK(Respawnable が付いていれば)。
@export var required_group: StringName = ""

## デバッグ用: 有効化されたときにコンソールにログを出す
@export var print_debug_log: bool = true

## 見た目を変えたいとき用(例: 有効化済みアンカーは光らせるなど)
@export var activated_material: Material

## 既に誰かに踏まれて「アクティブ」になっているか
var _activated: bool = false


func _ready() -> void:
    # Area3D のボディ侵入シグナルを接続
    body_entered.connect(_on_body_entered)


## Body がこのアンカーに触れたときの処理
func _on_body_entered(body: Node3D) -> void:
    if not enabled:
        return

    # グループ指定がある場合はチェック
    if required_group != "" and not body.is_in_group(required_group):
        return

    # Body かその親階層から Respawnable コンポーネントを探す
    var respawnable := _find_respawnable(body)
    if respawnable == null:
        return

    # Respawnable にこのアンカーの位置を登録
    respawnable.set_respawn_anchor(global_position)

    if print_debug_log:
        print("[RespawnAnchor] Respawn point set for: ", body.name, " at ", global_position)

    _set_activated_visuals()

    if one_shot:
        enabled = false


## body またはその親から Respawnable コンポーネントを探すユーティリティ関数
func _find_respawnable(start_node: Node) -> Respawnable:
    var current: Node = start_node
    while current != null:
        for child in current.get_children():
            if child is Respawnable:
                return child

        current = current.get_parent()

    return null


## 有効化されたときの見た目変更などをまとめて行う
func _set_activated_visuals() -> void:
    if _activated:
        return
    _activated = true

    # 見た目を変えたい場合の例
    if activated_material:
        # MeshInstance3D を子から探してマテリアルを切り替える
        var mesh := _find_mesh_instance(self)
        if mesh:
            mesh.set_surface_override_material(0, activated_material)


## 自分以下から MeshInstance3D を探すヘルパー
func _find_mesh_instance(node: Node) -> MeshInstance3D:
    if node is MeshInstance3D:
        return node

    for child in node.get_children():
        var result := _find_mesh_instance(child)
        if result:
            return result

    return null

使い方の手順

  1. コンポーネントスクリプトを用意
    上記の Respawnable.gdRespawnAnchor.gd をプロジェクト内(例: res://components/)に保存します。
  2. プレイヤーに Respawnable をアタッチ
    例として、3Dのプレイヤーシーンが以下のような構成だとします。
    Player (CharacterBody3D)
     ├── Camera3D
     ├── CollisionShape3D
     └── Respawnable (Node)
        
    • Player シーンを開く
    • 子ノードとして Node を追加し、名前を Respawnable に変更
    • そのノードに Respawnable.gd をアタッチ
    • インスペクタで target_nodePlayer (CharacterBody3D) を指定(未指定でも自動で親を使います)

    プレイヤーの HP が 0 になったときなどに、Respawnable.die() を呼べばリスポーンが発動します。

  3. チェックポイントとして RespawnAnchor を配置
    ステージシーンを開き、以下のように配置します:
    LevelRoot (Node3D)
     ├── Player (CharacterBody3D)
     │    ├── Camera3D
     │    ├── CollisionShape3D
     │    └── Respawnable (Node)
     ├── RespawnAnchor_Start (RespawnAnchor)
     │    └── CollisionShape3D
     ├── RespawnAnchor_Middle (RespawnAnchor)
     │    └── CollisionShape3D
     └── RespawnAnchor_Boss (RespawnAnchor)
          └── CollisionShape3D
        
    • 新規ノードで Area3D を追加し、RespawnAnchor.gd をアタッチ
    • 子として CollisionShape3D を追加し、プレイヤーが触れる範囲を設定
    • 必要に応じて required_group = "player" のようにグループ制限をかける(Player を “player” グループに入れておく)
    • 一度だけ有効にしたいアンカーは one_shot = true

    これで、プレイヤーがどれかのアンカーに触れるたびに、その位置が新しいリスポーン地点として登録されます。

  4. 死亡トリガーから Respawnable.die() を呼ぶ
    例: プレイヤーのスクリプトで HP 管理をしている場合:
    
    # Player.gd(抜粋)
    @onready var respawnable: Respawnable = $Respawnable
    
    var hp: int = 3
    
    func apply_damage(amount: int) -> void:
        hp -= amount
        if hp <= 0:
            hp = 0
            respawnable.die()
            # HP を回復しておく
            hp = 3
        

    あるいは、「奈落」に落ちたときに死亡させるトリガーを作るのも簡単です:

    KillZone (Area3D)
     └── CollisionShape3D
        
    
    # KillZone.gd
    extends Area3D
    
    func _ready() -> void:
        body_entered.connect(_on_body_entered)
    
    func _on_body_entered(body: Node3D) -> void:
        # Body か親に Respawnable がいれば die() を呼ぶ
        var current: Node = body
        while current:
            for child in current.get_children():
                if child is Respawnable:
                    child.die()
                    return
            current = current.get_parent()
        

メリットと応用

RespawnAnchorRespawnable を分離した「コンポーネント指向」の構成にすることで、以下のようなメリットがあります。

  • Player.gd が「死亡処理の塊」にならない
    リスポーン位置の管理・更新は Respawnable に丸投げできるので、プレイヤーのコードは「いつ死ぬか」だけに集中できます。
  • 敵や動く床にもそのまま使い回せる
    例えば、落下して戻ってくる敵や、落ちたら元の位置に戻る動く足場にも、同じ Respawnable をアタッチするだけでOKです。
    MovingPlatform (Node3D)
     ├── MeshInstance3D
     ├── CollisionShape3D
     └── Respawnable (Node)
        

    こうしておけば、共通の KillZone に落ちたときに、プレイヤーも足場も同じ仕組みで復活できます。

  • レベルデザインが超ラクになる
    チェックポイントを増やしたいときは、RespawnAnchor をコピペして置くだけ。
    プレイヤーのスクリプトを一切触らずに「ここから復活してほしい」をマップ側で決められます。
  • シーン構造が浅く・シンプルに保てる
    「リスポーン専用のベースクラス」を継承してプレイヤーや敵を作る、という設計にしなくて済みます。
    それぞれのシーンは好きなように作りつつ、「Respawnable コンポーネントを付けたら復活できる」というルールだけ共有すればOKです。

改造案として、「死亡前の位置と速度を記録して、そこからスムーズに戻す」ような演出も簡単に追加できます。例えば、Respawnable に「フェードアウト→ワープ→フェードイン」を挟む処理を入れてみましょう。


## Respawnable.gd の中に差し替え/追加できる改造版 _do_respawn()
func _do_respawn() -> void:
    if target_node == null:
        return

    # 例: 画面フェード用の UI にシグナルを送る(別シングルトンなど)
    if Engine.has_singleton("ScreenFader"):
        var fader = Engine.get_singleton("ScreenFader")
        await fader.fade_out(0.3)

    if "velocity" in target_node:
        target_node.velocity = Vector3.ZERO

    target_node.global_position = _current_respawn_position

    if Engine.has_singleton("ScreenFader"):
        var fader = Engine.get_singleton("ScreenFader")
        await fader.fade_in(0.3)

    emit_signal("respawned")

このように、「どこから復活するか」を RespawnAnchor に、「どう復活するか」を Respawnable に分離しておくと、演出の追加やゲームデザインの変更にも柔軟に対応できます。
ぜひ、自分のプロジェクトに合わせてコンポーネントを育てていってみてください。