第9回目の今回は、ゲームオーバーの仕組みを実装していく。

世に出ているプラットフォーマーゲームにはほぼ確実にゲームオーバーの仕組みが存在する。ゲームオーバーになるかもしれない緊張感がなければプラットフォーマーの面白さは激減する。だから、ゲームオーバーの実装は必要不可欠だ。さらに、ゲームオーバーの条件設定もまたゲームの難易度を左右するため非常に重要だ。

一般的にゲームオーバーの条件としては、敵や敵の攻撃に一回でも当たるか、当たってヘルス(ライフ)が 0 になるか、画面下部に落下した時などにゲームオーバーになることが多い。

今回のチュートリアルでは、ゲームーオーバーの仕組みとして以下の作業を行う。

  • ゲームオーバー画面のシーンを作る。
  • ゲームオーバー画面から再度ゲーム開始またはゲーム終了する仕組みを作る。
  • プレイヤーキャラクターが敵キャラクターに当たってヘルスが 0 になった時のゲームオーバー画面への遷移を実装する。
  • プレイヤーキャラクターが地面のないところから画面外へ落下した時のゲームオーバー画面への遷移を実装する。

Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るプラットフォーマー



GameOver シーンを作る

新規シーンを作成して必要なノードを追加する

それではまず以下の手順で「GameOver」シーンを新規作成しよう。

  1. 「シーン」メニュー>「新規シーン」を選択する。
  2. 「ルートノードを生成」で「ユーザーインターフェース」を選択する。
  3. 「Control」ルートノードが追加されたら、名前を「GameOver」に変更する。
  4. 「GameOver」ルートノードに「TextureRect」ノードを追加する。
  5. 「TextureRect」ノードに「VBoxContainer」ノードを追加する。
  6. 「VBoxContainer」ノードに「Label」ノードを追加し、名前を「GameOverLabel」に変更する。
  7. 「VBoxContainer」ノードに「HBoxContainer」ノードを追加し、名前を「ButtonsHBox」に変更する。
  8. 「ButtonsHBox」ノードに「VBoxContainer」ノードを2つ追加し、名前をそれぞれ「RestartVBox」、および「QuitVBox」に変更する。
  9. 「RestartVBox」ノードに「TextureButton」ノードを追加し、名前を「RestartButton」に変更する。
  10. 同じく「RestartVBox」ノードに「Label」ノードを追加し、名前を「RestartLabel」に変更する。
  11. 「QuitVBox」ノードに「TextureButton」ノードを追加し、名前を「QuitButton」に変更する。
  12. 同じく「QuitVBox」ノードに「Label」ノードを追加し、名前を「QuitLabel」に変更する。
  13. 「GameOver」ルートノードに「ColorRect」ノードを追加する。
  14. さらに「GameOver」ルートノードに「ConfirmationDialog」ノードを追加する。

ひとまずここまでで「GameOver」のシーンツリーは以下のようになった。
GameOverシーンツリー

シーンドックを見ると「ConfirmationDialog」ノードの右側に警告マークがついている。これをクリックすると、以下のダイアログが表示される。(日本語のローカライズ時にタイポがあった様子だが)このノードは基本的に非表示になるようだ。
ConfirmationDialogの警告ダイアログ

シーンドック上でこのノードの右側にある目のアイコンをクリックして非表示に切り替えれば、警告は消えるが、2Dワークスペース上にこのノードは表示されなくなる。お好みで表示/非表示を切り替えてほしい。
ConfirmationDialogを非表示


さらに「Control」系ではないクラスのノードを2つ追加する。

  1. 「GameOver」ルートノードに「Line2D」ノードを追加する。
  2. 「Line2D」ノードに「AnimationPlayer」ノードを追加する。

これでシーンツリーは以下のようになった。
ConfirmationDialogを非表示

ここまでできたらシーンを保存しておこう。とその前に、「GameOver」シーンは UI なので、このタイミングでファイルシステム上のフォルダ構成を編集しておく。「res://UI/」に「HUD」フォルダを作成し、以前作成した以下の HUD 関連ファイルをそこへ移動しよう。

  • res://UI/HUD/HUD.gd
  • res://UI/HUD/HUD.tscn

さらに「res://UI/」に「GameOver」フォルダを作成し、今作ったシーンのファイルパスが「res://UI/GameOver/GameOver.tscn」となるように「GameOver.tscn」という名前で保存しよう。


各ノードのプロパティを編集する

では「GameOver」シーンのそれぞれのノードのプロパティを編集していこう。


GameOver

GameOverノード

この「GameOver」シーンで、ルートノードは単なる子ノードの入れ物的役割にしているが、プロパティを一つ更新しておく。これは非常に重要だ。

  1. シーンドックで「GameOver」ルートノードを選択し、インスペクターで「Pause Mode」プロパティを「Process」に変更する
    GameOverのPause ModeをProcessにする

この設定の意味を説明しておこう。

ゲームオーバー画面を表示している間、メインのゲームは一時停止状態にしたい。その場合、「Game」ノードの「Paused」というプロパティを true にすることで、ゲームは一時停止させられる。

しかし、基本的に全てのノードの「Pause Mode」プロパティは「Inherit」(継承という意味)の設定になっているので、親ノードの「Pause Mode」の値を継承する。つまり、ゲームを一時停止させようと思って、シーンツリーの最上位にある「Game」ノードの「Paused」プロパティを true にすると、ゲームオーバー画面を含むゲームの全てが停止してしまう。

