【Godot】Godotのゲーム進行入門:タイトル・プレイ・ゲームオーバーをつなぎ、再挑戦で0に戻す

作成: 2026-09-22最終更新: 2026-10-10

タイトル・プレイ・ゲームオーバーをシーンの切り替えでつなぎ、再挑戦でスコアを0に戻す場所を1か所に決める方法を解説します。

タイトルから始めて、死んだらゲームオーバー、Enter で再挑戦、という1周の流れは、Godot ではシーンを切り替えて作ります。再挑戦では、HPや残り時間と一緒にスコアも0から始め直します。

ところが、スコアだけ前の数のまま残ることがあります。死んだ瞬間に0を代入すると、今度はゲームオーバー画面の点数が消えます。シーンを替えると中のノードは作り直されますが、シーンの外に置いたスコアはそのまま残るからです。

この記事では、シーンを替えたときに何が消えて何が残るのかを押さえて、スコアを0に戻す場所を1か所に決める方法を解説します。

ゲームオーバー画面のスコア10から再挑戦すると、プレイ画面の左上がスコア0・HP 3・のこり60に戻る

この記事でわかること

  • シーンの切り替えで作り直されるものと残るもの、3つの値をどこに置くか
  • スコアを0に戻す役を1か所に決める理由と、死んだときに触らない理由
  • change_scene_to_file() を呼んだ直後に何が起きているか
  • reload_current_scene() が効く場所

動作確認環境: Godot 4.7

Sponsored

シーンの切り替えで作り直されるものと残るもの

get_tree().change_scene_to_file("res://play.tscn") を呼ぶと、いまのシーンとその中のノードはすべて捨てられます。代わりに、指定したシーンが新しく作り直されます。res:// は、プロジェクトのフォルダから見たパスです。

作り直されたノードの変数は、書いたとおりの初期値から始まります。var hp := 3 と書いてあれば3です。前のプレイで0になっていても関係ありません。

残るのは、そのシーンの外にいるものです。Autoload に登録したノードがこれにあたります。ゲームが起動したときに1度だけ作られて、シーンが何度入れ替わってもそのまま居続けます。

シーンを入れ替�えると、Autoloadはそのまま残り、プレイシーンだけが中のノードごと作り直される

この違いが、そのまま「どこに置くか」の判断になります。

  • スコア: ゲームオーバー画面でも見せたいので、シーンの外(Autoload)
  • HP: プレイ中だけあればいいので、プレイシーンの変数
  • 残り時間: 同じくプレイ中だけなので、プレイシーンの Timer
scoreはAutoloadに、hpとTimerの残り時間はプレイシーンに置く。替えるとプレイシーンの中だけ作り直される

Autoload の作り方そのものは Autoloadの記事 にあります。この記事では、そこに score が1つ入っているところから始めます。

他のエンジンでいうと: シーンを丸ごと置き換える用途では、Godot の change_scene_to_file() は Unity の SceneManager.LoadScene(Single モード)、Unreal Engine の Open Level に近いものです。シーンをまたいで状態を持つ Autoload は、Unity で DontDestroyOnLoad を適用した管理役、Unreal Engine の GameInstance にあたります。

スコアを0に戻す場所

スコアはシーンの外にいるので、誰かが代入しないかぎり前の数のままです。戻す役は1か所に決めます。

置くのは、新しいプレイを始める処理です。Autoload の GameState に、score = 0 だけをする start_run() を用意します。呼ぶのは、タイトルで始めるときと、ゲームオーバーから再挑戦するときの2か所だけです。

start_run()を呼ぶのはタイトルの開始と再挑戦の2か所だけで、死んだときの矢印は途中で止まる

死んだときは呼びません。ゲームオーバー画面が見せるのは、いま終わったプレイの点数だからです。死ぬ側で0にしてしまうと、画面には毎回0が出ます。

同じ理由で、プレイシーンの _ready()(シーンがツリーに入って準備がそろったときに呼ばれる関数)でも0にしません。このデモなら動きますが、2つのステージをまたいでスコアを持ち越したくなったときに困ります。0にするのは「新しい挑戦が始まるとき」で、「プレイ画面が開くとき」ではありません。

