Part 8 の今回は、ゲームをプレイ中にポーズ(一時停止)する機能とポーズ画面を作っていく。といっても作業はこれまでのおさらい的内容が多くなっているので、気楽にやってみてほしい。

それでは前回に引き続きブロック崩しを開発していこう。


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


本題の前に

さて、本題のポーズ画面に入る前に、ここまでの開発で確認されたいくつかの問題について修正しておく。修正する内容は以下のとおりだ。

  • ボールが画面下に落ちて新しいボールが追加される時にエラーが出る
  • 「Level2」開始前の「NextScreen」のテキストが右にズレる

ボールが画面下に落ちて新しいボールが追加される時にエラーが出る問題を修正する

どうやら新しい「Ball」ノードを作って、それを「Game」シーンの子ノードに追加する際に、処理が追いつかずにエラーになっているようだ。

プロジェクトを実行してボールが画面下に落ちた時に吐き出されたエラーを見てみると、スクリプトをどう修正すれば良いかまで記載してくれている。直接add_childメソッドを使わずにcall_defferedメソッドの引数にadd_childメソッドとその引数を入れて実行すれば良いことがわかる。
エラー

公式ドキュメントcall_defferedの説明には「アイドルタイムにそのオブジェクトのメソッドを呼び出す」とある。このアイドルタイムというのは「Game」ルートノードが処理できるようになったら、と読み替えても良いだろう。

では、実際に「Game.gd」スクリプト内の_on_Ball_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()
		call_deferred("add_child", ball) # こちらを追加
		#add_child(ball) # こちらはエラーになるので削除またはコメントアウト
		call_deferred("move_child", ball, 3) # こちらを追加
		#move_child(ball, 3) # こちらはエラーになるので削除またはコメントアウト
		ball.connect("tree_exited", self, "_on_Ball_tree_exited")

プロジェクトを実行し、ボールを画面下部に落としてみると、エラーが解消されたことがわかるだろう。この問題はこれで解決だ。


「NextScreen」のテキストが右にズレる問題を修正する

問題画面が下のスクリーンショットだ。テキストが右にズレている。青い背景である「Background」ノードの位置はズレていないので、その兄弟ノードである「VBox」以下の問題であることがわかる。
NextScreenのテキストが右にズレる

これは、最初のレベルをクリアした時点でスコアの桁数が大きくなり、「Score」ノードの「Text」プロパティが初期値より長いテキストに置き換えられることに起因している。テキストの文字数増加によって「Score」ノードのサイズが右に拡大され、同じだけ「VBox」ノードも右に広がり、その子ノード以下も親ノードのセンターに合わせようと移動するため、一緒に右にズレてしまったようだ。

元々「VBox」ノードをツールバーの「レイアウト」>「中央」でセットし、ノードのサイズは特に広げていなかったのが良くなかった。

よって、この問題は「VBox」ノードをツールバーの「レイアウト」>「Rect全面」にして「VBox」のサイズを画面一杯に広げることで解決できる。それでは変更後の画面を確認しておこう。
NextScreenのテキストが右にズレる問題を修正
きちんと中央に表示されるようになった。これでこの問題は解決だ。



ポーズ画面を作る

それでは今回の本題に入ろう。前回の Part 7 で作った「NextScreen」と同様に、ポーズ画面はポーズ中だけ表示して、そうでない時は非表示にしておく。まずはポーズ画面を作っていこう。

ポーズ画面のノードを作る

ポーズ画面のために必要なノードを追加してプロパティを編集していこう。追加するノードは、全てユーザーインターフェース系のノードで構成する。

ノードを追加する

まずはじめに以下の手順で必要なノードを先にどんどん追加していこう。

  1. 「Game」ノードに「Control」ノードを追加し、名前を「PauseScreen」に変更する。
  2. 「PauseScreen」ノードに「ColorRect」ノードを追加し、名前を「Background」に変更する。
  3. 「PauseScreen」ノードに「VBoxContainer」ノードを追加し、名前を「VBox」に変更する。
  4. 「VBox」ノードに「Label」ノードを2つ追加し、名前をそれぞれ「Title」、「Message」に変更する。

この時点でシーンドックは以下のスクリーンショットのようになったはずだ。
PauseScreenの構成


プロパティを変更する