そこで、「GameOver」ブランチのみ機能させたい場合に、このノードの「Pause Mode」プロパティを「Process」に変更する。そうすると、親ノードの「Pause Mode」は継承されず、裏でゲームが停止中でもゲームオーバー画面だけは動作し操作可能となるのだ。

公式オンラインドキュメント:
ゲームの一時停止


TextureRect

TextureRectノード

このノードはゲームオーバー画面の背景テクスチャを設定するためのものだ。

  1. 「TextureRect」ノードを選択したら、2Dワークスペースのツールバー>「レイアウト」にて「Rect全面」を選択する。
  2. インスペクターの「Texture」プロパティの [空] 目掛けて、ファイルシステムドックから「res://Assets/Background/Gray.png」をドラッグ&ドロップする。
  3. 「Stretch Mode」プロパティを「Tile」に設定する。

以上で、グレーの菱形模様の背景が設定できた。
TextureRectデバッグ

公式オンラインドキュメント:
TextureRect


VBoxContainer

VBoxContainerノード

このノードは自身の子ノードを垂直方向に並べるコンテナだ。子ノードを全て中央揃えにしたいので、設定は以下のみ。

  1. インスペクターで「Alignment」プロパティを「Center」にする。

公式オンラインドキュメント:
VBoxContainer


GameOverLabel

GameOverLabelノード

このノードは「Game Over」という文字列を画面上に大きく表示するために使用する。

  1. インスペクターにて、「Text」プロパティの値として「game over」を入力する。
  2. 「Align」プロパティを「Center」にする。
  3. 「Uppercase」プロパティをオンにする。
  4. 「Theme Overrides」>「Fonts」>「Font」プロパティに「新規 DynamicFont」を割り当てる。
  5. 「Theme Overrides」>「Fonts」>「Font」プロパティの追加したフォントをクリックし、「Font」>「Font Data」プロパティめがけて、ファイルシステムドックからフォントファイル「res://fonts/connection_ii/ConnectionII.tres」をドラッグ&ドロップする。
  6. 「Theme Overrides」>「Fonts」>「Font」プロパティの追加したフォントをクリックし、「Settings」>「Size」プロパティを 64 にする。
    GameOverLabelノードのプロパティ
  7. 「Visibility」>「Modulate」プロパティで色を # e44a4a (赤)に変更する。
    GameOverLabelノードのプロパティ

これで、「Game Over」の赤い文字が中央揃えで大きく表示された。フォントも「ConnectionII.tres」により、レトロ感を演出できた。
シーンを実行してGameOverLabelノードの確認

公式オンラインドキュメント:
Label


ButtonsHBox

ButtonsHBoxノード

このノードは、子ノードである「RestartVBox」と「QuitVBox」を水平方向に格納するコンテナだ。それら2つの子ノードもまたコンテナだが、いずれも主な要素はボタンである。それらのボタンを2つ横並びに表示するためにこのノードを利用する。

  1. インスペクターで「Alignment」プロパティを「Center」にする。
  2. 「Theme Overrides」>「Constants」>「Separation」プロパティの値を 48 にする。

これでこのノードが中央揃えで配置され、格納する子ノードの間隔が 48 px で横並びになる。

公式オンラインドキュメント:
HBoxContainer


RestartVBox

RestartVBoxノード

このノードは、ゲームオーバーになった時に、ゲームをはじめから再開するための「Restart」ボタンとその説明用のラベルを格納するためのものだ。それらの子ノードは中央揃えにして、ボタンアイコンが上、説明用ラベルが下になるように並べる。VBox のため子ノードは自動的に縦並びに配置される。プロパティの編集は以下の1つだけだ。

  1. インスペクターで「Alignment」プロパティを「Center」にする。

公式オンラインドキュメント:
VBoxContainer


RestartButton

RestartButtonノード

このノードは「Restart」ボタンのアイコンを作るために使用する。「TextureButton」クラスは、ボタン機能を有しており、かつ見た目の設定としてテクスチャ画像ファイルを適用できる。

  1. インスペクターで「Expand」プロパティをオンにする。
  2. 「Stretch Mode」プロパティを「Keep Aspect Center」にする。
  3. 「Textures」>「Normal」プロパティに、ファイルシステムドックから「res://Assets/Menu/Buttons/Restart.png」をドラッグ&ドロップする。
    なお、ボタンを押した時のテクスチャを設定する「Pressed」、カーソルがボタンに重なった時のテクスチャ「Hover」など細かく設定可能だが、今回はアセットの種類の関係で、「Normal」のみ設定する。
  4. 「Rect」>「Min Size」プロパティを (56, 56) に設定する。

現在、「Restart」ボタンが水平方向で中央に配置されているが、後ほど「Quit」ボタンのテクスチャを設定すれば、2つのボタンの中間が中央に来るように自動的に調整されるので、現時点ではこのままで良い。
RestartButtonノードの確認

公式オンラインドキュメント:
TextureButton


RestartLabel

RestartLabelノード

このノードは単なるボタンアイコンの説明だ。アイコンだけだと何のボタンかわかりにくいかもしれないので、ボタンの下に小さめの文字で「RESTART」と表示するために使用する。

  1. インスペクターで「Text」プロパティの値として「restart」と入力する。
  2. 「Align」プロパティを「Center」にする。
  3. 「Uppercase」プロパティをオンにする。
  4. 「Theme Overrides」>「Fonts」>「Font」プロパティに「新規 DynamicFont」を割り当てる。
  5. 「Theme Overrides」>「Fonts」>「Font」プロパティの追加したフォントをクリックし、「Font」>「Font Data」プロパティめがけて、ファイルシステムドックからフォントファイル「res://fonts/poco/Poco.tres」をドラッグ&ドロップする。
  6. 「Font」>「Font Data」プロパティの矢印「v」をクリックし「ユニーク化」する。
    RestartLabelノードのプロパティ
  7. 「Visibility」>「Modulate」プロパティで色を # 000000 (黒)に変更する。