プレイシーンの _exit_tree()(ノードがツリーから外れるときに呼ばれる関数)で0にしても同じです。古いシーンの _exit_tree() は、次のシーンの _ready() より先に走るので、ゲームオーバー画面には0が出ます。

切り替えの順番。古いシーンの_exit_tree()で0にすると、フレームの終わりに入った新しいシーンの_ready()が0を読む
Sponsored

change_scene_to_file() の直後のツリー

change_scene_to_file() の切り替えは2段階で進みます。ファイルを読み込めたら、呼んだその場で古いシーンがツリーから外れます。新しいシーンがツリーへ入るのは、そのフレームの終わりです。

そのあいだは、触れないものが2つあります。

  • いま表示中のシーンを指す current_scene が、null(何も入っていない状態)になります
  • 古いシーンのノードから get_tree() を呼ぶと、null が返ります
呼ぶ前はツリーの中にあった現在のシーンが、呼んだ直後は外へ出て、current_sceneもget_tree()もnullになる

そのため、切り替えた直後に新しいシーンのノードへ値を渡すことはできません。ゲームオーバー画面に点数を見せたいときは、スコアを Autoload に置いておき、ゲームオーバー画面の _ready() がそれを読みます。実践の game_over.gd がこの形です。

切り替えを2行続けて書くと、2行目で止まります。1行目で、書いているノードごとツリーから外れているからです。

get_tree().change_scene_to_file("res://a.tscn")
get_tree().change_scene_to_file("res://b.tscn")   # ここで get_tree() が null

2行並べて書くことはなくても、同じことは起きます。「時間切れ」と「HPが0」がほぼ同時に起きると、切り替えが実行される前に2回呼ばれることがあります。そこで「もう切り替え中」を覚える変数を1つ置いて、2回目を弾きます。実践の play.gd の _ending がそれです。この変数はプレイシーンの中にあるので、次のプレイでは false から始まります。

切り替えは call_deferred() で、当たり判定の通知が終わってから行います。call_deferred() は、呼び出しをこのフレームの終わりまで遅らせる書き方です。body_entered の中で直接切り替えると、通知を配っている当たり判定ごとツリーから外すことになります。このとき「Remove with call_deferred() instead」というエラーが出ます。

パスを間違えると ERR_CANT_OPEN が返り、画面はそのまま変わりません。読み込みの失敗はエディタ下部の Debugger の Errors タブに出ます。「ボタンを押しても何も起きない」ときは、そこを見てください。

reload_current_scene() の使いどころ

get_tree().reload_current_scene() は、いまツリーにいるシーンをもう一度読み込みます。パスを書かずに済むので再挑戦に使いたくなりますが、置く場所を選びます。

reload_current_scene()はいまいるシーンを作り直す。ゲームオーバーで呼んでもプレイには戻らない

ゲームオーバー画面で呼ぶと、ゲームオーバー画面が作り直されるだけです。プレイには戻りません。使えるのは、まだプレイシーンにいるときの「やりなおす」です。ポーズ メニューの中のリスタートがこれにあたります。

どちらの場合も、Autoload のスコアは別に0へ戻します。シーンを読み直しても、外にいるものは関係がありません。

Sponsored

実践:コインを取って、死んで、やり直す

アクションのリトライ、ローグライクの次の周回、スコアアタックの再挑戦——どれも「画面を作り直して、持ち越すものだけ決める」形です。3つのシーンを行き来しながら、置き場所の違う3つの数を見ます。

① 置くもの

3つのシーンを新しく作ります。

まず Autoload です。game_state.gd を作り、Project Settings の Globals タブ → Autoload で GameState という名前で登録します。

# game_state.gd
extends Node

var score := 0

func start_run() -> void:
	score = 0

title.tscn(ルートは Control)。

  • 子に Label を置いて、テキストは「Enter ではじめる」
  • ルートに title.gd を付ける

