はじめてのGodot 第6回: 当たったらゲームオーバー — 当たり判定とif文
前回で隕石の雨が降るようになりましたが、当たってもすり抜けてしまいます。今回は衝突 → ゲームオーバーを作ります。プログラミングの要、if 文が初登場します。
今回のゴール

隕石に当たると宇宙船が消え、「GAME OVER」と表示され、隕石の出現が止まる。
隕石に「名札」を付ける — グループ
まず、衝突したとき「ぶつかった相手は隕石か?」を判定できるように、隕石に名札を付けます。Godotではこれを「グループ」と呼びます。
meteor.tscnを開き、ルートのMeteorを選択- 右側の「グループ」タブを開く
- 「+」ボタン(グループを追加)を押すと入力欄が出るので、
meteorsと入力して「OK」
このとき**「グローバル」のチェックはオフのままでOK**です。グループには「シーングループ」と「グローバルグループ」がありますが、違いは主にエディタ上の整理(他シーンからの補完など)で、実行時の is_in_group() 判定はどちらでも同じように動きます。今回はこの隕石シーン内で使うだけなのでオフで十分です。
これで、すべての隕石(から量産されるコピー)が meteors グループ所属になります。
宇宙船に当たり判定を付ける
宇宙船はまだただの画像(Sprite2D)で、当たり判定を持っていません。隕石と同じように、ノードを足して機能を足します。
main.tscnでPlayerを選択- 子に
Area2Dを追加し、名前をHitboxに変更 Hitboxの子にCollisionShape2Dを追加し、「新規CircleShape2D」で機体よりひと回り小さく形を合わせる(避けゲーは判定が小さいほど気持ちいい)
構造はこうなります:
Main
├── Player (Sprite2D)
│ └── Hitbox (Area2D)
│ └── CollisionShape2D
└── SpawnTimer (Timer)
子ノードは親と一緒に動くので、宇宙船を動かせばHitboxもついてきます。
衝突を受け取る
Hitbox を選択 → 「シグナル」タブ → area_entered(area: Area2D) をダブルクリック → 接続先に Player を選んで接続します。area_entered は「別のArea2Dが自分に入ってきた」ときに発射されるシグナルです(隕石のルートはArea2Dでしたね)。
player.gd に生えた関数と、その周辺をこう書きます(色が付いた行が、今回新しく追加する部分です)。
extends Sprite2D
signal died
var speed = 400
func _process(delta):
var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
position += direction * speed * delta
keep_inside_screen()
rotation = direction.x * 0.3
func keep_inside_screen():
var screen_size = get_viewport_rect().size
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
func _on_hitbox_area_entered(area):
if area.is_in_group("meteors"):
die()
func die():
hide()
$Hitbox/CollisionShape2D.set_deferred("disabled", true)
set_process(false)
died.emit()
新しいことが3つ
if 文 — 条件によって処理を分ける
if area.is_in_group("meteors"):
die()
「もしぶつかった相手が meteors グループなら、die() を実行する」。if の行が条件、字下げされた中身が「条件が成り立ったときだけ」実行されます。
今はぶつかる相手が隕石しかいないので if なしでも動きますが、将来アイテムを追加したら「アイテムに触れてもゲームオーバー」になってしまいます。先に相手を確かめるのが安全なコードです。
signal died — 自分でシグナルを作る
これまでTimerやArea2Dなど、ノードが持っているシグナルを使ってきました。実は、シグナルは自分でも作れます。signal died で「死んだ」という通知を宣言し、died.emit() で発射しています。
なぜわざわざシグナルにするのか? 「ゲームオーバー画面を出す」のはプレイヤーの仕事ではなくメインシーンの仕事だからです。プレイヤーは「死んだ!」と叫ぶだけ。誰がそれを聞いて何をするかは、聞く側(次のセクションのMain)が決めます。こうしておくと、プレイヤーのコードはどのゲームに持っていっても使い回せます。
set_deferred — おまじないには理由がある
$Hitbox/CollisionShape2D.set_deferred("disabled", true)
死んだ後も判定が生きていると、area_entered が何度も発射されてしまうので、当たり判定を無効化(disabled = true)しています。
ただし普通に disabled = true と書くと、エラーが出ることがあります。衝突処理の真っ最中に判定の形を変えるのは危険なので、Godotは「今のフレームの物理処理が終わってから変えてね」という書き方= set_deferred(遅延セット)を要求するのです。当たり判定まわりで disabled を触るときは set_deferred、と覚えてください。
set_process(false) は _process を止める命令です。死んだ後に操作できたら変ですからね。
Mainで「died」を受け取る
最後に、叫びを聞く側です。ここではまず見た目を気にせず、表示とシグナル接続だけを済ませます。main.tscn で:
Mainの子にLabelノードを追加し、名前をGameOverLabelに- インスペクタでTextに
GAME OVERと入力 GameOverLabelを非表示にしておく(普段は隠し、ゲームオーバー時にコードで表示します)。シーンドックでノード名の右にある目のアイコンをクリックするのが簡単です(インスペクタの CanvasItem → Visibility → Visible のチェックを外しても同じです)Playerを選択 → 「シグナル」タブ → さっき自作したdied()シグナルが一覧にいるはず → ダブルクリックしてMainに接続
main.gd に生えた関数:
func _on_player_died():
$SpawnTimer.stop()
$GameOverLabel.show()
実行して、わざと隕石に当たってみてください。宇宙船が消え、(まだ小さく地味ですが)「GAME OVER」の文字が出て、隕石が止まれば成功です(降ってる途中の隕石が落ちきるのはご愛嬌)。見た目は次で整えます。
ゲームオーバー表示を整える — Theme Overrides
いまの「GAME OVER」はデフォルトのままで小さく、位置も中途半端です。大きく・中央に表示して、ゲームオーバーらしく見せましょう。
GameOverLabelを選択し、ビューポート上でドラッグして画面中央あたりに配置- インスペクタを下にスクロールして Theme Overrides → Font Sizes を開き、「Font Size」にチェックを入れて
48くらいを入力
Theme Overrides(テーマの上書き)とは —
LabelやButtonのような画面表示用のノード(まとめてControlと呼びます)の見た目 — フォント・文字サイズ・色など — は「テーマ」という仕組みでまとめて決まっています。インスペクタにある「Theme Overrides」は、そのノード1つだけ見た目を上書きするための欄です。
もう一度実行して隕石に当たると、今度は大きな「GAME OVER」が画面中央に表示されるはずです。
つまずきポイント
- 当たっても何も起きない /
is_in_groupが常に false: ①Hitboxのarea_enteredの接続先がPlayerになっているか ②グループ名がmeteors(複数形・小文字)で一致しているか ③グループが隕石のルート(Meteor、Area2D)に付いているか(子ノードに付けると、衝突で渡されるareaはルートなので判定が外れます)の3点を確認。func _on_hitbox_area_entered(area):の先頭にprint(area.name, area.get_groups())を入れると、何がぶつかってどのグループに属しているか一目で分かります - 一瞬で何度もdieが呼ばれる:
set_deferredの行を書き忘れていませんか GameOverLabelが最初から見えている: Visibleのオフを忘れています
今回学んだこと
- グループはノードに付ける「名札」。
is_in_group()で確認できる - ノードを足せば機能が足せる(Sprite2DにHitboxを追加)
if 条件:で「条件が成り立つときだけ」処理するsignal 名前で自作シグナルを宣言し、emit()で発射する。「叫ぶ側」と「聞く側」を分けるのがGodot流- 当たり判定の
disabledはset_deferredで変える
次回予告
ゲームオーバーはできましたが、スコアがないので競えません。第7回はスコア表示とUI。CanvasLayerという新しい仲間が登場します。