RestartLabelノードの確認

公式オンラインドキュメント:
Label


QuitVBox

QuitVBoxノード

このノードは、ゲームオーバーになった時に、ゲームを完全に終了するための「Quit」ボタンと説明用のラベルを格納するためのものだ。「RestartVBox」同様だが、それらの子ノードは中央揃えにして、ボタンアイコンが上、説明用ラベルが下になるように並べる。VBox なので、子ノードは自動的に縦並びに配置される。プロパティの編集は以下の1つだけだ。

  1. インスペクターで「Alignment」プロパティを「Center」にする。

公式オンラインドキュメント:
VBoxContainer


QuitButton

QuitButtonノード

このノードは「RestartButton」ノードと同様に「Quit」ボタンのアイコンを作るために使用する。

  1. インスペクターで「Expand」プロパティをオンにする。
  2. 「Stretch Mode」プロパティを「Keep Aspect Center」にする。
  3. 「Textures」>「Normal」プロパティに、ファイルシステムドックから「res://Assets/Menu/Buttons/Close.png」をドラッグ&ドロップする。このノードもテクスチャは「Normal」プロパティのみ設定する。
  4. 「Rect」>「Min Size」プロパティを (56, 56) に設定する。

「Quit」ボタンの垂直方向の位置が「Restart」ボタンより低いが、この後「QuitLabel」ノードの設定をすれば同じ高さになるので、今はこのままで良い。
QuitButtonノードの確認

公式オンラインドキュメント:
TextureButton


QuitLabel

QuitLabelノード

このノードは、「RestartLabel」ノード同様、単なるボタンアイコンの説明だ。

  1. インスペクターで「Text」プロパティの値として「quit」と入力する。
  2. 「Align」プロパティを「Center」にする。
  3. 「Uppercase」プロパティをオンにする。
  4. 「Theme Overrides」>「Fonts」>「Font」プロパティに「新規 DynamicFont」を割り当てる。
  5. 「Theme Overrides」>「Fonts」>「Font」プロパティの追加したフォントをクリックし、「Font」>「Font Data」プロパティめがけて、ファイルシステムドックからフォントファイル「res://fonts/poco/Poco.tres」をドラッグ&ドロップする。
  6. 「Font」>「Font Data」プロパティの矢印「v」をクリックし「ユニーク化」する。
    RestartLabelノードのプロパティ
  7. 「Visibility」>「Modulate」プロパティで色を # 000000 (黒)に変更する。

QuitLabelノードの確認

公式オンラインドキュメント:
Label


ColorRect

ColorRectノード

この後「Restart」または「Quit」のボタンを押した時にダイアログが表示されるようにするのだが、ダイアログに注目してもらうために、その時だけダイアログの後ろにあるゲームオーバー画面を暗くしたい。そこで、この「ColorRect」ノードで半透明の黒いスクリーンを作り、ゲームオーバー画面に重ねることで、後ろのゲームオーバー画面が暗くなったように見せる。

  1. 2Dワークスペースのツールバー>「レイアウト」>「Rect全面」を選択する。
  2. インスペクターから「Color」プロパティで色を # a0000000 (R:0, G:0, B:0, A: 160)に設定する。
    ColorRectノードの確認
  3. このノードは、デフォルトでは非表示にしておき、ボタンを押した時だけ表示したいので、ひとまずシーンドック上で目のアイコンをクリックして非表示にしておく。
    ColorRectノードを非表示

公式オンラインドキュメント:
ColorRect


ConfirmationDialog

ConfirmationDialogノード

Confirmation とは日本語で確認という意味だ。このノードはプレイヤーに対してダイアログで確認のメッセージを表示し、それに対して OK / Cancel のボタンを選択してもらうために使用する。「Restart」ボタンも「Quit」ボタンも、誤って押してしまった場合を考慮して、ボタンを押したら、本当にそれで良いのかプレイヤーに確認するダイアログが表示されるようにする。

  1. インスペクターで「Dialog」>「Text」プロパティには何も入れずに置いておく。後ほど、スクリプトでどっちのボタンを押したかによって、このプロパティの値を切り替えるようにする。
  2. 「Dialog」>「Hide On OK」プロパティはオフにする。ダイアログ上の「OK」ボタンをクリックした時の挙動をスクリプトで制御したいのでここはオフにする必要がある。
  3. 「Window Title」には「Confirmation」と入力する。これは単にダイアログ上部のタイトルだ。
  4. 「Popup」>「Exclusive」プロパティはオフにする。これがオンだとダイアログの外をクリックすればキャンセル扱いになって便利ではある。しかし残念ながら、ダイアログのすぐ下あたりに「Restart」と「Quit」のボタンがあるため、ダイアログをキャンセルしようと思って外側をクリックしたつもりが、ボタンの上をクリックしてしまい、すぐさまダイアログが再表示されるという挙動になってしまう。ダイアログのサイズを大きくして背後のボタンを隠しても良いが、少し見た目に違和感があるため、今回はこのプロパティをオフにして対処することにした。
  5. 「Rect」>「Min Size」を(200, 50)に設定する。

公式オンラインドキュメント:
ConfirmationDialog


Line2D

Line2Dノード