play.tscn(ルートは Node2D、名前は Play)。

  • Play の子に CharacterBody2D を置いて Player。位置 (200, 300)。player.gd を付ける
  • Player の子に Sprite2D(icon.svg・Scale を (0.3, 0.3))と CollisionShape2D(CircleShape2D・半径 20)
  • Play の子に Area2D を置いて Coin。位置 (500, 300)。子に CollisionShape2D(CircleShape2D・半径 24)と Sprite2D(icon.svg・Scale を (0.35, 0.35))
  • Play の子に Area2D を置いて Spike。位置 (800, 300)。子に CollisionShape2D(CircleShape2D・半径 40)と Sprite2D(icon.svg・Scale を (0.6, 0.6))
  • Play の子に Timer を置いて StageTimer。Wait Time を 60、One Shot をオン(1回で止まる)、Autostart をオン(シーンが始まると自動で数え始める)
  • Play の子に CanvasLayer(カメラが動いても画面に貼り付いて描く層)を置いて HUD。その子に Label を3つ、ScoreLabel(16, 16)・HpLabel(16, 48)・TimeLabel(16, 80)
  • Play に play.gd を付ける。3つの Label は、右クリックから「Access as Unique Name」をオンにする(%ScoreLabel のように名前で取れる)

game_over.tscn(ルートは Control)。

  • 子に Label を2つ。ScoreLabel は画面中央あたり、その下に「Enter でもう一度」の Label
  • ScoreLabel も「Access as Unique Name」をオンにする
  • ルートに game_over.gd を付ける

最後にシグナルをつなぎます。Coin を選び、Signals ドックで body_entered をダブルクリックして、接続先に Play を選んで Connect を押します。Play のスクリプトに _on_coin_body_entered が作られるので、中身は次の play.gd に置き換えます。Spike の body_entered と StageTimer の timeout も同じように Play へつなぎます。

body_entered は範囲に入った瞬間の通知なので、トゲの上に立ち止まっていてもHPは減り続けません。減らすには、いったん出てから入り直します。

② 4つのスクリプト

# title.gd
extends Control

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		GameState.start_run()                      # ここで 0 に戻す
		get_tree().change_scene_to_file("res://play.tscn")
# play.gd
extends Node2D

var hp := 3
var _ending := false

func _ready() -> void:
	_update_labels()                               # ここでは 0 にしない

func _process(_delta: float) -> void:
	%TimeLabel.text = "のこり %d" % ceili($StageTimer.time_left)   # 小数を切り上げた整数で出す

func _on_coin_body_entered(body: Node2D) -> void:
	if body != $Player:
		return
	GameState.score += 10
	$Coin.queue_free()
	_update_labels()

func _on_spike_body_entered(body: Node2D) -> void:
	if body != $Player:
		return
	hp -= 1
	_update_labels()
	if hp <= 0:
		_go_game_over()

func _on_stage_timer_timeout() -> void:
	_go_game_over()

func _go_game_over() -> void:
	if _ending:
		return                                     # 続けて来た2回目を弾く
	_ending = true
	_to_game_over.call_deferred()                  # 当たり判定の処理が終わってから

func _to_game_over() -> void:
	get_tree().change_scene_to_file("res://game_over.tscn")

func _update_labels() -> void:
	%ScoreLabel.text = "スコア %d" % GameState.score
	%HpLabel.text = "HP %d" % hp
# player.gd
extends CharacterBody2D

const SPEED := 300.0

func _physics_process(_delta: float) -> void:
	velocity = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") * SPEED
	move_and_slide()
# game_over.gd
extends Control

func _ready() -> void:
	%ScoreLabel.text = "スコア %d" % GameState.score   # 終わったプレイの点数

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		GameState.start_run()                      # ここで 0 に戻す
		get_tree().change_scene_to_file("res://play.tscn")

start_run() を呼んでいるのは title.gd と game_over.gd の2か所だけです。play.gd には1つも出てきません。

FileSystem ドックで title.tscn を右クリックし、Set as Main Scene でメインシーンにしておきます。

③ 動かして確かめる

Debug メニューの Visible Collision Shapes をオンにしてから、F5 でタイトルから起動します。トゲの当たり判定の円が見えるので、どこまで離れれば入り直しになるかが分かります。見るのは2周目です。起動した直後のスコアはもともと0なので、1周目は0に戻す処理を書き忘れていても正しく見えます。

ゲームオーバーでスコア10、Enterのあとの2周目は左上がスコア0・HP 3・のこり60に戻っている

