ステージの途中に旗を立て、落ちたら最後に触れた旗から再開させるのがチェックポイントです。戻すのはプレイヤーだけで、拾ったコインや開けた扉はそのまま残すのが基本です。
シーンを読み直す形で作ると、プレイヤー以外まで作り直されます。拾ったはずのコインが地面に戻り、開けた扉も閉まってしまいます。
この記事では、プレイヤーに座標を1つ覚えさせて、落ちたら最後の旗へ戻す方法を解説します。
この記事でわかること
- チェックポイントが覚えているものが、座標ひとつだけで足りる理由
- 落ちたことに気づく書き方と、同じ処理を何度も走らせない止め方
- 戻すときに代入する3つと、その順番
- 戻さないもの(拾ったコイン、開けた扉)をそのままにする考え方
動作確認環境: Godot 4.7
戻り先の座標(respawn_position)
チェックポイントはセーブデータではありません。ステージの状態をまるごと覚えておいて復元する仕組みでもありません。
プレイヤーが Vector2 の変数を1つ持ち、旗に触れるたびに、その値が上書きされます。それだけです。
var respawn_position := Vector2.ZERO

この変数には、コインの枚数も、消したコインのノードも入っていません。だから落ちても戻りません。戻るのはプレイヤーだけで、世界はそのまま続きます。
一覧でも履歴でもないので、旗を何本立てても持つ値は1つのままです。旗2に触れた時点で、旗1の座標はもう残っていません。
シーンを丸ごと入れ替えると話が変わります。そちらはコインも扉も初期値に戻ります。仕組みは ゲーム進行の記事 にあります。この記事ではシーンを替えず、戻したい値だけを代入します。
他のエンジンでいうと: Unreal Engine には、GameMode が Player Start を使ってプレイヤーの Pawn を作り直す仕組みがあります。Unity では、保存した位置へ戻すか作り直すかを、ゲームに合わせて自分で書きます。この記事は作り直さず、同じプレイヤーを保存した位置へ戻す形です。
最初の戻り先と旗の足元(Marker2D)
旗は上書きしかしないので、まだ1本も触れていないときの戻り先は、最初に自分で入れておきます。Vector2.ZERO のままにしておくと、ステージの原点 (0, 0) へ戻ります。狙った場所とは限らないので、1件目にはゲームが始まった場所を入れます。
func _ready() -> void:
respawn_position = global_position # いま立っている場所を1件目に

エディタで配置したプレイヤーなら、これで足ります。ゲームが始まってから位置を動かす作りにしているなら、動かしたあとで入れます。
旗に触れたときは、旗が持っている目印の座標をもらいます。旗の Area2D そのものの位置ではなく、旗の足元に置いた Marker2D の global_position です。置く高さは地面そのものではなく、戻したプレイヤーの中心が来る高さにします。旗の原点は絵の中心にあることが多く、そこへ戻すと旗竿の中に埋まります。

position ではなく global_position を使う理由は Marker2Dの記事 にあります。旗の子に置いた目印の position は、旗から見た相対の値だからです。
落下の判定(KILL_Y)
落下判定は、_physics_process()(物理の処理のたびに毎回呼ばれる関数)の中で y 座標を見るだけで足ります。
if global_position.y > KILL_Y:
Godot の2Dは下が正なので、落ちたかどうかは > で調べます。position.y ではなく global_position.y にするのは、親の下に置いたときに値がずれないようにするためです。

線は、いちばん低い床よりはっきり下に引きます。穴の絵のさらに下です。比べているのはプレイヤーの原点なので、普通に歩いているあいだに原点が越えない高さにします。
もうひとつ、この判定は毎フレーム真になります。戻るまで待っているあいだも、座標は線を越えたままだからです。そこで死亡中を表す dying というフラグを立て、立っているあいだは _physics_process() の最初で抜けます。戻す処理が始まるのは、線を越えた最初の1フレームだけになります。

