はじめてのGodot 第6回: 当たったらゲームオーバー — 当たり判定とif文


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

今回のゴール

宇宙船が隕石に衝突して消え、GAME OVERが表示される様子

隕石に当たると宇宙船が消え、「GAME OVER」と表示され、隕石の出現が止まる。

隕石に「名札」を付ける — グループ

まず、衝突したとき「ぶつかった相手は隕石か?」を判定できるように、隕石に名札を付けます。Godotではこれを「グループ」と呼びます。

  1. meteor.tscn を開き、ルートの Meteor を選択
  2. 右側の「グループ」タブを開く
  3. 「+」ボタン(グループを追加)を押すと入力欄が出るので、meteors と入力して「OK」

このとき**「グローバル」のチェックはオフのままでOK**です。グループには「シーングループ」と「グローバルグループ」がありますが、違いは主にエディタ上の整理(他シーンからの補完など)で、実行時の is_in_group() 判定はどちらでも同じように動きます。今回はこの隕石シーン内で使うだけなのでオフで十分です。

これで、すべての隕石(から量産されるコピー)が meteors グループ所属になります。

宇宙船に当たり判定を付ける

宇宙船はまだただの画像(Sprite2D)で、当たり判定を持っていません。隕石と同じように、ノードを足して機能を足します

  1. main.tscnPlayer を選択
  2. 子に Area2D を追加し、名前を Hitbox に変更
  3. 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 で:

  1. Main の子に Label ノードを追加し、名前を GameOverLabel
  2. インスペクタでTextに GAME OVER と入力
  3. GameOverLabel非表示にしておく(普段は隠し、ゲームオーバー時にコードで表示します)。シーンドックでノード名の右にある目のアイコンをクリックするのが簡単です(インスペクタの CanvasItem → Visibility → Visible のチェックを外しても同じです)
  4. Player を選択 → 「シグナル」タブ → さっき自作した died() シグナルが一覧にいるはず → ダブルクリックして Main に接続

main.gd に生えた関数:

func _on_player_died():
	$SpawnTimer.stop()
	$GameOverLabel.show()

実行して、わざと隕石に当たってみてください。宇宙船が消え、(まだ小さく地味ですが)「GAME OVER」の文字が出て、隕石が止まれば成功です(降ってる途中の隕石が落ちきるのはご愛嬌)。見た目は次で整えます。

ゲームオーバー表示を整える — Theme Overrides

いまの「GAME OVER」はデフォルトのままで小さく、位置も中途半端です。大きく・中央に表示して、ゲームオーバーらしく見せましょう。

  1. GameOverLabel を選択し、ビューポート上でドラッグして画面中央あたりに配置
  2. インスペクタを下にスクロールして Theme Overrides → Font Sizes を開き、「Font Size」にチェックを入れて 48 くらいを入力

Theme Overrides(テーマの上書き)とはLabelButton のような画面表示用のノード(まとめて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流
  • 当たり判定の disabledset_deferred で変える

次回予告

ゲームオーバーはできましたが、スコアがないので競えません。第7回はスコア表示とUI。CanvasLayerという新しい仲間が登場します。

第7回: スコアを表示しよう — UIの基本