Part 7 の今回は、HUD(ヘッドアップディスプレイ)を作っていく。HUD というのは、例えば、プレイヤーのライフゲージやスコア、残り時間、レベル(ステージ)の番号などのように、ゲームプレイ画面に常に表示されているもののことだ。
それでは前回に引き続きブロック崩しを開発していこう。
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るブロック崩し
HUDの大まかなレイアウトをデザインする
ゲーム画面のデザインを作るとき、いきなり Godot でシーンを追加していくのは得策ではない。なぜなら、大まかなレイアウトが決まっていない状態では、「ああでもない、こうでもない」とノードの追加と削除を繰り返し、なかなかゴールに辿り着けなくなる恐れがあるからだ。
デザインの作業をする時は、まずをラフスケッチ作るところから始めよう。使う道具は紙と鉛筆でもいいし、PowerPoint や Keynote などスライド作成アプリでもいい。気軽に始められるツールで作業しよう。
今回は、このラフスケッチを基に「Game」シーンにノードを追加していく。ちなみにこのスケッチは iPad のメモアプリに ApplePencil で書いた。
HUD を作る
さきほどのラフスケッチに、さらにノードのクラスを加筆してみたのがこれだ。
それでは「Game」シーンに HUD を追加していこう。
ノードを追加する
まずはHUDに必要なノードを一気に追加していこう。
- 「Game」ノードに「Control」ノードを追加し、名前を「HUD」に変更
- 「HUD」ノードに「ColorRect」ノードを追加し、名前を「Backgournd」に変更
- 「HUD」ノードに「VBoxContainer」ノードを追加し、名前を「LeftBox」に変更
- 「HUD」ノードに「HBoxContainer」ノードを追加し、名前を「RightBox」に変更
- 「LeftBox」ノードに「Label」ノードを2つ追加し、それぞれの名前を「Level」、「Score」に変更
- 「RightBox」ノードに「TextureRect」ノードを追加し、名前を「Life1」に変更
ここまでできたら、次はノードの並び順を更新しよう。
ノードの並び順を変更する
一般的に HUD はゲーム画面の最前面に表示されることが多いが、ブロック崩しでは、プレイ中に HUD の背後にボールが隠れてしまわないように、HUD を最背面に配置する。
原則として、シーンドック内で下にあるノードほどゲーム画面では前面に表示される。ノード追加時の仕様上、「HUD」ノードを追加した直後は最前面(シーンドックでは一番下)に配置されているだろう。「HUD」ノードを「Game」ノードの最背面に配置するには、単純にシーンドック内で「Game」ノードのすぐ下に「HUD」ノードをドラッグ&ドロップすれば良い。
ここまでできたら、シーンドック上の「HUD」関連ノードの並びは以下のようになったはずだ。
シーンの背景を黒にする
HUDの「Level」や「Score」の文字は白で表示したい。白を強調するために、先に「Game」シーンの背景を黒にしてしまおう。
- 親の「HUD」ノードを画面いっぱいに広げるため、シーンドックで「HUD」ノードを選択して、ツールバーから「レイアウト」>「Rect全面」を選択する。
- シーンドックで「HUD」ノードの子ノード「Backgournd」を選択して、同様にツールバーから「レイアウト」>「Rect全面」を選択する。
- インスペクタで「Backgournd」ノードの「Color」プロパティを黒(000000)に設定する。
シーンを実行して確認
以下のように表示されたら想定通りだ。
HUD のレベルとスコアを作る
「LeftBox」ノードの編集
次に HUD の左側にレベルとスコアの表示を作っていく。「LeftBox」ノードを編集していこう。以下の手順で「LeftBox」のレイアウトを調整する。
- シーンドックで「LeftBox」ノードを選択する
- ツールバーで「レイアウト」>「左上」を選択する
- ツールバーでグリッドスナップを有効にする
- 移動モードを有効にし、2D ワークスペースで右と下に 1 グリッド(8 px)分移動する
- 選択モードを有効にして、2D ワークスペースで枠の右側を x 座標 320 まで広げる
インスペクタで「Margin」プロパティを見るとこのようになっているはずだ。もちろん上記のような 2D ワークスペースでの直感的操作ではなく、直接インスペクタ上でプロパティの数値を入力しても問題ない。
「Level」ノードと「Score」ノードの「Text」プロパティの編集
続いて「LeftBox」ノードに追加した2つの「Label」ノードを編集していく。
- 「Level」ノードの「Text」プロパティに「Level: 1」と入力する
- 「Score」ノードの「Text」プロパティに「Score: 0」と入力する
これで HUD の左側に「Level」と「Score」が表示された。実際の数字はスクリプトでプレイ中に随時更新されるようにしていくが、一旦初期値としてこの状態にしておく。
フォントはデフォルトのままでも違和感ないが、せっかくなので、チュートリアル Part 6 でダウンロードした「PressStart2P-Regular.ttf」を適用しよう。軽く手順をおさらいしておく。
Memo:
「PressStart2P-Regular.ttf」のフォントがファイルシステムドックに見当たらない場合は、Google Fonts のサイト から「Press Start 2P」というフォントをダウンロードして、ファイルシステムに改めて追加してください。
「Level」ノードの編集
- シーンドックで「Level」ノードを選択し、インスペクタで「Custom Fonts」セクションを開く
- 「Font」プロパティ(一番上の方)で「新規 DynamicFont」を選択する。
- 「Font」プロパティ(四番目の方)の[空]に、ファイルシステムから「PressStart2P-Regular.ttf」をドラッグ&ドロップする。
- 「Size」プロパティの値を
12
にする。
「Score」ノードの編集
「Score」ノードも同様に設定するが、フォントファイルはインスペクタ上でコピー&ペーストができるのでその方法で設定しよう。
- 「Level」ノードのインスペクタで「Custom Fonts」を開き、「Font」プロパティ(四番目の方)の「Font Data」右側のプルダウンをクリックして「コピー」を選択する。
- 「Score」ノードのインスペクタに切り替えて、「Custom Fonts」を開き、「Font」プロパティ(一番上の方)で「新規 DynamicFont」を選択する。
- 続けて「Font」プロパティ(四番目の方)の「Font Data」右側のプルダウンをクリックして「貼り付け」を選択する。
これでカスタムフォントの設定ができた。
シーンを実行して確認
シーンを実行して、レベルとスコアの表示を確認しておこう。
HUD のライフを作る
次は HUD の右側にライフを表示させる。
「RightBox」ノードの編集
以下の手順で「RightBox」ノードのレイアウトを調整する。
- シーンドックで「RightBox」ノードを選択する
- ツールバーで「レイアウト」>「右上」を選択する
- 移動モードを有効にして、左と下に 1 グリッド(8 px)分移動する
- 選択モードを有効にして、枠の左側を x 座標 320 まで広げる
- インスペクタで「Alignment」プロパティの値を「End」に変更しておく。これで子ノードが右寄せで配置される。
インスペクタでプロパティを見るとこのようになっているはずだ。さきほどの「LeftBox」の時と同様に、「Margin」プロパティに関しては、直接インスペクタ上で数値を入力してもOKだ。
「Life1」ノードの編集
次に「RightBox」の子ノード「Life1」ノードを編集していこう。「TextureRect」クラスは「Sprite」クラス同様、画像ファイルを割り当ててそれを表示することができる。
- シーンドックで「Life1」ノードを選択する。
- ファイルシステムの「res://sprites/Heart3.png」(一番小さいハート)をインスペクタの「Texture」プロパティの[空]めがけてドラッグ&ドロップする。
これで画像がセットされた。
2D ワークスペースにはこのように表示されたはずだ。ただ、残念ながら、画像が元のサイズのままだと少し大きすぎる。もう少し小さく控えめにしていく。
ではインスペクタで「Life1」ノードの必要なプロパティを以下の手順で変更していこう。
- まずは「Expand」プロパティを「オン」にする。「Strech Mode」プロパティはそのまま。
- 「Rect」>「Min Size」プロパティの x, y をそれぞれ 16 にする。これはノードのサイズの最小値だ。ここを(16, 16)としておくことで、親ノードの「RightBox」のサイズは子ノードの最初値(16, 16)より小さくすることはできない。
- 「RightBox」を選択モードにして枠の下側を 2 グリッド分(16 px)上げて、ちょうど「Life1」ノードにフィットするように上下幅を小さくする。一見回りくどい設定の仕方であり、「Life1」ノードだけを単に 16 px 上に移動させれば良いのでは、と思った方もいるかもしれない。しかし、「Containter」系のクラスの子ノードはそれ自体の位置を変更することができない。位置は完全に親の「Container」クラスに支配されているのだ。
改めて「RightBox」のインスペクタでプロパティを見るとこのようになっているはずだ。
「Life1」のプロパティ設定が終わったところで、シーンドックで「Life1」ノードを4つ複製して「Life5」まで量産しよう。このような作業はショートカットキーが便利だ(複製 > Windows: Ctrl + D / macOS: Cmd + D)。
シーンドックでこのようになっていればOKだ。
2D ワークスペースの方で見ると、このようにHBoxContainer内で横並びに配置される。
ここまでの作業で、シーンドックはこのようになっている。
シーンを実行して確認
実際にシーンを実行して、HUD の表示が最初のラフスケッチとだいたい同じになったか確認しておこう。
HUD をプレイ状況と連動させる
ここからはゲームプレイの状況に応じて HUD の表示が変化するように、スクリプトも使って連動させていく。連動させたい内容は以下の3つだ。
- 現在のレベルを表示させる
- 現在のスコアを表示させる
- 現在のライフを表示させる
では順番に作業を進めていこう。
現在のレベルを表示させる
以下の作業が必要になるが、一つずつやっていこう。
- 次のレベルのシーンを追加する
- 次のレベルに行く前の準備画面を作る
- スクリプトで HUD のレベルを変化させる
次のレベルのシーンを追加する
ブロックの配置などこだわり出すと時間がかかるので、ここではひとまず仮で次のレベルのシーンである「Level2」というシーンを作成する。一番簡単な方法は、すでに作成済みの「Level1」シーンを継承して作成する方法だ。
Memo:
継承とは、オブジェクト指向のプログラミングでは大事な概念の一つです。他のクラスのプロパティやメソッドをそのまま引き継いで新たなクラスを作成することを指します。さらに内容を上書きして更新することもでき、また親のクラスを変更すると、自動的に継承した子クラスにも変更が反映されます。
ではまず「シーン」メニュー>「新しい継承シーン」を選択する。
次に 「Level1.tscn」ファイルを選択すれば、「Level1」シーンをまるまま継承したシーンができる。すぐにルートノードの名前を「Level2」に変更しておこう。
そのまま新しく作ったシーン「Level2」を「Level2.tscn」という名前で保存すればひとまず2つ目のシーンの出来上がりだ。
次のレベルに行く前の準備画面を作る
現状、レベルごとの準備画面がない。例えば「Level1」のブロックを全て消した後「Level2」に移行する時に、このままだとすぐに次のLevel2のブロックが画面に配置され、落ち着く暇もない。一呼吸置く意味でも、準備画面を作成していこう。準備画面には HUD と同じく以下の項目を表示させるよう構成する。
- 次のレベル
- 現在のスコア
- ライフ
準備画面として「NextScreen」という名前のノードを「Game」ノードに追加し、レベルの切り替え時のみ画面に表示されるようにしていく。以下の手順で作業を進めていこう。
- では「Game」ノードに「Control」ノードを追加し、名前を「NextScreen」に変更する。
- 2D ワークスペースで「NextScreen」を選択モードにして、手動でサイズをプレイ画面全体に広げる。
- 「NextScreen」ノードに「ColorRect」ノードを追加し、名前を「Background」に変更する。
- 2D ワークスペースで「Background」を「レイアウト」>「Rect全面」で親ノードと同じだけ広げる。
- インスペクタで「Background」の「Color」プロパティをお好みの色に変更する(例えば #106ed1)
- 「NextScreen」ノードに「VBoxContainer」ノードを追加し、名前を「VBox」に変更する。インスペクタで「Alignment」プロパティの値を「Center」にする。さらに「Custom Constants」セクションの「Separation」プロパティの値を
24
にする。 - 「VBox」ノードに「Label」ノードを2つ追加し、名前をそれぞれ「Level」、「Score」とする。
- インスペクタで「Level」ノードの「Text」プロパティの値を「Level: 1」とし、「Align」プロパティを「Center」にする。
- 同様に、「Score」ノードの「Text」プロパティの値を「Score: 0」とし、「Align」プロパティを「Center」にする。
- 「VBox」ノードに「HBoxContainer」ノードを追加し、名前を「HBox」に変更する。インスペクタで「Alignment」プロパティの値を「Center」にする。さらに「Custom Constants」セクションの「Separation」プロパティの値を
12
にする。 - 「HBox」ノードに「TextureRect」ノードを追加し、名前を「HeartImage」に変更する。インスペクタで「Texture」プロパティに、ファイルシステムから「res://sprites/Heart3.png」を割り当てる。続けて「Expand」プロパティをオンにし、「Rect」>「Min Size」プロパティを (24, 24) にする。
- 「HBox」ノードに「Label」ノードを追加し、名前を「Life」に変更する。インスペクタで「Text」プロパティに「x 3」と入力する。
- 「Level」、「Score」、「Life」の3つの Label クラスのノードにカスタムフォントを設定する。「新規 DynamicFont」を設定し、「Font」プロパティにファイルシステムから「PressStart2P-Regular.ttf」を適用する。「Font」>「Size」プロパティは
16
のままで良い。 - シーンドックで「VBox」ノードを選択し、ツールバーの「レイアウト」>「中央」を選択する。
さて、「NextScreen」ノードを追加したあと、シーンドックはこのようになっているだろうか。
大事なポイントとして、「Game」ノードの子ノードのうち、「NextScreen」ノードが一番下に位置している必要がある。理由はゲームプレイ中は「NextScreen」ノードを非表示にしておき、一つのレベルをクリアして次のレベルをスタートする前に準備画面として表示し、その際、他のノードを覆い隠すためだ。
スクリプトで HUD のレベルを変化させる
では、ここからスクリプトを作成していく。「NextScreen」にスクリプトをアタッチしよう。「res://scripts/NextScreen.gd」として保存する。
「NextScreen」ノードが画面に表示されている時に、インプットマップの「ui_accept」に該当するキー(Space、Enterなど)を入力すれば、次のレベルのプレイが開始されるような仕様にしたい。
そこでまず「NextScreen.gd」のスクリプトの中身を以下のコードで置き換えよう。
extends Control
func _input(event):
if Input.is_action_just_pressed("ui_accept"):
yield(get_tree().create_timer(0.1), "timeout")
hide()
get_tree().paused = false
まず、yield
から始まる行のコードは丸ごと「GameStartView.gd」スクリプトから移動させた。これがないと、プレイヤーがスペースキーで次に進めた場合、プレイ画面に切り替わった瞬間にボールが発射されてしまうからだ。ということで、このタイミングで、「GameStartView.gd」スクリプトからはこの一行のコードをコメントアウトするか削除しておいてほしい。
hide
という関数は、「CanvasItem」クラスに組み込みのメソッドである。この関数を実行すると、画面上そのノードは非表示になる。ちなみにこれは、下のGIF画像のように、シーンドックでノードの右側の「目」アイコンをクリックして閉じるのと同じ意味だ。
get_tree().paused = false
は、プロジェクトの一時停止状態を解除するために実行する。後ほど、次のレベルの準備が終わったら「NextScreen」が最前面に表示された状態でプロジェクトを一時停止する処理を追加する。その時、インプットマップui_accept
の操作により一時停止を解除して、次のレベルをスタートできるようにした。
それでは次に「Game」ノードにスクリプトをアタッチしよう。「res://scripts/Game.gd」として保存する。
「Game」ノードは、シーンドック上、他の全てのノードの親ノードだ。そのため、このスクリプトではノード間をまたがって処理が必要な部分をコーディングしていく。少しコード量が今までより多くなるが頑張ろう。
まずは「Game.gd」スクリプトの内容を以下のコードで置き換えてほしい。そのあと一つずつスクリプトの内容を確認していく。
extends Node2D
var level_num = 1
onready var level = $Level1
onready var next_screen = $NextScreen
onready var next_screen_level = $NextScreen/VBox/Level
onready var hud_level = $HUD/LeftBox/Level
onready var paddle = $Paddle
onready var ball = $Ball
onready var paddle_position = paddle.position
onready var ball_position = ball.position
func _ready():
# For debug
leave_one_brick(43)
for brick in level.get_children():
brick.connect("tree_exited", self, "_on_Brick_tree_exited")
get_tree().paused = true
# For debug
func leave_one_brick(brick_num: int):
for child in level.get_children():
if child.get_name() == "Brick" + str(brick_num):
continue
child.queue_free()
# Method receiving signal
func _on_Brick_tree_exited():
if level.get_child_count() <= 0:
level.queue_free()
set_next_level()
func set_next_level():
print("set_next_level() start")
level_num += 1
# Show NextScreen node
next_screen_level.text = "Level: " + str(level_num)
next_screen.show()
# Set Level of HUD the next level
hud_level.text = "Level: " + str(level_num)
# Set Paddle and Ball the first position
paddle.position = paddle_position
ball.position = ball_position
ball.mode = 3
# Set next Level node
level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
add_child(level)
move_child(level, 5)
for child in level.get_children():
child.connect("tree_exited", self, "_on_Brick_tree_exited")
# Pause game until NextScreen is hidden
get_tree().paused = true
それでは上から順番に見ていこう。
最初に定義しているlevel_num
は単純に、現在のレベルの数値を保持する変数だ。初期値として1
を入れている。
ノードが全て読み込まれてから定義すべき変数にはonready
キーワードをつけた。これらの変数は、ノードが全て読み込まれたあとのステップ(_ready
関数を実行するタイミング)で定義されるので、変数の値にget_node()
やget_parent()
などの関数を含む場合に利用する。ちなみに変数の値の頭に$
記号がついているものは、get_node()
と同じ意味で、やや簡略化して記述できるので採用している。
変数hud_level
からnext_screen_level
までは、値の示す通りそれぞれのノードを保持する。paddle_position
とball_position
については、「Paddle」ノード、「Ball」ノードの位置を保持する。
_ready
関数では、コメントでも記述しているが、デバッグ用にleave_one_brick(43)
という関数を実行している。これは_ready
関数のすぐ下で定義している関数だ。
# For debug
func leave_one_brick(brick_num: int):
for child in level.get_children():
if child.get_name() == "Brick" + str(brick_num):
continue
child.queue_free()
その内容は、「Level1」の子ノードの名前の最後が引数で指定した43
の「Brick」ノードを残し、それ以外の「Brick」ノードを全て消すという処理だ。この「Brick43」というのは配置上、パドルの初期位置からボールを発射した時に最初にボールが当たるブロックなのだ。つまり、この唯一画面に残っている「Brick43」ノードにボールで当てればゲームクリアの状態が作れるようにしている。
_ready
関数内ではそのあとfor
ループを利用して、「Level1」の子ノードである全ての「Brick–」ノードでそれぞれシグナルの接続をコードで実行している。それがbrick.connect("tree_exited", self, "_on_Brick_tree_exited")
だ。第一引数にもなっているtree_exited
というシグナルはデフォルトで用意されているもので、シーンツリーから消えたら信号を発信する。self
はこの「Game.gd」スクリプトを指す。_on_Brick_tree_exited
はこのスクリプト内の関数の名前だ。この関数にシグナルを接続している。これにより、シグナルが発信されれば_on_Brick_tree_exited
関数が実行される。
get_tree().paused = true
で、プロジェクトを一時停止している。これは_ready
で全ての準備が整ったら、一旦一時停止状態にして、「NextScreen」ノードが非表示になるまで入力操作も受け付けなくするためだ。ただし、このままでは「NextScreen」ノードも含めて全ての処理が停止してしまう。そこで、インスペクタで「NextScreen」ノードの「Pause Mode」プロパティを「Inherit」から「Process」に変更しておこう。これにより「NextScreen」ノードだけはプロセスが続行されるようになる。
# Method receiving signal
func _on_Brick_tree_exited():
if level.get_child_count() <= 0:
level.queue_free()
set_next_level()
さて、シグナルを受信するメソッドがこちらの関数だ。「Level1」などの「Level」ノードの子ノード「Brick(番号)」がそれぞれボールに当たったら、消えるタイミングで、シグナルを発信し、この関数がそれを受け取る。そして、シグナルを受け取るたびに、ブロックの数がゼロになっていないかを確認している。それがif level.get_child_count() <= 0:
の一行だ。つまりこの if 構文で true になるのは、最後の1つのブロックが消えた時だ。
最後のブロックが消え、if 構文が true になったら、queue_free
関数によって、親の「Level」ノードそのものが消える。そしてそのあとset_next_level
関数によって、次のレベルの準備作業に入る、という流れだ。ではこのset_next_level
関数の中身を見ていこう。
print("set_next_level() start")
は、この関数が実行されたのを確認するために用意している。
level_num += 1
これはlevel_num = level_num + 1
と同じ意味である。今のレベルをクリアしたら、今現在のレベルナンバーを格納している変数level_num
の値に 1 を加算し、レベルナンバーを次の数字に更新しているわけだ。
# Show NextScreen node
next_screen_level.text = "Level: " + str(level_num)
next_screen.show()
next_screen_level
という変数はこのスクリプトの冒頭で定義している「NextScreen」ノードの孫の「Level」ノードのことだ。そして、.text
はその「Text」プロパティのことで画面に表示される文字列だ。その値を更新している。String 型(文字列)の値に int 型(整数)の値を格納した変数を結合したい場合、int 型はstr
関数によって文字列に変換できるので、このように+
記号で連結すればひと続きの文字列にすることができる。
show
関数によって、非表示になっていた「NextScreen」ノードを表示している。このタイミングで次のレベルに行く前の準備画面に切り替わるわけだ。
# Set Level of HUD the next level
hud_level.text = "Level: " + str(level_num)
hud_level.text
も冒頭で定義している「HUD」ノードの孫「Level」ノードの「Text」プロパティだ。その値の数字部分を次のレベルナンバーに置き換えて、HUDの表示を更新している。たくさんのコードで埋もれそうだが、これが元々このチュートリアルでやろうとしていたことだ。
# Set Paddle and Ball the first position
paddle.position = paddle_position
ball.position = ball_position
ball.mode = 3
一つのレベルをクリアしたら次のレベルに移行するまでに、パドルとボールの位置を初期位置に戻す処理をしている。.position
はそのノードの位置情報を格納するプロパティを指している。paddle_position
及びball_position
はスクリプト冒頭で「Paddle」ノード、「Ball」ノードの初期位置を代入する形で定義済みだ。
ボールは「RigidBody2D」クラスのオブジェクトなので、「Mode」プロパティを「Character」から「Kinematic」に変更しないと、次のレベルが開始してパドルを動かした瞬間、どこか意図しない方向へ流れていってしまう。「Kinematic」に割り当てられている「Mode」プロパティの番号は3
なので、ball.mode = 3
としている。このあたりの「Ball」ノードの内容を忘れてしまった場合は、チュートリアル Part 2 を参照のこと。
# Set next Level node
level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
add_child(level)
move_child(level, 5)
for child in level.get_children():
child.connect("tree_exited", self, "_on_Brick_tree_exited")
「Level1」ノードの全ての「Brick-」ノードを消したあと「Level1」ノード自体もqueue_free
関数で削除しているのえ、次の「Level2」シーンをノードとして「Game」シーンに追加する必要がある。
load
関数は、シーンファイルを読み込むための関数だ。引数にファイルパスを指定すれば読み込んでくれる。先にlevel_num
変数は更新済みなので、それを利用してファイル名を指定している。.instance
関数は読み込んだシーンファイルのインスタンスを作る。つまり設計図から実体を作っているわけだ。それを先ほどまで「Level1」ノードを格納していた変数level
に代入している。
add_child
関数により、次の「Level2」シーンをこの「Game」シーンの子ノードとして追加している。シーンドックで「Game」ノードを選択した状態でノードを新しく追加する作業と同じ意味合いだ。
move_child
関数は、指定したノードを「Game」ノードの何番目の子ノードにするかを設定できる。先にadd_child
関数が実行された時には、 下のスクリーンショットのように「Level2」ノードがデフォルトで一番最後の順番(ここでは 6 番目)になってしまう。つまり、このままだと次のレベルのブロックが「NextScreen」ノードより全面に表示されてしまう。
そこでこのmove_child
の第一引数に変数level
(「Level2」ノード)を指定し、第二引数に5
を指定して、5番目に移動しているという理屈だ。
最後に「Level2」ノードの全ての子ノード、つまり全ての「Brick-」ノードに対して、for
ループを利用してtree_exited
シグナルを_on_Brick_tree_exited
関数に接続している。_ready
関数内で最初に実行していたことと同じだ。なぜコードを使ってシグナルを接続するかというと、一つのレベルをクリアするたびに今の「Level-」ノードを削除して、次のレベルの「Level-」シーンを新たに子ノードとして追加するので、前もってノードドックから「Game」シーンのスクリプトにシグナルを接続しておくことができないからだ。
# Pause game until NextScreen is hidden
get_tree().paused = true
最後に_ready
でも最後に記述していたプロジェクト全体を一時停止するのためのコードだ。次のレベルの準備が全て整ったら、プレイヤーがインプットマップ「ui_accept」の操作を行うまでは、一時停止にする。
ではプロジェクトを実行して、実際の挙動を確認してみよう。
以下の動作に問題ないことが確認できた。
- 次のレベル開始前に「NextScreen」ノードが表示されること
- 「NextScreen」ノードの孫ノード「Level」(Label クラス)にレベルナンバーが反映していること
- インプットマップ「ui_accept」のキー操作により「NextScreen」ノードが非表示になること
- 次のレベルのプレイ画面が表示された時に、パドルとボールが初期位置に戻っていること
- プレイ画面の「HUD」ノードの孫ノード「Level」(Label クラス)にもレベルナンバーが反映していること
現在のスコアを表示する
では次に現在のスコアを「HUD」の孫ノード「Score」に反映するように更新していこう。併せて「NextScreen」の孫ノードの方の「Score」にも同様に反映させる。また、現状、得点のシステムが全くない状態なので、ブロックを消したらポイントを獲得できるようにしていく。
では「Game.gd」スクリプトを更新していこう。
const POINT = 100 # 追加
スコア関連の定数と変数を追加した。POINT
はブロックを一つ消した時に獲得できるポイントを100
として定義した。これはずっと変わらないのでconst
キーワードにより定数としている。
var score = 0 # 追加
var bonus_rate = 1.0 # 追加
変数score
には一旦初期値の0
を代入している。この変数には、ブロックを消して獲得したポイントを毎回加算してこれまでの合計値を保持させる。また、後ほどその値を「HUD」および「NextScreen」に反映させるように調整していく。
変数bonus_rate
は、ブロックを消すたびに増加させて、ボールを落としたら初期値1.0
にリセットされるようにする予定である。一つのブロックを消したときにPOINT
定数の100
にbonus_rate
を乗じた値をポイントとして獲得できるようにする。そうすることで、うまくブロックを消し続けるほど高得点が得られるようになる。ボールを落とすとbonus_rate
の値を初期値にリセットさせる予定だ。
onready var next_screen_score = $NextScreen/VBox/Score # 追加
onready var hud_score = $HUD/LeftBox/Score # 追加
次にonready
キーワード付きの変数について見ていこう。追加しているのは 2 行だけだ。どちらも特定のノードを変数として定義している。next_screen_score
は「NextScreen」ノードの孫ノード「Score」を、hud_score
は「HUD」ノードの孫ノード「Score」をそれぞれ代入している。
# Method receiving signal
func _on_Brick_tree_exited():
# Update Score
score += POINT * bonus_rate # 追加
bonus_rate += 0.1 # 追加
hud_score.text = "Level: " + str(score) # 追加
# メソッド内の以下省略
_on_Brick_tree_exited
にいくつか処理を追加する。この関数はブロックが消された時に「Brick-」ノードからtree_exited
シグナルを受信するメソッドであることを思い出してほしい。今回、ブロックを消した時の処理に以下の3つを追加した形だ。
score += POINT * bonus_rate
により、変数score
の値にボーナス率であるbonus_rate
を乗じた値がポイントとして加算される。bonus_rate += 0.1
は、その時のbonus_rate
の値に0.1
が加算する、という意味だ。なお、このアップデートされたボーナス率は次にブロックを消した時に利用される。hud_score.text = "Level: " + str(score)
は、常に HUD の表示を更新するためのコードだ。「HUD」の孫ノード「Score」(Labelクラス)の「Text」プロパティの値として、ブロックが消されるたびにその時の変数score
の値を代入している。
func set_next_level():
print("set_next_level() start")
level_num += 1
# Show NextScreen node
next_screen_level.text = "Level: " + str(level_num)
next_screen_score.text = "Score: " + str(score) # 追加
next_screen.show()
# メソッド内の以下省略
set_next_level
関数にも 1 行だけコードを追加した。next_screen_score.text = "Score: " + str(score)
だ。これは「NextScreen」ノードの孫ノード「Score」(Labelクラス)の「Text」プロパティに「Score: (現在の変数score
の値)」という文字列を代入している。これにより、次のレベルの準備画面に現在のスコアが表示されるようになるはずだ。
ではまず、ブロックを消したら HUD のスコア表示が更新されるか確認してみよう。「Level1」ノードのブロックを全て表示してテストするため、_ready
内のleave_one_brick
の行をコメントアウトしてから、プロジェクトを実行する。
func _ready():
# For debug
#leave_one_brick(43) #コメントアウト
ブロックを消すたびに HUD のスコアにボーナス込みのポイントが加算されているのが確認できた。
今度は次のレベルの前の準備画面「NextScreen」にスコアが反映するかを確認する。さっきコメントアウトした_ready
内のleave_one_brick
の行をアクティブにしてから、プロジェクトを実行して確認してみよう。
func _ready():
# For debug
leave_one_brick(43)
最後の1つを残して他のブロックを消した状態からスタートしているので、かなり高得点からの開始だが、「NextScreen」には問題なく変数score
の値が反映されたことが確認できた。
それでは一旦ここまでに更新した「Game.gd」スクリプト全体を確認しておこう。
extends Node2D
const POINT = 100
var level_num = 1
var score = 0
var bonus_rate = 1.0
onready var level = $Level1
onready var next_screen = $NextScreen
onready var next_screen_level = $NextScreen/VBox/Level
onready var next_screen_score = $NextScreen/VBox/Score
onready var hud_level = $HUD/LeftBox/Level
onready var hud_score = $HUD/LeftBox/Score
onready var paddle = $Paddle
onready var ball = $Ball
onready var paddle_position = paddle.position
onready var ball_position = ball.position
func _ready():
# For debug
leave_one_brick(43)
for brick in level.get_children():
brick.connect("tree_exited", self, "_on_Brick_tree_exited")
get_tree().paused = true
# For debug
func leave_one_brick(brick_num: int):
for child in level.get_children():
if child.get_name() == "Brick" + str(brick_num):
continue
child.queue_free()
# Method receiving signal
func _on_Brick_tree_exited():
# Update Score
score += POINT * bonus_rate
bonus_rate += 0.1
hud_score.text = "Score: " + str(score)
# Exit current Level node
print("get child count: ", level.get_child_count())
if level.get_child_count() <= 0:
level.queue_free()
print("level queue free")
set_next_level()
func set_next_level():
print("set_next_level() start")
level_num += 1
# Show NextScreen node
next_screen_level.text = "Level: " + str(level_num)
next_screen_score.text = "Score: " + str(score)
next_screen.show()
# Set Level of HUD the next level
hud_level.text = "Level: " + str(level_num)
# Set Paddle and Ball the first position
paddle.position = paddle_position
ball.position = ball_position
ball.mode = 3
# Set next Level node
level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
add_child(level)
move_child(level, 5)
for child in level.get_children():
child.connect("tree_exited", self, "_on_Brick_tree_exited")
# Pause game until NextScreen is hidden
get_tree().paused = true
現在のライフを表示する
それでは最後に HUD の最後のセクションであるライフの表示に関わる更新していく。必要な作業は以下の通りだ。
- 「Ball」ノードをシーンとして保存する
- 「Ball.gd」スクリプトを更新する
- 「Game.gd」スクリプトを更新する
「Ball」ノードをシーンとして保存する
Ballが画面下に落ちたらライフが一つ減る、という仕組みにしたい。この仕組みを成立させるためには、ボールが下に落ちたら「Ball」ノードを削除して、新しい「Ball」ノードを追加する必要がある。「Ball」ノードを追加するには、現在「Game」ルートノードの子である「Ball」ノードをシーンとして保存し、毎回そのインスタンスを新しいノードとして追加するのが効率的だ。
シーンドックで「Game」シーンの「Ball」ノードを右クリックし、「ブランチをシーンとして保存」を選択しよう。
「res://scene/Ball.tscn」として保存すれば「Ball」シーンの完成だ。
「Ball.gd」スクリプトを更新する
この流れで、先に「Ball」ノードにアタッチしているスクリプト「Ball.gd」の方を先に更新しておく。
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
#get_tree().change_scene("res://scene/GameOverView.tscn") # 不要
「Ball.gd」スクリプトの一番最後にあるfunc _on_VisibilityNotifier2D_screen_exited():
のブロックの中を修正する。ゲームオーバー画面へシーンを切り替えるコードget_tree().change_scene("res://scene/GameOverView.tscn")
を削除、またはコメントアウトするだけだ。理由は、ゲームオーバーになるタイミングが、今までは1回ボールが画面下に落ちた時だったが、これからはライフがゼロになった時に変わるからだ。その部分のコードは「Game.gd」スクリプトの方に記述していくので、こちらの更新はこれだけだ。
「Game.gd」スクリプトを更新する
では「Game.gd」スクリプトの更新内容を上から順番に確認していこう。
var life = 3
変数life
の初期値を3
として定義した。この値と「HUD」ノードの孫ノード「Life-」の表示数とが一致するようにしていく。「Life-」の数は全部で5
個あるが、のちのちライフアップのアイテムも検討しているのと、5
だと易しすぎるので3
とした。
onready var next_screen_life = $NextScreen/VBox/HBoxContainer/Life
onready var hud_rightbox = $HUD/RightBox
onready
キーワード付きの変数を2つ追加した。next_screen_life
は「NextScreen」ノードの曽孫ノード「Life」を、hud_rightbox
は「HUD」ノードの子ノード「RightBox」をそれぞれ指している。
func _ready():
# For debug
leave_one_brick(43)
update_hud_life() # 追加
ball.connect("tree_exited", self, "_on_Ball_tree_exited") # 追加
for brick in level.get_children():
brick.connect("tree_exited", self, "_on_Brick_tree_exited")
_ready
のブロック内にいくつか処理を追加した。まずupdate_hud_life
という関数を追加したが、この関数の内容はあとで説明する。
ball.connect("tree_exited", self, "_on_Ball_tree_exited")
は「Ball」ノードのtree_exited
シグナルをこのスクリプトの_on_Ball_tree_exited
という関数に接続している。tree_exited
はたびたび使用しているが、そのノードがシーンツリーから削除された時に発信されるシグナルだ。ボールが画面下に落ちて、シーンツリーから消えたタイミングでライフを更新するために絶対に必要なシグナルなのだ。
# Method receiving Ball signal
func _on_Ball_tree_exited():
life -= 1
update_hud_life()
if life <= 0:
get_tree().change_scene("res://scene/GameOverView.tscn")
else:
paddle.position = paddle_position
ball = load("res://scene/Ball.tscn").instance()
add_child(ball)
move_child(ball, 3)
ball.connect("tree_exited", self, "_on_Ball_tree_exited")
先程のシグナルtree_exited
の接続先のメソッドがこの_on_Ball_tree_exited
だ。ボールが画面下に落ちて消えたタイミングで実行される。
ボールが画面下に落ちた時、ライフを一つ減らしたいので、まず変数life
から- 1
している。その次にupdate_hud_life
という関数を実行して、ライフに関わるアップデートをしているが、その処理内容について詳しくはのちほど説明する。
if life <= 0:
のブロックでは、ライフがゼロの場合、get_tree().change_scene("res://scene/GameOverView.tscn")
でゲームオーバー画面に遷移するようにしている。このコードは元々「Ball.gd」スクリプトにあったものだが、ゲームオーバーの条件が変わったため、こちらに移動させた。
次にelse:
ブロックだ。
ボールが画面下に落ちたら、パドルも一旦初期位置に戻したいのでpaddle.position = paddle_position
としている。
ボールが画面下に落ちて「Ball」ノードが消えたら、新しい「Ball」ノードを用意する必要がある。そのために、まず「Ball.tscn」シーンファイルを読み込み、そのインスタンス作って、変数ball
に代入している。そのあとadd_child
で、その新しい「Ball」インスタンスを「Game」ルートノードの子ノードとして追加している。そしてさらにmove_child
で、「Ball」ノードの順序を最後尾から最初と同じ3
番目に移動させている。最後に改めて、新しい「Ball」ノードのtree_exited
シグナルを_on_Ball_tree_exited
メソッドに接続している。
# Set life nodes shown and hidden as life variable
func update_hud_life():
var count = 0
for child in hud_rightbox.get_children():
count += 1
if count <= life:
child.show()
else:
child.hide()
update_hud_life
という関数を新たに追加した。_ready
内で実行されていたものだ。
これは HUD のライフ表示を更新するためのメソッドだ。for
ループ内では、「HUD」ノードの子ノード「RightBox」にぶら下がっている全ての子ノード、つまり全ての「Life-」ノードに対して処理を実行している。その処理というのは、「Life-」ノードの「RightBox」ノード中の順番が、変数life
の値以下だったら表示し、そうでなければ非表示にする、というものだ。例えば、変数life
の値が2
になったら、「Life1」、「Life2」までは表示し、「Life3」、「Life4」、「Life5」は非表示にする、ということになる。
func set_next_level():
print("set_next_level() start")
level_num += 1
# Show NextScreen node
next_screen_level.text = "Level: " + str(level_num)
next_screen_score.text = "Score: " + str(score)
next_screen_life.text = "x " + str(life) # 追加
next_screen.show()
# 以下省略
最後の更新箇所がこちらのset_next_level
内のブロックだ。next_screen_life.text = "x " + str(life)
の一行を追加した。内容は「NextScreen」ノードの「Life」ノードの「Text」プロパティの値(文字列)に変数life
の値を反映させる、というものだ。
これで HUD のライフに関わる更新ができたはずだ。実際にプロジェクトを実行して確認してみよう。
以下の動作に問題がないことを確認できた。
- 「NextScreen」に現在のライフが表示される
- HUD のライフがボールを画面下に落とすたびに減る
- HUD のライフがゼロになったらゲームオーバー画面に遷移する
最終的に「Game.gd」のスクリプト全体はこのようになっている。うまく動作しなかった方はご自身で作ったスクリプトと見比べてみて欲しい。
extends Node2D
const POINT = 100
var level_num = 1
var score = 0
var bonus_rate = 1.0
var life = 3
onready var level = $Level1
onready var next_screen = $NextScreen
onready var next_screen_level = $NextScreen/VBox/Level
onready var next_screen_score = $NextScreen/VBox/Score
onready var next_screen_life = $NextScreen/VBox/HBox/Life
onready var hud_level = $HUD/LeftBox/Level
onready var hud_score = $HUD/LeftBox/Score
onready var hud_rightbox = $HUD/RightBox
onready var paddle = $Paddle
onready var ball = $Ball
onready var paddle_position = paddle.position
onready var ball_position = ball.position
func _ready():
# For debug
leave_one_brick(43)
update_hud_life()
ball.connect("tree_exited", self, "_on_Ball_tree_exited")
for brick in level.get_children():
brick.connect("tree_exited", self, "_on_Brick_tree_exited")
get_tree().paused = true
# For debug
func leave_one_brick(brick_num: int):
for child in level.get_children():
if child.get_name() == "Brick" + str(brick_num):
continue
child.queue_free()
# Method receiving Ball signal
func _on_Ball_tree_exited():
life -= 1
update_hud_life()
if life <= 0:
get_tree().change_scene("res://scene/GameOverView.tscn")
else:
paddle.position = paddle_position
ball = load("res://scene/Ball.tscn").instance()
add_child(ball)
move_child(ball, 3)
ball.connect("tree_exited", self, "_on_Ball_tree_exited")
# Set life nodes shown and hidden as life variable
func update_hud_life():
var count = 0
for child in hud_rightbox.get_children():
count += 1
if count <= life:
child.show()
else:
child.hide()
# Method receiving Brick signal
func _on_Brick_tree_exited():
# Update Score
score += POINT * bonus_rate
bonus_rate += 0.1
hud_score.text = "Score: " + str(score)
# Exit current Level node
print("get child count: ", level.get_child_count())
if level.get_child_count() <= 0:
level.queue_free()
print("level queue free")
set_next_level()
func set_next_level():
print("set_next_level() start")
level_num += 1
# Show NextScreen node
next_screen_level.text = "Level: " + str(level_num)
next_screen_score.text = "Score: " + str(score)
next_screen_life.text = "x " + str(life)
next_screen.show()
# Set Level of HUD the next level
hud_level.text = "Level: " + str(level_num)
# Set Paddle and Ball the first position
paddle.position = paddle_position
ball.position = ball_position
ball.mode = 3
# Set next Level node
level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
add_child(level)
move_child(level, 5)
for child in level.get_children():
child.connect("tree_exited", self, "_on_Brick_tree_exited")
# Pause game until NextScreen is hidden
get_tree().paused = true
おわりに
以上で Part 7 は完了だ。かなり長くなってしまったが、最後までうまく進められただろうか。今回行ったブロック崩しの更新内容をまとめておく。
- HUD に必要なノードを追加した
- 次のレベルの準備画面「NextScreen」に必要なノードを追加した
- 主に「Game.gd」スクリプトによってゲームの状況を HUD と NextScreen に連動させた
次回 Part 8 ではさらにプレイ中のポーズ画面とポーズ機能を追加していく予定だ。