リスポーンで戻す3つの値
少し待ってから戻します。待つのは await get_tree().create_timer(RESPAWN_DELAY, false, true).timeout の1行です。await は、使い捨てのタイマーが鳴る(timeout)まで、この関数の続きを止めておく書き方です。そのあとに代入するのは3つです。
global_position = respawn_position # 位置
velocity = Vector2.ZERO # 速度
dying = false # 最後にフラグ
落下中の速度を持ち越さないよう、速度もゼロに戻します。あとか らプレイヤーに値を足したら(二段ジャンプの残り回数や、被弾後の無敵時間など)、それもここで戻します。書かなければ、落ちる前の値のまま残ります。
create_timer() の後ろの false, true は、ポーズ中は待ち時間を止める指定と、物理の処理と同じ刻みで数える指定です。あとからポーズを足しても、止めているあいだに勝手に戻りません。
dying を最後に下ろすのは、戻す処理を全部済ませたことの印にするためです。待っている途中で下ろすと、まだ穴の底にいるフレームで判定が動いて、もう一度死にます。
そのまま残すもの
リスポーン(落ちたプレイヤーを戻して、もう一度動けるようにすること)の関数に、書かないものを決めておきます。
- 拾ったコインの枚数
queue_free()したコインのノード- 開けた扉、倒した敵、動かしたスイッチ

