Godotでチェックポイント(復活地点)を実装しようとすると、ついこんな構成にしがちですよね。
Player.gd を肥大化させる: - チェックポイント検知 - GameManagerへの通知 - リスポーン処理 - アニメーションやSE再生 …ぜんぶ Player に書いてしまう
あるいは、チェックポイントごとに個別のスクリプトを作って、似たような処理をコピペしてしまったり…。
これだと、
- プレイヤーを別シーンに分けたときに再利用しづらい
- ゲームが大きくなると Player.gd が「なんでも屋」になって地獄
- チェックポイントの仕様変更があると、あちこち修正が必要
といった問題が出てきます。
そこで今回は、「継承より合成」の考え方で、どのシーンにもポン付けできる
「Checkpoint(復活地点)」コンポーネントを作ってみましょう。
プレイヤーが触れたら、現在の座標を GameManager(オートロード)などのグローバル変数に保存するだけ、という単機能コンポーネントです。
【Godot 4】どのシーンにもポン付けOKな復活地点!「Checkpoint」コンポーネント
このコンポーネントは、ざっくり言うと
- エリア(Area2D / Area3D)にアタッチして
- プレイヤーが入ってきたら
GameManagerに「ここが最新チェックポイントだよ」と座標を保存する
というだけの、シンプルな仕組みです。
プレイヤー側は「死んだときに GameManager.last_checkpoint_position に戻る」処理だけ持っていればOK。
チェックポイントのロジックはすべてコンポーネント側に閉じ込めましょう。
Checkpoint コンポーネントのフルコード
extends Area2D
class_name Checkpoint
## プレイヤーが触れると、現在の座標を GameManager(グローバル) に保存するコンポーネント。
## どのシーンにもポン付けできる「復活地点」用エリアです。
## --- 設定パラメータ ------------------------------------------
@export var player_group_name: StringName = &"player":
## どのグループに属するノードを「プレイヤー」とみなすか。
## デフォルトでは "player" グループ。
## 例: プレイヤーのシーンの root ノードを "player" グループに追加しておきましょう。
@export var auto_activate: bool = true:
## true の場合、プレイヤーが入った瞬間に自動でチェックポイントとして登録します。
## false にすると、スイッチ連動など、外部から手動で有効化したいケースに使えます。
@export var override_position: bool = false:
## true の場合、このノードの position ではなく、下記 respawn_position を保存します。
## 「見た目の位置」と「復活させたい位置」をズラしたいときに便利です。
@export var respawn_position: Vector2 = Vector2.ZERO:
## override_position = true のときに使う、実際に復活させたい座標。
## ローカル座標で指定します(このノードの position を原点としたオフセット)。
@export var one_shot: bool = false:
## true の場合、一度有効化されたら二度と反応しません。
## チュートリアル用の一回限りのチェックポイントなどに。
@export var play_sound_on_activate: bool = false:
## 有効化時にサウンドを鳴らすかどうか。
## 子ノードに AudioStreamPlayer2D がある場合に再生します。
@export var play_animation_on_activate: bool = false:
## 有効化時にアニメーションを再生するかどうか。
## 子ノードに AnimatedSprite2D / AnimationPlayer がある場合を想定しています。
@export var debug_print: bool = false:
## true にすると、チェックポイント登録時にデバッグログを出力します。
## --- 内部状態 -----------------------------------------------
var _is_activated: bool = false
var _has_manual_owner: bool = false
## -------------------------------------------------------------
## ライフサイクル
## -------------------------------------------------------------
func _ready() -> void:
## 衝突レイヤ・マスクの設定はエディタ側で行う想定。
## ここではシグナル接続だけ行います。
connect("body_entered", _on_body_entered)
# GameManager が存在するか簡易チェック(任意)
if not Engine.has_singleton("GameManager") and not has_node("/root/GameManager"):
if debug_print:
push_warning("GameManager が見つかりません。オートロード設定を確認してください。")
## -------------------------------------------------------------
## パブリックAPI
## -------------------------------------------------------------
func activate_checkpoint(by: Node = null) -> void:
## 外部から手動でチェックポイントを有効化したい場合に呼び出します。
## 例: スイッチを押したら特定の Checkpoint を有効化する、など。
if one_shot and _is_activated:
return
_is_activated = true
var global_pos := _get_respawn_global_position()
_save_to_game_manager(global_pos)
if debug_print:
var who := by if by != null else self
print("[Checkpoint] activated at ", global_pos, " by: ", who)
_play_effects()
func reset_checkpoint() -> void:
## one_shot 用。チェックポイントを再び有効化可能な状態に戻します。
_is_activated = false
## -------------------------------------------------------------
## シグナルハンドラ
## -------------------------------------------------------------
func _on_body_entered(body: Node) -> void:
## Area2D に何かが入ってきたときに呼ばれる。
if not auto_activate:
return
if not _is_player(body):
return
activate_checkpoint(body)
## -------------------------------------------------------------
## 内部処理
## -------------------------------------------------------------
func _is_player(body: Node) -> bool:
## プレイヤー判定用。
## グループ判定をベースにしているので、プレイヤーの root ノードを
## あらかじめ "player" グループに入れておきましょう。
return body.is_in_group(player_group_name)
func _get_respawn_global_position() -> Vector2:
## 実際に保存するグローバル座標を計算する。
if override_position:
# ローカル座標 respawn_position をグローバルに変換
return to_global(respawn_position)
else:
# このノード自身のグローバル座標を使う
return global_position
func _save_to_game_manager(global_pos: Vector2) -> void:
## GameManager に座標を保存する。
## ここでは 2パターンをサポート:
## - /root/GameManager ノードがある
## - Engine.singleton("GameManager") がある(C#などで実装されたシングルトンなど)
var gm := _get_game_manager()
if gm == null:
if debug_print:
push_warning("GameManager が取得できませんでした。座標は保存されません。")
return
# GameManager 側に last_checkpoint_position という変数がある想定。
# ない場合は自動でプロパティを作ることはできないので、GameManager.gd 側で用意してください。
if not gm.has_variable("last_checkpoint_position"):
# 動的に作ることはできないので、警告だけ出す。
if debug_print:
push_warning("GameManager に 'last_checkpoint_position' が定義されていません。")
else:
gm.last_checkpoint_position = global_pos
# ついでに「どのチェックポイントか」を覚えたい場合は、idなどを追加しても良いですね。
if gm.has_variable("last_checkpoint_node"):
gm.last_checkpoint_node = self
func _get_game_manager() -> Object:
## GameManager シングルトンを探すヘルパー。
## プロジェクトに合わせて実装を調整してください。
if has_node("/root/GameManager"):
return get_node("/root/GameManager")
if Engine.has_singleton("GameManager"):
return Engine.get_singleton("GameManager")
return null
func _play_effects() -> void:
## 有効化時の視覚・聴覚効果を再生する。
if play_sound_on_activate:
var audio := _find_child_audio()
if audio:
audio.play()
if play_animation_on_activate:
var anim_player := _find_child_animation_player()
if anim_player:
# "activate" というアニメーションがあれば再生する
if anim_player.has_animation("activate"):
anim_player.play("activate")
var sprite := _find_child_animated_sprite()
if sprite:
# "activate" というアニメーションがあれば再生する
if sprite.sprite_frames and sprite.sprite_frames.has_animation("activate"):
sprite.play("activate")
func _find_child_audio() -> AudioStreamPlayer2D:
for child in get_children():
if child is AudioStreamPlayer2D:
return child
return null
func _find_child_animation_player() -> AnimationPlayer:
for child in get_children():
if child is AnimationPlayer:
return child
return null
func _find_child_animated_sprite() -> AnimatedSprite2D:
for child in get_children():
if child is AnimatedSprite2D:
return child
return null
使い方の手順
ここでは、2Dアクションゲームを例に「プレイヤーが死んだら最後に触れたチェックポイントに復活する」フローを作ってみます。
手順① GameManager(オートロード)を用意する
まずは、チェックポイント情報を保存するグローバルオブジェクトを作ります。
# GameManager.gd
extends Node
class_name GameManager
## 最後に有効化されたチェックポイントの座標
var last_checkpoint_position: Vector2 = Vector2.ZERO
## どのチェックポイントノードか覚えておきたい場合(任意)
var last_checkpoint_node: Node = null
func reset_checkpoint() -> void:
last_checkpoint_position = Vector2.ZERO
last_checkpoint_node = null
その後、Godot のメニューから
- Project > Project Settings… > Autoload
- Path:
res://GameManager.gd - Node Name:
GameManager - Add を押す
これでどこからでも GameManager にアクセスできるようになります。
手順② Checkpoint コンポーネントをシーンに追加する
ステージシーンに、チェックポイント用のエリアを作ります。
ノード構成のイメージはこんな感じです。
Level01 (Node2D)
├── Player (CharacterBody2D)
│ ├── Sprite2D
│ ├── CollisionShape2D
│ └── ...
├── Checkpoint1 (Area2D)
│ ├── CollisionShape2D
│ ├── Sprite2D (Optional: 旗や光るマーカー)
│ └── Checkpoint (Script) ← 今回のコンポーネント
└── Checkpoint2 (Area2D)
├── CollisionShape2D
├── Sprite2D
└── Checkpoint (Script)
- Area2D ノードを追加(名前を
Checkpoint1などに) - 子に CollisionShape2D を追加して、プレイヤーが触れられる範囲を設定
- Area2D に今回の
Checkpoint.gdをアタッチ - 必要に応じて
play_sound_on_activateやplay_animation_on_activateをオン
プレイヤーの root ノード(例: Player (CharacterBody2D))を
インスペクタの「Node > Groups」から player グループに追加しておくのを忘れずに。
手順③ プレイヤーのリスポーン処理を実装する
プレイヤーが「死んだ」ときに、GameManager.last_checkpoint_position にワープする処理を書きます。
ここでは超シンプルな例として、HPが0になったら復活するスニペットを示します。
# Player.gd
extends CharacterBody2D
@export var max_hp: int = 3
var hp: int
func _ready() -> void:
hp = max_hp
# シーン開始時に、現在位置を初期チェックポイントとして登録しても良い
if GameManager:
GameManager.last_checkpoint_position = global_position
func take_damage(amount: int) -> void:
hp -= amount
if hp <= 0:
_die_and_respawn()
func _die_and_respawn() -> void:
# 死亡演出などを挟みたい場合は、アニメーション後に呼ぶ
if GameManager and GameManager.last_checkpoint_position != Vector2.ZERO:
global_position = GameManager.last_checkpoint_position
else:
# チェックポイントが未設定なら、単にHPだけ回復
global_position = Vector2.ZERO # あるいはステージの開始位置など
hp = max_hp
velocity = Vector2.ZERO
これだけで、「最後に触れた Checkpoint コンポーネントの位置に復活する」仕組みが完成します。
手順④ 実際にステージ上に複数配置する
あとは、チェックポイントを好きなだけコピペしてステージに並べるだけです。
Level01 (Node2D)
├── Player (CharacterBody2D)
├── Checkpoint_Start (Area2D)
│ └── Checkpoint (auto_activate = false) ← ステージ開始時に手動で有効化しても良い
├── Checkpoint_Mid (Area2D)
│ └── Checkpoint (auto_activate = true)
└── Checkpoint_Boss (Area2D)
└── Checkpoint (auto_activate = true, one_shot = true)
例えば、ステージ開始時に Checkpoint_Start を有効化したい場合は、
Level シーンのスクリプトからこんな感じで呼べます。
# Level01.gd
extends Node2D
@onready var checkpoint_start: Checkpoint = $Checkpoint_Start/Checkpoint
func _ready() -> void:
checkpoint_start.activate_checkpoint()
メリットと応用
この「Checkpoint」コンポーネントを使うメリットはかなり大きいです。
- Player.gd がスリムになる
チェックポイント検知ロジックはすべてコンポーネント側に閉じ込められます。
プレイヤーは「死んだら GameManager に聞いて戻る」だけ。 - シーン構造がフラットで見通しが良い
「Checkpoint1」「Checkpoint2」…と、ただの Area2D として並んでいるだけなので、
レベルデザイナーがシーンを開いてもすぐに構造を理解できます。 - 再利用性が高い
2Dアクションでも、パズルでも、メトロイドヴァニアでも、
「プレイヤーが復活する位置を保存する」という概念は同じなので、
どんなプロジェクトにもそのまま持っていけます。 - 仕様変更に強い
例えば「チェックポイントを通過したらミニマップを更新したい」
「チェックポイントごとにセーブデータを書き出したい」などの要件が出ても、
Checkpoint.gd だけを修正すれば全ステージに反映されます。
深いノード継承や複雑な Player.gd に依存しないので、
「合成(コンポーネント)」志向の設計としてもかなり良いバランスですね。
改造案:チェックポイントごとに「ID」を付けてステージ進行を管理する
例えば、「どのチェックポイントまで到達したか」でステージ進行度をセーブしたい場合、
Checkpoint に checkpoint_id を追加して、GameManager にも保存するように改造できます。
# Checkpoint.gd の一部に追加
@export var checkpoint_id: StringName = &"":
## このチェックポイントを一意に識別するID。
## 例: "stage1_mid", "stage1_boss" など。
func _save_to_game_manager(global_pos: Vector2) -> void:
var gm := _get_game_manager()
if gm == null:
if debug_print:
push_warning("GameManager が取得できませんでした。座標は保存されません。")
return
if gm.has_variable("last_checkpoint_position"):
gm.last_checkpoint_position = global_pos
if gm.has_variable("last_checkpoint_node"):
gm.last_checkpoint_node = self
if gm.has_variable("last_checkpoint_id"):
gm.last_checkpoint_id = checkpoint_id
GameManager 側で last_checkpoint_id をセーブデータに書き出せば、
「どのチェックポイントまで進んだか」を簡単に復元できるようになります。
こんな感じで、小さなコンポーネントを組み合わせていくと、
継承に頼らずに柔軟なゲームアーキテクチャを作れるので、ぜひ試してみてください。
