Godotでプレイヤーのリスポーン処理を組むとき、ありがちなのが「Playerシーンに全部詰め込む」パターンですね。
- Player.gd の中に「死亡処理」「チェックポイント処理」「ステージ遷移処理」などが全部入り
- チェックポイントの見た目を変えたいだけなのに、Playerのコードをいじる羽目になる
- 敵や動く床など「プレイヤー以外のリスポーン」が必要になった瞬間、設計が破綻する
Godot標準のサンプルだと、つい「プレイヤーが自分でリスポーン位置を管理する」実装になりがちですが、それだと継承と巨大スクリプトに依存した設計になってしまいます。
そこで今回は、「リスポーン地点そのものをコンポーネント化」してしまいましょう。
プレイヤーや敵は「死んだらどこから復活するか」を自分で管理せず、RespawnAnchor コンポーネントに任せる設計です。
【Godot 4】どこでもチェックポイント化!「RespawnAnchor」コンポーネント
今回の RespawnAnchor は、ざっくり言うとこんなコンポーネントです:
- 触れたオブジェクトの「リスポーン位置」をこのアンカーに更新する
- シーン上に好きなだけ配置できるチェックポイント
- プレイヤーだけでなく、敵や動く床などにも使い回し可能
- 「死亡時にどこから復活するか」をコンポーネント間のシグナルでやり取りする
設計のキモは以下の2つです。
- Respawnable というインターフェース的コンポーネントを用意し、「復活できるオブジェクト」を統一的に扱う
- 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
使い方の手順
- コンポーネントスクリプトを用意
上記のRespawnable.gdとRespawnAnchor.gdをプロジェクト内(例:res://components/)に保存します。 - プレイヤーに Respawnable をアタッチ
例として、3Dのプレイヤーシーンが以下のような構成だとします。Player (CharacterBody3D) ├── Camera3D ├── CollisionShape3D └── Respawnable (Node)Playerシーンを開く- 子ノードとして
Nodeを追加し、名前をRespawnableに変更 - そのノードに
Respawnable.gdをアタッチ - インスペクタで
target_nodeにPlayer (CharacterBody3D)を指定(未指定でも自動で親を使います)
プレイヤーの HP が 0 になったときなどに、
Respawnable.die()を呼べばリスポーンが発動します。 - チェックポイントとして 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に
これで、プレイヤーがどれかのアンカーに触れるたびに、その位置が新しいリスポーン地点として登録されます。
- 新規ノードで
- 死亡トリガーから 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()
メリットと応用
RespawnAnchor と Respawnable を分離した「コンポーネント指向」の構成にすることで、以下のようなメリットがあります。
- 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 に分離しておくと、演出の追加やゲームデザインの変更にも柔軟に対応できます。
ぜひ、自分のプロジェクトに合わせてコンポーネントを育てていってみてください。