このノードは選択しているボタンを囲う四角い枠を作るために利用する。「Line2D」は線を描画するためのノードだが、任意の数の点を打ち、その点を通る線を描画できる。つまり、長方形の角にあたる点を指定し、最初の点と最後の点が重なるようにすれば長方形になる。

なお、四角い枠を作るなら「Sprite」ノードなどでテクスチャ画像を適用して利用する方法が早そうだが、アセットがなくても四角い枠くらいはノードだけで作成できるので、今回は敢えてこの「Line2D」というクラスのノードを利用する。

  1. インスペクターで、「Node2D」ラクスの「Position」プロパティの値を (108, 128) に変更する。これはノードの位置を「RestartButton」に合わせるためだ。
    Line2Dノードプロパティ
  2. 次に、線の幅である「Line2D」クラスの「Width」プロパティを 4 にする。
  3. 「Default Color」プロパティを # ffffff(白)にする。
  4. 「Points」プロパティに適用されている「PoolVector2Array」をクリックし、「サイズ」プロパティを 5 にする。これは線が通る点の数だ。
  5. 続けて、それぞれの Point(0 ~ 4)の位置を以下の通りに設定する。
    0: (0, 0)
    1: (64, 0)
    2: (64, 64)
    3: (0, 64)
    4: (0, -2)
    左上の角が欠けた枠にならないように、Point 4 の位置を「Width」プロパティの半分にあたる 2 px だけ最初の点より上に設定した。
    Line2Dノードプロパティ

これで画面は以下のようになったはずだ。
シーンを実行

公式オンラインドキュメント:
Line2D


AnimationPlayer

AnimationPlayerノード

このノードは単に「Line2D」で作成した四角い枠を点滅させるために利用する。点滅させることで、プレイヤーに注目させ、四角い枠でボタンを選択することを促すことが狙いだ。

インスペクターのプロパティは元々以下のようになっているはずなので、そのままで良い。
AnimationPlayerノードプロパティ

アニメーションを一つ、以下の内容で作成しよう。

  • アニメーションの名前: blink_square
  • アニメーションの長さ(秒): 2
  • アニメーションループ: 有効
  • トラック: 「Line2D」ノード >「default color」プロパティ
    • Time: 0 秒 / Value: # ffffff / Easing: 1.00
    • Time: 1 秒 / Value: # e44a4a / Easing: 1.00
    • Time: 2 秒 / Value: # ffffff / Easing: 1.00

アニメーションを再生してみよう。
AnimationPlayerアニメーション再生

公式オンラインドキュメント:
AnimationPlayer


以上で各ノードのプロパティの編集は完了だ。次はスクリプトで動的な制御を施していく。


GameOver ノードにスクリプトをアタッチする

それでは「GameOver」ルートノードにスクリプトを新規でアタッチしよう。ファイルパスを「res://UI/GameOver/GameOver.gd」としてスクリプトファイルを作成してほしい。

スクリプトエディタで「GameOver.gd」を開いたら、まずは以下のコードを記述しよう。

extends Control # Added @ Part 9

# どちらのボタンをクリックしたかわかるようにボタンの種類をenumで定義
enum {
	RESTART,
	QUIT,
}

# Line2D の四角い枠が2つのボタン囲う位置の配列
var square_pos = [
	# Restart ボタンを囲う位置
	Vector2(108, 128),
	# Quit ボタンを囲う位置
	Vector2(212, 128)
]

# 上のenumで定義したパラメータを格納する(ボタン押下前は null)
var selected_option = null
# Restart ボタンを押した時に ConfirmationDialog の dialog_text プロパティに適用するテキスト
var restart_text = "Do you really want to restart the game?"
# Quit ボタンを押した時に ConfirmationDialog の dialog_text プロパティに適用するテキスト
var quit_text = "Do you really want to quit the game?"
# RestartButton ノードの参照
onready var restart_btn = $VBoxContainer/ButtonsHBox/RestartVBox/RestartButton
# QuitButton ノードの参照
onready var quit_btn = $VBoxContainer/ButtonsHBox/QuitVBox/QuitButton
# ColorRect ノードの参照
onready var color_rect = $ColorRect
# ConfirmationDialog ノードの参照
onready var confirmation = $ConfirmationDialog
# Line2D ノードの参照
onready var line2d = $Line2D
# AnimationPlayer ノードの参照
onready var anim_player = $Line2D/AnimationPlayer


func _ready():
	# ゲーム開始時は ColorRect ノードを非表示にする
	color_rect.visible = false
	# ゲーム開始時に AnimationPlayer のblink_square アニメーションを再生する
	anim_player.play("blink_square")

ここから必要なシグナルをこのスクリプトに接続していく。

まずはマウスカーソルが「Restart」ボタンまたは「Quit」ボタンに重なった時に「Line2D」の四角い枠をそのボタンの方に移動させる。これには「RestartButton」ノード、および「QuitButton」ノードの「mouse_entered()」シグナルを利用する。さっそくシグナルを「GameOver.gd」スクリプトに接続しよう。
bmouse_enteredシグナルを接続

接続したら、生成されたメソッドにそれぞれ「Line2D」の位置を指定するコードを記述する。具体的には以下のようなコードになる。

func _on_RestartButton_mouse_entered():
	line2d.position = square_pos[0]


func _on_QuitButton_mouse_entered():
	line2d.position = square_pos[1]

次に「Restart」ボタンまたは「Quit」ボタンをクリックした時にシグナルを発信させ、それをトリガーにして「ConfirmationDialog」ノードのダイアログを表示させる。