では次に、それぞれのノードのプロパティなど必要な変更をしていこう。ルートノード「Game」の中で最前面に表示される「NextScreen」ノードは、それより一つ背面にある(これから作る) 「PauseScreen」が 2D ワークスペースで確認できるように、シーンドック上で非表示にしておこう(目のアイコンが閉じた状態にする)。
NextScreenを非表示にする

  1. 「PauseScreen」ノードを選択し、2D ワークスペースで、プレイ画面全体を覆うように拡大する。
    PauseScreenを拡大
  2. 「Background」ノードを選択し、ツールバーから「レイアウト」>「Rect全面」を選択して、目一杯拡大する。
  3. 「Background」ノードの「Color」プロパティを (0, 0, 0, 160) または #a0000000 で設定する。黒で不透明度を 160 にして、少しプレイ画面が透けて見えるような見た目を演出するのが目的だ。
  4. インスペクタで「VBox」ノードの「Alignment」プロパティを「Center」に変更する。
  5. インスペクタで「Title」ノードの「Text」プロパティに「paused」と入力し、「Align」プロパティを「Center」にして、「Uppercase」プロパティをオンにする。
  6. 続けて「Custom Fonts」セクションで「新規 DynamicFont」を選択し、「Font」>「Font Data」プロパティに以前にダウンロードした「res://PressStart2P-Regular.ttf」のフォントファイルを反映し、「Settings」>「Size」プロパティを32にする。
  7. インスペクタで「Message」ノードの「Text」プロパティに以下の文言を(改行を含めて)入力する。
    Press P to continue.
    Press Q to back to start menu.
  8. 続けて「Align」プロパティを「Center」にする。
  9. さらに「Custom Fonts」セクションで「新規 DynamicFont」を選択し、こちらも「Font」>「Font Data」プロパティに「res://PressStart2P-Regular.ttf」のフォントファイルを反映する。さらに「Settings」>「Size」プロパティを10にし、「Extra Spacing」>「Bottom」プロパティの値は'16’にする。
  10. 「VBox」ノードを選択し、ツールバーから「レイアウト」>「Rect全面」を選択して、子ノードのテキストを画面中央に配置する。

シーンを実行して確認する

さて、ここまでできたら一度シーンを実行してみよう。下のスクリーンショットのようにプレイ画面が少し透けた状態でポーズ画面が表示されていればOKだ。
シーンを実行して確認



ポーズ機能を作る

ポーズ中の見た目の部分ができたので、次は機能的な部分を追加していこう。


インプットマップにアクションを追加する

まずはポーズおよび再開の操作をするために、インプットマップにアクションを一つ追加しよう。

「プロジェクト」メニュー>「プロジェクト設定」>「インプットマップ」タブで、「Pause」という名前でアクションを一つ追加し、キーボードの「P」キーを割り当てる。
インプットマップにPauseを追加


スクリプトを作る

ポーズ画面はポーズのキー操作をするまでは非表示にしておきたいので、シーンドックで「PauseScreen」ノードを一旦、非表示にしておこう。目のアイコンをクリックするだけで非表示にできるから便利である。
PauseScreenを非表示にする

次に、ゲームをポーズした時、他のプロセスは一時停止させても「PauseScreen」ノードだけはプロセスを続行させなければならない。「PauseScreen」ノードを選択し、インスペクタで「Pause Mode」プロパティを「Inherit」から「Process」に変更しておこう。
PauseScreenのPause ModeをProcessに変更

下準備ができたので、ここからはいよいよスクリプトを作成していく。

まずは「PauseScreen」ノードにスクリプトをアタッチしよう。この時「res://scripts/PauseScreen.gd」として保存しておく。


「PauseScreen.gd」スクリプトを編集する

「PauseScreen.gd」スクリプトの内容は以下のとおりだ。ひとまずスクリプトを以下のコードの内容で置き換えてほしい。

extends Control

func _ready():
	hide()

func _input(event):
	if event.is_action_released("Pause"):
		visible = not visible
		get_tree().paused = not get_tree().paused
	elif event.is_action_released("Quit"):
		get_tree().paused = false
		get_tree().change_scene("res://scene/GameStartView.tscn")

では、小分けにして解説していく。

func _ready():
	hide()

_readyメソッドの中でhideメソッドを実行しているが、これは完全に念のためだ。さきほどシーンドックで「PauseScreen」を非表示にしたが、このブロック崩しの開発途中で、もしかしたら表示に切り替えたまま忘れるかもしれない。そのため、readyメソッド実行時に問答無用で「PauseScreen」ノードを非表示にするようにしておけば、シーンドックでノードの表示/非表示がどうなっていようと気にする必要がなくなるというわけだ。

func _input(event):
	if event.is_action_released("Pause"):
		visible = not visible
		get_tree().paused = not get_tree().paused
	elif event.is_action_released("Quit"):
		get_tree().paused = false
		get_tree().change_scene("res://scene/GameStartView.tscn")

最後に_inputメソッドだ。if / elif 構文で条件分岐処理を行っている。

最初の if ブロックを見てみよう。if event.is_action_released("Pause"):では、さきほどインプットマップに登録した「Pause」アクションに当たる「P」キーを押したら( released なので厳密にはキーを放したら)、という条件を定義をしている。

if 文の中のvisibleプロパティには、現在の表示/非表示の状態が Bool 型のデータとして格納されている。true ならば表示されている状態だ。つまりvisible = not visibleの1行で、「表示してたら非表示にして、非表示だったら表示せよ」という意味になる。

同様に、get_tree().paused = not get_tree().pausedも「プロジェクトが一時停止していたらプロセスを再開し、プロセスが進行中であれば一時停止せよ」という意味だ。

次に elif ブロックを見ていこう。プログラムは、先のifの条件に当てはまらなかったら、このelifブロックに進む。elif event.is_action_released("Quit"):で、インプットマップの「Quit」アクションにあたる「Q」キーを押したら(厳密にはキーを放したら)、という条件を定義している。