Enter(最初から ui_accept に割り当てられているキー)でプレイへ入ります。矢印キーで動いてコインを1枚取ると、スコアが10になります。トゲに入るとHPが2に減ります。触れっぱなしでは減らないので、いったん左へ離れてから入り直し、3回入るとゲームオーバーです。ゲームオーバー画面の中央に「スコア 10」と出れば、終わったプレイの点数が残っています。0なら、死ぬ側かプレイの _exit_tree() で消しています。

Enter を押した直後に左上を見ると、スコア 0、HP 3、のこり 60 に戻っています。そのままコインを取らずにもう一度死ぬと、ゲームオーバーはスコア 0 です。10のままなら、1周目の数が残っています。

最後に1行だけ壊します。game_over.gd の GameState.start_run() の行の頭に # を付けてコメントにし、同じように1周してから再挑戦します。2周目の左上は、スコア 10、HP 3、のこり 60 です。HPとタイマーはシーンと一緒に作り直されて戻り、シーンの外にいるスコアだけが前の数のまま残ります。確かめたら # を外します。

start_run()をコメントにした2周目。HP 3とのこり60は初期値に戻り、スコア10だけが前のまま残っている

ポイントは2つです。

  • 0に戻すのは、新しい挑戦を始める処理: プレイ画面が開くときではありません。この区別があるから、ゲームオーバー画面に前のプレイの点数が残ります
  • シーンの中の変数は、放っておいても戻る: HPもタイマーも、初期値を書いておけばそれで済みます。自分で戻す必要があるのは、シーンの外へ出したものだけです

シーンを替えずに、落ちたプレイヤーだけを戻したいなら チェックポイント入門 です。ステージをまたいでスコアを持ち越したい、宝箱の開封状態を覚えたい、という話になったら Autoloadの記事 が続きです。重いステージを読み込むあいだに進捗を見せたいなら 動的ロードの記事 にあります。

うまくいかないときは

症状原因直し方
Enter を押しても画面が変わらないパスの綴りが違い、ERR_CANT_OPEN で切り替えが止まっているDebugger の Errors タブで読み込みの失敗を見る。パスは FileSystem ドックの右クリック → Copy Path で貼る
ポーズを足したら、ゲームオーバーで Enter が効かないget_tree().paused はシーンの外の設定で、シーンを替えても true のまま残り、ゲームオーバー画面の入力まで止まっているプレイ側の _to_game_over() で、切り替える直前に get_tree().paused = false に戻す。ゲームオーバー側の Enter を受ける処理に書いても、その処理ごと止まっているので効かない
ハイスコアまで0に戻るstart_run() でハイスコアも0にしている1回のプレイだけの score と、次へ持ち越す high_score を分け、start_run() では score だけ戻す
画面の切り替えに入れた黒いフェードが、途中で消えるフェードの板を、入れ替わるシーンの中に置いている板は Autoload など、残る側に置く

おまけ:先に知っておくと良いこと

Autoload に置いたものも、ゲームを閉じれば消えます。次に起動したときにも残したいハイスコアは、ファイルに保存します。やり方は セーブの記事 にあります。

ステージが増えたら、いま何面かを表す stage も GameState に置きます。これも次のシーンへ持ち越すものです。1に戻すのは score と同じく start_run() だけです。ステージをクリアしたときは、1つ進めるだけにします。

Sponsored

まとめ

  • シーンを替えると中は作り直される: HPもタイマーも、書いてある初期値から始まるので戻す必要はない
  • 残るのはシーンの外だけ: Autoload に出したスコアは、誰かが0を代入するまで前の数のまま
  • 0に戻すのは新しい挑戦を始める処理: タイトルの開始と再挑戦の2か所。死んだときには触らない
  • 呼んだ直後に古いシーンはもういない: current_scene は null、古いノードからは get_tree() も返らない
  • 切り替えは1つの関数で1回だけ: 続けて来た2回目を弾き、call_deferred() で当たり判定の通知のあとに替える

持ち越さないものは、シーンの中に置いておくだけで片付きます。いま作っているゲームで、次の挑戦へ持ち越したいものはどれでしょうか。

次に読む学習パスの次の記事
シーン構成

Godotのチェックポイント入門:落ちたら最後の旗へ戻し、拾ったコインは戻さない

この記事を読む
Godot このセクションのノート104