では「RestartButton」ノードおよび「QuitButton」ノードの「button_up()」シグナルを「GameOver.gd」スクリプトに接続しよう。
button_upシグナルを接続


ちなみに細かい話だが、このシグナルはボタンをクリックした時にクリックした指が離れたら発動される。逆にクリックした指が押し続けている間は発動しない。

シグナルを接続して追加されたメソッドをそれぞれ以下のように編集しよう。

func _on_RestartButton_button_up():
	work_at_button_up(RESTART)


func _on_QuitButton_button_up():
	work_at_button_up(QUIT)

「Restart」ボタンまたは「Quit」ボタンをクリックしたら、work_at_button_upメソッドが呼ばれるようにした。このメソッドはあとで定義する。引数には定義済みの enum のパラメータを取る形式にする。


では、どちらのボタンをクリックした時も呼ばれるwork_at_button_upメソッドを定義しよう。メソッドにはボタンをクリックした時に実行してほしい処理をコーディングしていく。

# ボタンをクリックした時に呼ばれるメソッドを定義
func work_at_button_up(option):
	# 押したボタンに応じた enum パラメータを保持する引数 option を渡す
	selected_option = option
	# ColorRect ノードを表示して後ろを暗くする
	color_rect.visible = true
	# もし Restart ボタンが押された場合
	if selected_option == RESTART:
		# ConfirmationDialog ノードの Text プロパティに restart_text プロパティの文字列を適用する
		confirmation.dialog_text = restart_text
	# もし Quit ボタンが押された場合
	elif selected_option == QUIT:
		# ConfirmationDialog ノードの Text プロパティに quit_text プロパティの文字列を適用する
		confirmation.dialog_text = quit_text
	# ConfirmationDialog を画面中央に表示する
	confirmation.popup_centered()

これで、どちらのボタンを押しても、必要な設定を施したダイアログが表示されるようになった。


次に「ConfirmationDialog」ノードのダイアログが表示された後の操作を制御していこう。そのためにはまずこのノードのシグナルを利用する。「ConfirmationDialog」ノードの「confirmed()」シグナルを「GameOver.gd」スクリプトに接続しよう。
confirmedシグナルを接続

このシグナルは、ダイアログ上の「OK」ボタンをクリックした時、または space か enter キーを押した時に発信される。つまり、「Restart」か「Quit」の選択を確定した時に発信されるのだ。

_on_ConfirmationDialog_confirmedメソッドが生成されたら、その中に、選択を確定した時に必要な処理をコーディングしていこう。

func _on_ConfirmationDialog_confirmed():
	# Restart ボタンを押した場合
	if selected_option == RESTART:
		# シーンツリーの一時停止を再開する(一時停止は Game.gd 側に実装予定)
		get_tree().paused = false
		# デバッグ用
		print("Scene tree paused: ", get_tree().paused)
		# Game.tscn シーンを読み込み直し、ゲームをはじめから再開する
		get_tree().change_scene("res://Game/Game.tscn")
		# デバッグ用
		print("The game is restarted.")
	# Quit ボタンを押した場合
	elif selected_option == QUIT:
		# ゲームを完全に終了する
		get_tree().quit()
		# デバッグ用
		print("The game is quited.")

これで「Restart」ボタン、「Quit」ボタンをクリックして選択を確定したあとの挙動が実装できた。


続いて「Cancel」ボタンをクリックしてダイアログを閉じる時の挙動を制御していく。こちらもまずはシグナルを接続するところから始める。「ConfirmationDialog」ノードの「popup_hide()」シグナルをスクリプトに接続しよう。
popup_hideシグナルを接続


_on_ConfirmationDialog_popup_hideメソッドが生成されたら以下のように編集しよう。ダイアログが表示された時の設定を元に戻すようにコーディングしている。

func _on_ConfirmationDialog_popup_hide():
	# ボタン選択ステータスをリセットする(値なし状態にする)
	selected_option = null
	# ConfirmationDialog ノードの dialog_text プロパティのテキストを消す
	confirmation.dialog_text = ""
	# ColorRect ノードを非表示に戻す
	color_rect.visible = false

最後に_inputメソッドを追加する。このメソッドを利用すると、入力イベントを利用してゲームを制御することができる。ここまででマウス操作による制御は実装できているが、ゲームをキーボードでプレイしていることを考えると、そのままキーボードで操作できた方が良いので、キーボード入力に対してこのメソッドを利用して制御しようというわけだ。ユーザーエクスペリエンスに配慮するのはゲーム開発者として大事なことなのだ。

func _input(event):
	# もし GameOver ノードが表示されていたら(ゲームオーバーになったら)
	if visible:
		# もし左矢印キーが押されてキーから指が離れたら
		if event.is_action_released("ui_left"):
			# Line2D の四角い枠を Restart ボタンの方に配置する
			line2d.position = square_pos[0]
		# もし右矢印キーが押されてキーから指が離れたら
		elif event.is_action_released("ui_right"):
			# Line2D の四角い枠を Quit ボタンの方に配置する
			line2d.position = square_pos[1]
		# もし space キーか enter キーが押されて指が離れてかつ ColorRect が非表示(まだダイアログが非表示)だったら
		elif event.is_action_released("ui_accept") and not color_rect.visible:
			# もし Line2D の四角い枠が Restart ボタンの方に配置されていたら
			if line2d.position == square_pos[0]:
				# Restart ボタンをクリックした時のメソッドを実行
				_on_RestartButton_button_up()
			# もし Line2D の四角い枠が Quit ボタンの方に配置されていたら
			elif line2d.position == square_pos[1]:
				# Quit ボタンをクリックした時のメソッドを実行
				_on_QuitButton_button_up()