queue_free() したノードは、もう存在しません。戻すには作り直すことになります。そこまでやるなら、シーンを読み直すほうが簡単です。読み直すとプレイヤーも作り直されるので、respawn_position は開始地点に戻ります。コインを戻したうえで旗から始めたいなら、戻り先の座標を Autoload に置きます。Autoload はシーンの外にあるので、読み直しても消えません。戻したいのがプレイヤーだけなら、この記事の形で足ります。
どこまで巻き戻すかは、ゲームの手触りを決める判断です。倒した敵を復活させるゲームもあれば、倒したままにするゲームもあります。そこを最初に決めておくと、あとから足す処理の置き場所が迷わなくなります。
実践:同じ穴に、二度落ちる
プラットフォーマーの中間地点、メトロイドヴァニアのセーブ部屋、レースの復帰地点——どれも「最後に通った印へ戻す」形です。旗を2本と穴を1つ置いて、同じ穴に2回落ちます。
① 置くもの
新しいシーンを1つ作ります。
まず旗のシーンです。ルートに Area2D を置いて Flag、checkpoint.gd を付けます。
- Flag の子に Sprite2D(
icon.svg・Scale を(0.4, 0.4)) - Flag の子に CollisionShape2D(RectangleShape2D・サイズ
(64, 96)) - Flag の子に Marker2D を置いて SpawnPoint。位置
(0, 16) Flag.tscnとして保存する
SpawnPoint を少し下にずらしているのは、旗の中心ではなく足元の高さへ戻すためです。置くのは「復帰したプレイヤーの原点を置きたい場所」で、この記事の実践のプレイヤーは、原点が円の中心にあります。床の少し上になる位置にします。
次に Main(ルートは Node2D)。
- Main の子に、動かない床や壁に使う StaticBody2D を置いて GroundLeft。位置
(200, 500)。子に CollisionShape2D(RectangleShape2D・サイズ(400, 64)) - Main の子に StaticBody2D を置いて GroundRight。位置
(900, 500)。子に同じ CollisionShape2D - 2つの床のあいだが穴になります。
xが400から700のあたりが空きます - Main の子に CharacterBody2D を置いて Player。位置
(100, 448)(床の上にちょうど立つ高さ)。player.gdを付ける - Player の子に Sprite2D(
icon.svg・Scale を(0.3, 0.3))、CollisionShape2D(CircleShape2D・半径20)、Camera2D(設定は既定のまま) - Main の子に
Flag.tscnを2つ置いて Flag1(位置(250, 430))と Flag2(位置(850, 430))。番号は、②でスクリプトを書いてから入れる - Main の子に Area2D を3つ置いて Coin1(
(320, 440))・Coin2((820, 440))・Coin3((980, 440))。それぞれcoin.gdを付け、子に Sprite2D(icon.svg・Scale を(0.2, 0.2))と CollisionShape2D(CircleShape2D・半径24) - Main の子に CanvasLayer(カメラが動いても画面に固定される層)を置いて HUD。子に Label を2つ、CoinLabel(16, 16)と FlagLabel(16, 48)。どちらも右クリックから「Access as Unique Name」をオンにする(
%CoinLabelのように名前だけで取れるようになる) - Main に
main.gdを付ける
body_entered は、それぞれ自分自身のスクリプトへつなぎます。ノードを選び、Signals ドックで body_entered をダブ ルクリックして、接続先に自分自身を選んで Connect を押します。受け取る関数の名前は _on_body_entered が既定で入り、スクリプトに空の関数が作られるので、中身は次のコードに置き換えます。旗は Flag.tscn を開いてルートの Flag で1回つなげば、置いた2本の両方に効きます。コインは Main に置いた3枚それぞれでつなぎます。
② 4つのスクリプト
本文の抜粋は、すべてこの4本の中に入っています。書くのはこの形です。
# player.gd
extends CharacterBody2D
const SPEED := 300.0
const JUMP_VELOCITY := -600.0
const KILL_Y := 800.0
const RESPAWN_DELAY := 0.5
var respawn_position := Vector2.ZERO
var dying := false
var _flag: Area2D = null
func _ready() -> void:
respawn_position = global_position # 最初の1件
%FlagLabel.text = "戻り先 開始地点"
func set_checkpoint(pos: Vector2, flag: Area2D) -> void:
if _flag == flag:
return # 同じ旗なら何もしない
if _flag != null:
_flag.turn_off() # 前の旗を消す
_flag = flag
respawn_position = pos
%FlagLabel.text = "戻り先 %d" % flag.number
func _physics_process(delta: float) -> void:
if dying:
return # 死んでいる間は何もしない
if global_position.y > KILL_Y:
dying = true
_respawn_after_delay()
return
if not is_on_floor():
velocity += get_gravity() * delta # プロジェクト設定の重力で落とす
elif Input.is_action_just_pressed("ui_accept"):
velocity.y = JUMP_VELOCITY
velocity.x = Input.get_axis("ui_left", "ui_right") * SPEED
move_and_slide()
func _respawn_after_delay() -> void:
await get_tree().create_timer(RESPAWN_DELAY, false, true).timeout
global_position = respawn_position
velocity = Vector2.ZERO
dying = false # 最後にフラグ
# checkpoint.gd
extends Area2D
@export var number := 1 # インスペクタの Number
func _on_body_entered(body: Node2D) -> void:
if not body.has_method("set_checkpoint"):
return # 受け取れない相手は無視
body.set_checkpoint($SpawnPoint.global_position, self)
$Sprite2D.modulate = Color(1.6, 1.6, 1.6) # 1より大きい値で明るく光らせる
func turn_off() -> void:
$Sprite2D.modulate = Color.WHITE
# coin.gd
extends Area2D
func _on_body_entered(body: Node2D) -> void:
if not body.has_method("set_checkpoint"):
return
get_parent().add_coin() # Main の直接の子なので
queue_free()
# main.gd
extends Node2D
var coins := 0
func _ready() -> void:
%CoinLabel.text = "コイン 0"
func add_coin() -> void:
coins += 1
%CoinLabel.text = "コイン %d" % coins
プレイヤーかどうかは has_method("set_checkpoint") で見ています。「チェックポイントを受け取れる相手か」を直接たずねる形なので、グループ名の綴りを気にせずに済みます。
set_checkpoint() の最初で同じ旗を弾いているのは、同じ旗には何度も触れるからです。戻った足元も旗の当たり判定の内側なので、戻るたびに、その旗の body_entered がもう一度届きます。
4本を書き終えたら、Main の Flag1 と Flag2 を選び、インスペクタの Number を 1 と 2 にします。@export を書いたので、この欄が出ています。
③ 動かして確かめる
床には見た目を付けていないので、Debug メニューの Visible Collision Shapes をオンにしてから、main.tscn を開いて F6 で実行します。当たり判定の形で床と穴が見えます。矢印キーで移動、スペースでジャンプです。