elif ブロックの中に進むと、最初に実行されるのがget_tree().paused = falseだ。pausedプロパティにfalseの値を適用することによって、プロジェクトの一時停止が解除され、プロセスが再開される。次にchange_sceneメソッドによるスタート画面へのシーン切り替えだ。いきなりゲームを終了するより、一度スタート画面に戻った方が自然な印象があるのでそのようにしている。


「NextScreen.gd」スクリプトを編集する

「NextScreen.gd」スクリプトもプロジェクトの一時停止、解除に関わっているため、今回作成している「PauseScreen.gd」と重複しないように調整が必要だ。

それでは更新したスクリプトを見てみよう。

extends Control

onready var pause_screen = get_node("../PauseScreen") # 追加

func _ready():
	pause_screen.pause_mode = 1 # 追加
	get_tree().paused = true

func _input(event):
	if Input.is_action_just_pressed("ui_accept"):
		yield(get_tree().create_timer(0.1), "timeout")
		hide()
		pause_screen.pause_mode = 2 # 追加
		get_tree().paused = false
		pause_mode = 1 # 追加

「# 追加」とコメントしている行が今回編集した箇所だ。

まずonreadyキーワードで「PauseScreen」ノードを値にもつ変数を定義している。あとでこのノードをこのスクリプト内で操作するためだ。

次に_ready内のpause_screen.pause_mode = 1のコードだ。これは「PauseScreen」ノードの「Pause Mode」を「Stop」に変更してこのノードのプロセスを停止している。理由は「NextScreen」の画面の裏でポーズ操作が有効なままだとややこしいからだ。ちなみに「Pause Mode」プロパティは enum 型の値でそれぞれのステータスを格納している。具体的には以下の通りで、1を指定すれば停止させられるし、2を指定すればプロセスを再開できる。

  • Inherit: 0
  • Stop: 1
  • Process: 2

最後に_inputメソッド内に追加したコードを確認しておこう。

if Input.is_action_just_pressed("ui_accept"):という if 構文によって、「NextScreen」の画面が表示されている最中に、インプットマップの「ui_accept」のキー入力を受け付けると、if ブロックの中のコードが実行される。

この時「NextScreen」は非表示になってプレイ画面に切り替わるので、pause_screen.pause_mode = 2によって、プレイヤーのポーズ操作を有効にしている。そして最後に「NextScreen」ノード自身をpause_mode = 1で停止している。


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

「Game.gd」スクリプトは、一つのレベルをクリアした時に「NextScreen」ノードのプロパティを更新した上で非表示から表示に切り替える処理を行っている。そのため、プロジェクト全体の停止/再開の操作が「NextScreen」ノードと「PauseScreen」ノードの間で重複しないように、このスクリプトにもコードをいくつか追加する必要がある。

onready var pause_screen = $PauseScreen

こちらのpause_screen変数を追加で定義した。これは単純に「PauseScreen」ノードを指している。

あとはset_next_levelメソッドの中に2行だけコードを追加した。メソッドのコードは以下のようになった。

func set_next_level():
	print("set_next_level() start")
	level_num += 1
	
	# Stop PauseScreen node
	pause_screen.pause_mode = 1 # 追加
	
	# Show NextScreen node
	next_screen.pause_mode = 2 # 追加
	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

「# 追加」とコメントしている行が今回編集した箇所だ。

前提としてset_next_levelというメソッドは、一つのレベルでブロックを全て消してクリアした時に実行される。

そして、最初に追加したpause_screen.pause_mode = 1のコードは、「NextScreen」の画面を表示する前に、「PauseScreen」を停止させている。これで「NextScreen」の画面が表示されている裏でポーズの操作は機能しなくなる。

次にnext_screen.pause_mode = 2のコードだ。これは、ゲームプレイ中、ずっと停止中だった「NextScreen」ノードのプロセスを再開させている。この直後に「NextScreen」のプロパティを更新して非表示から表示に切り替える流れになっている。


プロジェクトを実行して確認する

以上でスクリプトの編集は完了だ。最後にポーズの操作に問題ないか、プロジェクトを実行して確認しておこう。
プロジェクトを実行して最終チェック

以下のことが確認できた。

  • 「NextScreen」表示中は「P」を押しても機能しない。
  • プレイ画面時に「P」を押すとポーズ画面が表示される。
  • ポーズ画面で「P」「Q」以外のキーを押しても反応しない。パドルもボールも動かない。
  • ポーズ画面で再び「P」を押すとポーズ画面が非表示になり、プレイ画面に戻る。
  • ポーズ画面で「Q」を押すとスタート画面に戻る。


おわりに

以上で Part 8 は完了だ。今回はあまり新しい知識は必要なかったので、比較的簡単に完了できたのではないだろうか。今回行ったブロック崩しの更新内容をまとめておく。

  • ノードを追加・編集してポーズ画面を作った
  • スクリプトでポーズ機能を実装した

次回 Part 9 ではゲームのいくつかの要素にアニメーションを追加していく。