シーンを実行して挙動を確認しておこう。
popup_hideシグナルを接続

良い感じである。ここらで一度休憩しよう。


GameOver シーンをインスタンス化して Game シーンに追加する

出来上がった「GameOver」シーンをインスタンス化して「Ggame」シーンツリーにブランチとして追加しよう。
popup_hideシグナルを接続


Game.gd スクリプトを編集する

ゲームオーバーになって「Restart」を選択した場合、「Game.tscn」を読み込み直すようにコーディングしたが、この時「Level_」シーンも解放される。解放時にシグナル「tree_exited」が発信され、「Game.gd」スクリプト内でこのシグナルが接続されているchange_levelメソッドが呼ばれる。しかし、ゲームオーバーなので、このメソッドで次のレベルに遷移させる必要はないし、最後のレベルだった場合にはゲームクリアの判定になる上、get_treeメソッドを実行しようとしても解放されて「/root/」を取得できずnullが返され、エラーとなる。

これを回避するため「Game.gd」スクリプトのchange_levelメソッドを以下のように編集する。

func change_level():
	if get_tree(): # Added @ Part 9
		print("change_level() called.")
		if current_level < final_level:
			print("change to next level.")
			level.queue_free()
			current_level += 1
			hud.update_level(current_level) # Added @ Part 8
			add_level()
		else:
			print("Game Clear! Congrats!")
			get_tree().quit()


プレイヤーのヘルスが 0 になったらゲームオーバーになる仕組みを実装する

ここからはスクリプトをコーディングして、プレイヤーのヘルスが 0 になった時にゲームオーバーになる仕組みを実装していく。


プレイヤーのヘルスが 0 になった時のアニメーションを作る

プレイヤーのヘルスが 0 になった時のアニメーションを追加したい。

久しぶりに「Player.tscn」シーンを開き、エディタ下部のアニメーションパネルを開いたら、以下の内容で新しい「die」という名前のアニメーションを作成しよう。

  1. アニメーションの名前: die
  2. アニメーションの長さ(秒):0.9
  3. 以下のトラックを追加
  • トラック 1: AnimatedSprite ノード > scale プロパティ
    • Time: 0秒 / Value: (0, 0) / Easing: 50
    • Time: 0.9秒 / Value: (10, 0) / Easing: 1.00
  • トラック 2: AnimatedSprite ノード > modulate プロパティ
    • Time: 0秒 / Value: # ffffff / Easing: 1.00
    • Time: 0.1秒 / Value: # 00ffffff / Easing: 1.00
    • Time: 0.2秒 / Value: # ffffff / Easing: 1.00
    • Time: 0.3秒 / Value: # 00ffffff / Easing: 1.00
    • Time: 0.4秒 / Value: # ffffff / Easing: 1.00
    • Time: 0.5秒 / Value: # 00ffffff / Easing: 1.00
    • Time: 0.6秒 / Value: # ffffff / Easing: 1.00
    • Time: 0.7秒 / Value: # 00ffffff / Easing: 1.00
    • Time: 0.8秒 / Value: # ffffff / Easing: 1.00
    • Time: 0.9秒 / Value: # 00ffffff / Easing: 1.00
    • 補完モード: 近傍
      トラック:補完モード:近傍

アニメーションパネルは以下のようになったはずだ。
PlayerシーンAnimationPlayerノードアニメーションパネル

アニメーションの内容としては、まず modulate プロパティのトラックにて、プレイヤーキャラクターの透明度を 0 と 100 の間で 0.1 秒周期で切り替え、明滅したように見せる。さらに scale プロパティのトラックにては最後の 0.9 秒で一気に横に引き伸ばしつつ縦に潰すことで、プレイヤーキャラクターのテクスチャが消える。

アニメーションを確認してみよう。
PlayerシーンAnimationPlayerノードアニメーション再生

このあと、スクリプトで今作成したアニメーションをプレイヤーのヘルスが 0 になった時に再生させる。


スクリプトで制御する

スクリプトでプレイヤーのヘルスとゲームオーバーを連動させる。「Game.gd」スクリプトを開いたら、以下のようにコードをアップデートしよう。「# 追加」のコメントがある行が更新箇所だ。

extends Node

# 中略

onready var hud = $UI/HUD
onready var gameover = $UI/GameOver # 追加:GameOver ノードの参照

func _ready():
	gameover.visible = false # 追加:ゲーム開始時に GameOver ノードを非表示にする
	add_level()
	hud.update_health(health)
	hud.update_score(score)
	hud.update_level(current_level)

# 以下省略

これでゲームプレイ中はゲームオーバー画面は非表示になった。

続いて、いくつかのメソッドも編集する。


# 中略

func _on_Player_enemy_hit(damage):
	manage_health(damage) # 変更

# 中略

func manage_health(damage): # 追加:引数にダメージの数値をとる
	health -= damage # 移動:_on_Player_enemy_hit()メソッドから
	print("Health updated: ", health) # 移動:Moved from _on_Player_enemy_hit()メソッドから
	hud.update_health(health) # 移動: _on_Player_enemy_hit()メソッドから
	# もし ヘルスが 0 以下になったら
	if health <= 0: # 追加
		# Player の AnimationPlayer ノードで「die」アニメーションを再生する
		player.anim_player.play("die")
		# アニメーションが終了するまで待つ
		yield(player.anim_player, "animation_finished")
		# ゲームオーバー画面を表示する
		gameover.visible = true
		# デバッグ用
		print("Game over screen is shown up.")
		# シーンツリーを一時停止する(GameOver ブランチは Process)
		get_tree().paused = true
		# デバッグ用
		print("Scene tree paused: ", get_tree().paused)