右へ歩いて旗1に触れると、旗1が明るくなり、HUDが「戻り先 1」に変わります。そのまま右へ進んで Coin1 を拾い、「コイン 1」になったら穴へ落ちます。穴の下の線を越えてから0.5秒で旗1の足元に戻り、HUDは「コイン 1」のままです。旗1ではなく開始地点に戻ったら、旗の body_entered がつながっていません。
次は穴を越えます。床の右端あたり(x が370を過ぎたところ)で、右を押したままジャンプします。跳ぶ瞬間に右を離すと真上に上がるだけで、向こう岸には届きません。向こう岸で Coin2 を拾い、旗2に触れると、旗2が明るくなって旗1は元の明るさへ戻り、HUDが「戻り先 2」になります。
右端の Coin3 は取らずに、左へ引き返して同じ穴にもう一度落ちます。今度は旗2の足元に戻ります。同じ穴なのに出てくる場所が変わったのは、穴ではなく、最後に触れた旗が行き先を決めているからです。拾った2枚のコインは地面に戻らず、HUDも「コイン 2」のままです。
最後に1行だけ壊します。player.gd の set_checkpoint() にある respawn_position = pos の行の頭に # を付けて、同じ手順をもう一度や ります。旗は明るくなり、HUDも「戻り先 1」「戻り先 2」と変わるのに、落ちると毎回開始地点に戻ります。旗の明かりもHUDも表示にすぎず、戻り先を決めているのはこの Vector2 1つだけです。確かめたら # を外します。

ポイントは2つです。
- 記憶は一覧ではなく、上書きされる1個: 旗を増やしても、プレイヤーが持つ Vector2 は1つのままです。「どの旗を通ったか」を全部覚える必要はありません
- 戻す処理に書かないものを決めておく: コインも扉も敵も、リスポーンの関数に出てきません。書かないことが、そのまま「戻らない」になります
うまくいかないときは
| 症状 | 原因 | 直し方 |
|---|---|---|
| Camera2D の Position Smoothing を有効にしたら、戻るたびにカメラが穴の底から滑ってくる | 追従カメラが、前の位置から新しい位置へなめらかに向かっている | 座標を代入したあとに $Camera2D.reset_smoothing() を呼ぶ。カメラの動きは Camera2Dの記事 |
| トゲ(Area2D)に触れても戻らない | トゲの側で dying = true だけ立てていて、待ち時間を始める処理を呼んでいない | フラグを立てて待ち時間を始める処理を die() のような1つの関数にまとめ、落下判定とトゲの両方から呼ぶ。die() の先頭で、dying ならすぐ抜ける |
| 下の階へ続くステージで、降りた瞬間に死ぬ | KILL_Y はステージ全体で1本の線で、下の通路まで巻き込んでいる | 下へ続くステージでは、線ではなく穴の底に置いた Area2D で死なせる |
| Project Settings の Physics → Common → Physics Interpolation をオンにしたら、戻る瞬間にプレイヤーが画面を横切る | 補間が、前の位置と新しい位置のあいだをつないで描いている | 座標を代入した直後に reset_physics_interpolation() を呼ぶ。補間は FPS管理の記事 |
おまけ:先に知っておくと良いこと
旗に触れた瞬間にHPを全快させるかどうかは、戻り先を覚えることとは別の決定です。まとめて書きたくなりますが、分けておくと「旗では回復しないが、戻ったときは全快」のような調整が効きます。全快を旗の body_entered に書くと、戻った瞬間にもう一度届くので、戻るたびに全快します。
まとめ
- 覚えているのは座標ひとつ: 旗に触れるたびに上書き される Vector2 で、一覧にも履歴にもならない
- 最初の1件は開始位置:
_ready()でglobal_positionを入れる。旗からは足元の Marker2D の座標をもらう - 落下は
yと線の比較: 毎フレーム真になるので、dyingを立てて判定ごと抜ける - 戻すのは位置・速度・フラグ: 少し待ってから代入し、フラグは最後に下ろす
- 書かなかったものは戻らない: 拾ったコインも開けた扉も、リスポーンの関数に出てこないのでそのまま
プレイヤーだけを戻すなら、覚えるのは座標ひとつで足ります。いま作っているゲームは、どこまで巻き戻すゲームでしょうか。