今回manage_health(damage)というメソッドを新たに作成し、そちらでプレイヤーのヘルスの更新を処理するようにした。メソッドの中身の前半は、以前_on_Player_enemy_hit()メソッドの中にあったコードだ。if health <= 0:ブロックが今回新しく追加した、ヘルスが 0 になった場合の処理だ。

プロジェクトを実行して、挙動を確認してみよう。
ヘルス0になった時の挙動確認

敵キャラクターに当たってヘルスが 0 になったら「die」アニメーションのあとゲームオーバー画面に遷移した。問題なさそうだ。ちなみに、裏では「GameOver」ノード以外は一時停止している。



プレイヤーキャラクターが画面下に落下した時にゲームオーバーになる仕組みを実装する

画面下に落下した時もゲームオーバー画面を表示させたい。少ない工数で実装するには、ヘルスが 0 になった時の処理を流用するのが良いだろう。つまり、画面下に落下したらヘルスが 0 になるようにすれば良い。

画面下部への落下の判定は、「Level.gd」スクリプトで行う。理由は、プレイヤーキャラクターの位置がカメラの位置の画面下方向のリミットを超えた場合(つまり、画面下方向に落下したキャラクターが画面から消えた場合)にゲームオーバーにすれば良いので、プレイヤーキャラクターとカメラの両方を子ノードとして持つ「Level_」ノードのスクリプトで制御するのが最適だからだ。

では「Level.gd」スクリプトを開いて、編集しよう。

extends Node2D 

# オリジナルのシグナル:プレイヤーキャラクターが画面外へ落下した時に発信する
signal player_dropped # 追加

# 中略

func _process(_delta):
	camera.global_position = player.global_position
	# プレイヤーキャラクターの位置の y 座標の値が、カメラの下方向のリミットより大きい場合
	if player.global_position.y > camera.limit_bottom: # 追加: if ブロック
		# player_dropped シグナルを発信
		emit_signal("player_dropped")
		# デバッグ用
		print("player dropped and die.")
		# プロセスを停止してプレイヤーキャラクターが落下し続けるのを止める
		set_process(false)

これで、プレイヤーキャラクターが落下してカメラの外に出てしまった時に、シグナルが発信するようになった。では「Game.gd」スクリプトへ戻ろう。


# 中略

func add_level():
	level = load("res://Levels/Level" + str(current_level) + ".tscn").instance()
	level.connect("tree_exited", self, "change_level")
	# シグナル player_dropped をこのスクリプトの _on_Level_player_dropped メソッドに接続
	level.connect("player_dropped", self, "_on_Level_player_dropped") # 追加
	add_child(level)
	player = level.get_node("Player")
	player.connect("enemy_hit", self, "_on_Player_enemy_hit") 
	player.connect("item_hit", self, "_on_Player_item_hit") 

# 中略

# シグナル player_dropped で呼ばれるメソッド
func _on_Level_player_dropped(): # 追加
	# メソッド manage_helth の引数 damage に 100 を渡して実行
	manage_health(100)

これで以下の流れが出来上がった。

  1. Gameノードへの「Level_」インスタンス追加時にシグナル「player_dropped」を「Game.gd」に接続する。
  2. プレイヤーキャラクターが画面下に落下する。
  3. シグナルが発信される。
  4. ダメージを 100 受けヘルスが 0 以下になる。
  5. ゲームオーバーになる

ではプレイヤーキャラクターを落下させてみよう。
プレイヤーキャラクターが画面下に落下した時の挙動確認

落下直後、プレイヤーのヘルスは一気に 0 になり、そのあとすぐにゲームオーバー画面が表示された。想定通りだ。



Part 9 で編集したスクリプトのコード

最後に今回の Part 9 で編集したスクリプトのコードを共有しておくので、必要に応じて確認してほしい。

GameOver.gd の全コード
extends Control # Added @ Part 9


enum {
	RESTART,
	QUIT,
}

# Added later
var square_pos = [
	Vector2(108, 128),
	Vector2(212, 128)
]

var selected_option = null
var restart_text = "Do you really want to restart the game?"
var quit_text = "Do you really want to quit the game?"

onready var restart_btn = $VBoxContainer/ButtonsHBox/RestartVBox/RestartButton
onready var quit_btn = $VBoxContainer/ButtonsHBox/QuitVBox/QuitButton
onready var color_rect = $ColorRect
onready var confirmation = $ConfirmationDialog
onready var line2d = $Line2D # Added later
onready var anim_player = $Line2D/AnimationPlayer # Added later


func _ready():
	color_rect.visible = false
	anim_player.play("blink_square") # Added later


func _on_RestartButton_mouse_entered():
	line2d.position = square_pos[0]


func _on_QuitButton_mouse_entered():
	line2d.position = square_pos[1]


func _on_RestartButton_button_up():
	work_at_button_up(RESTART)


func _on_QuitButton_button_up():
	work_at_button_up(QUIT)


func work_at_button_up(option):
	selected_option = option
	color_rect.visible = true
	if selected_option == RESTART:
		confirmation.dialog_text = restart_text
	elif selected_option == QUIT:
		confirmation.dialog_text = quit_text
	confirmation.popup_centered()


func _on_ConfirmationDialog_confirmed():
	if selected_option == RESTART:
		get_tree().paused = false
		print("Scene tree paused: ", get_tree().paused)
		get_tree().change_scene("res://Game/Game.tscn")
		print("The game is restarted.")
	elif selected_option == QUIT:
		get_tree().quit()
		print("The game is quited.")


func _on_ConfirmationDialog_popup_hide():
	selected_option = null
	confirmation.dialog_text = ""
	color_rect.visible = false


func _input(event): # Added later
	if visible:
		if event.is_action_released("ui_left"):
			line2d.position = square_pos[0]
		elif event.is_action_released("ui_right"):
			line2d.position = square_pos[1]
		elif event.is_action_released("ui_accept") and not color_rect.visible:
			if line2d.position == square_pos[0]:
				_on_RestartButton_button_up()
			elif line2d.position == square_pos[1]:
				_on_QuitButton_button_up()

Game.gd の全コード
extends Node # Added @ Part 7

var health: float = 100.0 # Added @ Part 8
var score: int = 0 # Added @ Part 8
var level: Node2D
var player: KinematicBody2D

export var current_level = 1
export var final_level = 2

onready var hud = $UI/HUD # Added @ Part 8
onready var gameover = $UI/GameOver # Added @ Part 9

func _ready():
	gameover.visible = false # Added @ Part 9
	add_level()
	hud.update_health(health) # Added @ Part 8
	hud.update_score(score) # Added @ Part 8
	hud.update_level(current_level) # Added @ Part 8


func add_level():
	level = load("res://Levels/Level" + str(current_level) + ".tscn").instance()
	level.connect("tree_exited", self, "change_level")
	level.connect("player_dropped", self, "_on_Level_player_dropped") # Added @ Part 9
	add_child(level)
	player = level.get_node("Player") # Added @ Part 8
	player.connect("enemy_hit", self, "_on_Player_enemy_hit") # Added @ Part 8
	player.connect("item_hit", self, "_on_Player_item_hit") # Added @ Part 8


func change_level():
	if get_tree(): # Added @ Part 9
		print("change_level() called.")
		if current_level < final_level:
			print("change to next level.")
			level.queue_free()
			current_level += 1
			hud.update_level(current_level) # Added @ Part 8
			add_level()
		else:
			print("Game Clear! Congrats!")
			get_tree().quit()


func _on_Player_enemy_hit(damage): # Added @ Part 8
	manage_health(damage) # Modified @ Part 9


func _on_Player_item_hit(point): # Added @ Part 8
	score += point
	hud.update_score(score)


func _on_Level_player_dropped(): # Added @ Part 9
	manage_health(100)


func manage_health(damage): # Added @ Part 9
	health -= damage # Moved from _on_Player_enemy_hit() @ Part 9
	print("Health updated: ", health) # Moved from _on_Player_enemy_hit() @ Part 9
	hud.update_health(health) # Moved from _on_Player_enemy_hit() @ Part 9
	# Added @ Part 9
	if health <= 0:
		player.anim_player.play("die")
		yield(player.anim_player, "animation_finished")
		gameover.visible = true
		print("Game over screen is shown up.")
		get_tree().paused = true
		print("Scene tree paused: ", get_tree().paused)

Level.gd の全コード
extends Node2D # Created @ Part 3

signal player_dropped # Added @ Part 9

onready var player = $Player
onready var map = $TileMap
onready var camera = $Camera2D


func _ready():
	adjust_camera()


func _process(_delta):
	camera.global_position = player.global_position
	if player.global_position.y > camera.limit_bottom: # Added @ Par 9
		emit_signal("player_dropped")
		print("player dropped and die.")
		set_process(false)


func adjust_camera():
	var map_limits = map.get_used_rect()
	print("map_limits", map_limits)
	var map_cell_size = map.cell_size
	print("map_cell_size", map_cell_size)
	camera.limit_left = map_limits.position.x * map_cell_size.x
	camera.limit_right = map_limits.end.x * map_cell_size.x
	#camera.limit_top = map_limits.position.y * map_cell_size.y # 指定しない
	camera.limit_bottom = map_limits.end.y * map_cell_size.y
	camera.limit_smoothed = true


おわりに

以上で Part 9 は完了だ。今回はゲームオーバーの仕組みを実装した。ゲームオーバー画面も UI の一つなので、「Control」系クラスのノードばかりで構成した。各クラスにはそれぞれ異なる機能が割り当てられているのがご理解いただけただろう。今回使用した以外にも「Control」系クラスはまだまだたくさんある。それぞれの役割を少しずつ覚えていけば、今後さまざまな UI を作れるようになるだろう。

また UI 上のプレイヤー操作に対するゲームの制御において、シグナルの役割は大きいことも実感いただけたのではないだろうか。何かをトリガーにして実行したい処理がある場合、シグナルを積極的に利用していこう。デフォルトで用意されているシグナルだけでなく、自作のシグナルもスクリプトなら簡単に利用できることを覚えておこう。

さて、次回のチュートリアルでは、スタート画面を実装する予定だ(やっぱりな、というご意見が多そうだが)。ただ単にスタートボタンを押すだけの UI では面白くないので、プレイヤーキャラクターの種類を増やして、スタート画面でキャラクター選択もできるようにする予定だ。

では次回もお楽しみに。



UPDATE:
2022-03-16 公式オンラインドキュメント>ゲームの一時停止のリンク追加
2022-03-20 「# GameOver シーンを作る」に「Line2D」ノードのボタンを囲う四角い枠とキーボード入力でのボタン操作を追加し、それ以降の各種画像を差し替え
2022-04-23 change_levelメソッド内の冒頭にif get_tree():を追加