Part 6 の今回は、ゲームスタート画面とゲームオーバーの画面を作り、それらとプレイ画面との間で適宜、画面が遷移するようにしていく。

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


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


スタート画面を作る

ゲームを開始した時、ゲームタイトルが表示され、ボタンを押すなり何らかの操作によってプレイ画面に遷移するのが一般的だし、プレイヤーにとってもそのような流れが馴染み深く、わかりやすいはずだ。では実際にブロック崩しでもスタート画面を作ってプレイ画面へ遷移するようにアップデートしていこう。

スタート画面のシーンを作って必要なノードを追加する

「シーン」メニュー>「新規シーン」を選択して新しいシーンを作る。

続けて緑色の「ユーザーインターフェース」を選択すると、シーンのルートノードとして「Control」クラスのノードが生成される。
ユーザーインターフェースを選択

名前を「GameStartView」に変更する。
ノードの名前をGameStartViewに変更

続いて「GameStartView」に「VBoxContainer」ノードを追加する。
VBoxContainerを追加

名前を「VBox」に変更しよう。
名前をVBoxに変更

この「VBoxContainer」クラスのノードは、単にその子ノードを縦に自動的に配置してくれる箱のようなものだ。頭文字の VVertical(垂直方向)の V だ。似たようなクラスで「HBoxContainer」もあり、こちらは Horizontal(水平方向)に子ノードを並べたいときに使う。

まずは、シーンドックで「VBox」ノードを選択して、ツールバーの「レイアウト」をクリックし、「Rect全面」を選択しよう。
レイアウト>Rect全面を選択

すると「VBox」ノードが親ノード「GameStartView」の大きさまで目一杯拡大する。
Rect全面が適用された画面

インスペクタで「Alignment」プロパティを「Center」に変更しておこう。これで「VBox」ノードの子ノードは全て中央に合わせて配置される。
インスペクタでAlignmentプロパティ編集

「VBox」ノードができたら、その子ノードとして「Label」クラスのノードを3つ追加していく。
Labelノード追加

追加できたら、それぞれ名前を「Title」、「Message」、「SIL」に変更する。
Labelノード3つの名前変更

ここまでできたら、一度シーンを保存しておこう。保存先フォルダを「res://scene/」にして、「GameStartView.tscn」という名前で保存する。


それぞれのLabelノードの設定を変更する

次にインスペクタで「Title」ノードの以下のプロパティを変更する。
Titleノードのプロパティ編集

  1. Text: 画面に表示したい文言を入力する。ここでは「breakout」と入力。一旦小文字で良い。
  2. Align: テキストの位置を決める。今回は中央に配置したいので「Center」を選択。
  3. Uppercase: これは大文字という意味だ。文字全てを大文字にしたい場合に有効にする。では有効にしよう。先ほど小文字で入力した「Text」プロパティが大文字で表示されているのがわかるだろう。
    2DワークスペースのTitleノード表示

同様に「Message」ノードも以下の内容で設定する。
Messageノードのプロパティ編集

  1. Text: 「Press Any Key」と入力。大文字、小文字はお好みで。
  2. Align: 「Center」を選択。

2D ワークスペースにはこのように表示されるはずだ。
2DワークスペースのMessageノード表示


さらに「SIL」ノードも編集していこう。ちなみに、SIL とはフォントのライセンスの一つだ。詳しくはGoogle検索で調べてほしいが、大事なこと部分お伝えすると、このライセンスを保持するフォントデータは、利用は比較的自由だが、ソフトウェアなどに利用する場合は書作権とライセンスの情報を明記する必要がある。後ほどこのライセンスのフォントを一つ利用するので、著作権とSILの情報を明記するためにこのラベルを用意した。

設定すべき内容は以下のとおりだ。
SILノードのプロパティ編集

  1. Text: 以下の文を改行を含めて設定。

[Press Start 2P]
Designed by CodeMan38 (https://fonts.google.com/?query=CodeMan38 )
Licensed under SIL Open Font License 1.1 (http://scripts.sil.org/OFL )
©Google Inc.

  1. Align: 「Center」を選択。

2D ワークスペースの表示はこのようになる。
2DワークスペースのSILノード表示


ここまでできたら、一度シーンを実行してみよう。デバッグパネルは以下のようなに表示されただろうか。
デバッグパネルで確認


カスタムフォントを設定する

全ての文字がデフォルトのフォントのままでは、とても味気ないので、Google fontsから SIL のフォントを一つダウンロードし、それをゲームに適用していく。

まずは Google Fonts のサイト から「Press Start 2P」というフォントをダウンロードしよう。
Google Fontsからダウンロード

ダウンロードしたら.zipファイルを展開して、フォントファイルをファイルシステムドックへドラッグ&ドロップする。
システムドックにドラッグ&ドロップ

次にシーンドックで「Title」ノードを選択したら、インスペクタで「Custom Fonts」を開き、「Font」プロパティの**[空]**のプルダウンをクリックして「新規 DynamicFont」を選択する。
Custom Fonts > Font

続けてインスペクタ上で「Font」を開いておき「Font Data」プロパティの**[空]**を目掛けて、さっきファイルシステムに追加したフォントファイル「PressStart2P-Regular.ttf」をドラッグ&ドロップする。
フォントファイルをドラッグ&ドロップ

これでフォントファイルが「Title」ノードのテキストに反映したはずだ。
Custom Fontsプロパティ群

さらにフォントサイズをタイトルらしく大きく表示したいので、そのままインスペクタにて「Custom Fonts」>「Settings」>「Size」プロパティの値を56に変更する。
Custom Fonts > Settings > Size

ここまでカスタムフォントの設定ができたら、シーンを実行してみて実際の画面表示を確認してみよう。ゲームがレトロなのでフォントもレトロにしたが、タイトルは一旦これで良い塩梅になっただろう。
デバッグパネルで確認

同様に「Message」ノードの「Custom Fonts」プロパティも同様に「ressStart2P-Regular.ttf」をフォントとして読み込み、設定していこう。ただし、「Settings」>「Size」プロパティの値は16とする。

さらに「Custom Colors」>「Font Color」プロパティで文字の色を変える。ここでは「41b0ff」の水色っぽいカラーを指定したがお好みの色に変えていただいて問題ない。
Custom Colors > Font Color

「VBox」の子ノード(Label ノード)同志、間隔が狭すぎるので広げよう。「VBox」ノードの「Custom Constants」>「Separation」プロパティにチェックを入れ、値を入力する。値が大きいほど間隔が広くなる。ここでは48とする。
VBoxノードのSeparationプロパティを編集

ここで再度シーンを実行して確認しよう。これでさっきよりスッキリした印象だ。


スタート画面の背景色を変える

それでは最後にスタート画面の背景の色を変えていこう。

まず、背景用のノードを「GameStartView」ノードに追加する必要がある。「ColorRect」ノードを追加しよう。ちなみに Rect は Rectanble(長方形)の略だ。
ColorRectクラスのノード追加

名前を「Background」に変更し、シーンドック上「GameStartView」ノードのすぐ下(子ノードの中で一番上)に移動させる。シーンドック上、上にあるほど画面上では背面に位置することになるので、「Background」ノードが一番後ろに移動した形だ。
Backgroundに名前変更して順序を再背面に

そのまま「Background」を選択した状態で、ツールバーの「レイアウト」>「Rect前面」を選択しよう。「VBox」ノードの時と同様に、「Background」ノードが画面全体にフィットして拡大する。

最後に「Background」ノードの色をデフォルトの白からに変えてみよう。これは「Backgournd」の「Color」プロパティをインスペクタから 黒(#000000) に変更するだけだ。ここもご自身で設定したい色があれば、その色にしていただいて全く問題ない。ただし、各 Label ノードとのコントラストには要注意だ。
BackgroundノードのColorプロパティを黒に変更

シーンを実行して確認してみると、グッと引き締まった印象になった。これでスタート画面の見た目は出来上がった。次はスタート画面からプレイ画面への画面遷移を作っていく。
デバッグパネルで確認


スタート画面からプレイ画面に遷移させる

ゲーム開始時のシーンとして、現在「Game.tscn」ファイルが設定されている。これを先ほど作った「GameStartView.tscn」ファイルに変更する。

「プロジェクト」メニュー>「プロジェクト設定」>「一般」タブを開き、サイドバーから「Application」>「Run」を選択する。「Main Scene」項目が現在「res://scene/Game.tscn」に設定されているのがわかるだろう。
Main Sceneの編集

「フォルダ」アイコンをクリックし、「res://scene/GameStartView.tscn」ファイルを選択する。
フォルダ選択

確認のため、プロジェクトを実行(シーンを実行ではない)してみてほしい。この時、スタート画面が表示されるはずだ。では次に画面遷移を作っていく。

ついでだが、シーンドックで「GameStartView」ノードを選択し、ツールバーにある「オブジェクトの子を選択不可にする」設定を有効にしておこう。これでこのスタート画面一式の位置関係が変わることはない。
オブジェクトの子を選択不可にする

「GameStartView」ノードにスクリプトをアタッチしよう。名前は「GameStartView.gd」のまま、保存先に「scripts」フォルダを選択してスクリプトファイルを作成する。
スクリプトをアタッチ

「GameStartView.gd」ファイルをスクリプトエディタで開いたら、以下の内容でスクリプトのコードを置き換えてほしい。

extends Control


func _input(event):
	if event is InputEventKey:
		yield(get_tree().create_timer(0.1), "timeout")
		print("Input at Game Start: ", event.as_text())
		get_tree().change_scene("res://scene/Game.tscn")

ではスクリプトの内容を確認していこう。

まずこのスクリプトで唯一の関数_inputは Node クラスに組み込みのメソッドで、いわゆるコールバック関数だ。何らかの入力操作があれば、そのイベントをevent引数に格納して、関数内のコードを実行する。

InputEventKeyというのはキーボードのいずれかのキー入力を指す。つまりif event is InputEventKey:の if 構文の意味は、何らかの入力イベントを受信した時に、その入力イベントがキーボードのキー操作だった場合は、という意味だ。

if 構文の中を見ていこう。

		yield(get_tree().create_timer(0.1), "timeout")

まず、yieldというコルーチン用の組み込み関数だ。コルーチンとは、関数を途中の任意のタイミングで停止し、その後任意のタイミングで再開できる機能を持つ関数を指す。この停止、再開を担当するのがyieldだ。yieldは引数なしでも使われることが多いが、ここでは引数にオブジェクトとシグナルをとる形で利用している。

第一引数に割り当てているのは「Timer」クラスのノードだ。ただし、yieldの引数の中でノードが作成されている。get_tree関数はシーンツリーにアクセスする関数で、シーンツリーに「Timer」ノードを作成するのがcreate_timer関数だ。引数0.1はタイマーの長さで、0.1秒のタイマーということになる。第二引数timeoutは「Timer」ノードのシグナルで、タイマーの時間が経過したら発信される。まとめると、0.1 秒のタイマーがセットされた「Timer」ノードがタイムアップしたら、次の行のコードに進む、という意味になる。

なぜ、このようなコードを記述するかを説明しておこう。スタート画面で何らかのキー入力をした時、もしそのキーがスペースキーだった場合、次のゲームプレイ画面に切り替わった瞬間、即座にパドルからボールが発射してしまう。つまり、スタート画面で入力したスペースキーが次のプレイ画面の操作としても影響してしまうということだ。そのため、プレイヤーがスタート画面で何らかのキー入力をしたら、0.1秒待機させるようにして、キー入力が次のプレイ画面に影響しないようにしているというわけである。

Memo:
コルーチンとyieldについての正確な情報は公式ドキュメント 参照のこと。


print("Input at Game Start: ", event.as_text())

これは出力コンソールにどのキーが入力されたかを出力するために追加している。目的は挙動をチェックする際に本当にキー入力が正しく認識されているかを確認することだ。つまり、コードに不備がなければこの一行のコードは無くても良い。


get_tree().change_scene("res://scene/Game.tscn")

最後にget_tree関数でシーンツリーにアクセスし、change_scene関数で引数で指定したシーンに切り替える。change_scene関数の引数にはゲームプレイ画面のシーンファイルのパス「res://scene/Game.tscn」を入力している。シーンの切り替えは基本的にこのような記述になる。今後よく利用するので頭の片隅に置いておくと良いだろう。


では、実際にスタート画面からプレイ画面にきちんと切り替わるか、プロジェクトを実行して見てみよう。スタート画面でスペースキーやエンターキーその他何でも良いので適当にキーを押下して、プレイ画面に遷移したらOKだ。
スタート画面からプレイ画面への遷移

もし余力があれば、動作確認として、ぜひyield(get_tree().create_timer(0.1), "timeout")の一行をコメントアウトしてプロジェクトを実行してみてほしい。



ゲームオーバー画面を作る

基本的な手順はスタート画面を作るのと同じなので、細かい解説はなしにしてどんどん進めていこう。

ゲームオーバー画面のシーンを作る

「シーン」メニュー>「新規シーン」から、「ユーザーインターフェース」を選択して「Control」ノードをルートノードにする。名前は「GameOverView」とする。

ここで先にシーンを保存しておこう。名前はそのまま「GameOverView.tscn」として、「scene」フォルダに保存する。

「GameOverView」ノードに「ColorRect」クラスのノードを追加する。名前を「Background」に変更しておこう。インスペクタ一番上の「Color」プロパティを黒(000000)に変更する。

「GameOverView」ノードに「VBoxContainer」クラスのノードを追加し、名前を「VBox」とする。あとは、インスペクタで「Alignment」を「Center」に変更、「Custom Constants」の値を60に変更しよう。

「Background」、「VBox」それぞれについて、ツールバーの「レイアウト」>「Rect全面」を選択して画面一杯に広げておこう。

次に「VBox」ノードの子ノードとして2つの「Label」クラスのノードを追加する。それぞれ名前を「GameOverLabel」、「Message」とする。

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

シーンドックでGameOverViewシーンのノード確認


では「GameOverLabel」ノードのプロパティを以下のように設定しよう。

  • 「Text」プロパティに「gameover」と入力
  • 「Align」を「Center」にする
  • 「Uppercase」プロパティをオンにする
  • 「Custom Fonts」>「Font」に「新規 DynamicFont」を適用
  • 「Custom Fonts」>「Font」>「Font Data」にフォントファイルの「PressStart2P」を割り当てる
  • 「Custom Fonts」>「Settings」>「Size」の値を48に変更
  • 「Custom Colors」>「Font Color」に赤色(ff0000)を割り当てて、ゲームオーバーを演出する

続いて「Message」ノードのプロパティを以下のように設定しよう。

  • 「Text」プロパティに下記文言を入力
    To Quit Press Q
    To Continue Press Enter
  • 「Align」を「Center」にする
  • 「Custom Fonts」>「Font」に「新規 DynamicFont」を適用
  • 「Custom Fonts」>「Font」>「Font Data」にフォントファイルの「PressStart2P」を割り当てる
  • 「Custom Fonts」>「Settings」>「Size」の値を12に変更
  • 「Custom Colors」>「Font Color」に薄いピンク(ffbebe)を割り当てる

シーンドックで「GameOverView」ノードを選択した状態で、ツールバーから「オブジェクトの子を選択不可にする」を有効にして位置関係を固定しておこう。

ここまでできたら、一度シーンを実行してみよう。以下のスクリーンショットのようになっていればOKだ。
デバッグパネルでゲームオーバー画面の確認


ゲームオーバー画面からスタート画面に遷移またはゲームを終了させる

画面遷移の処理も、基本的にはスタート画面でやったことと同じなので、サクサク進めていくが、一つ下準備をしておこう。

下準備というのはインプットマップの追加だ。ゲームオーバー画面で「Q」キーを押下したらゲームを完全に終了できるようにしていく。ここではひとまず、「プロジェクト」メニュー>「プロジェクト設定」>「インプットマップ」タブにて、「Quit」という名前のインプットマップを追加して「Q」キーを割り当てよう。
Quitというインプットマップを追加

それではスクリプトの方の作業に移ろう。

まずはシーンドックで「GameOverView」ノードを選択して、新規スクリプトをアタッチしよう。スクリプトは保存先を「res://scripts」、名前を「GameOverView.gd」として保存する。

スクリプトの内容を以下のコードで置き換えてほしい。

extends Control


func _input(event):
	if event is InputEventKey:
		print("Input at Game Over: ", event.as_text())
		if event.is_action_released("Quit"):
			get_tree().quit()
		elif event.is_action_released("ui_accept"):
			get_tree().change_scene("res://scene/GameStartView.tscn")

では、スクリプトの内容を確認していく。

スタート画面と大まかには同じようなコードだ。組み込みのコールバック関数_inputによって、プレイヤーが何らかの入力をすれば関数内のコードが実行される。

今回のコードは if 構文が2段階になっている(これをネストという)。まず最初の if 構文で、入力がキーボード入力だった場合、ひとまず入力したキーを出力コンソールに出力して確認するためprint関数を使用している。

その後、さらに内側の if 構文に進む。

		if event.is_action_pressed("Quit"):

これはシンプルに、入力されたイベントがインプットマップの「Quit」の操作だったら、つまりプレイヤーが「Q」のキーを押したら、という意味合いだ。その場合に実行されるのがget_tree().quit()だ。get_tree関数でシーンツリーにアクセスし、そこからquit関数によって、ゲームアプリケーションが終了させられる。なお、iOSデバイス向けにゲームを作る場合は、この関数は機能しないようなので、今後のご自身の開発ではご注意いただきたい。

次にelifのブロックだ。

		elif event.is_action_released("ui_accept"):

elifから始まる行は、最初のifから始まる行の条件に当てはまらなかった場合に実行される。is_action_released関数は、引数のインプットマップの操作があったかどうかを Bool 値で返してくれる。ただし、さきほどのifの行では関数名に pressedが含まれていたが、今回は releasedが名前に含まれている。これは、操作の最後にキーから指が離れたかどうかを確認している。

具体的に、ここでのコードは『入力操作がインプットマップの「ui_accept」の操作で、最後に指がキーから離れたら』という意味合いになる。そして「ui_accept」はデフォルトで用意されているインプットマップで、スペースキーやエンターキーが割り当てられている。
インプットマップのui_accept

その次の最後の行のコードはスタート画面の時と同様の処理で、get_tree関数でシーンツリーにアクセスし、そこから別のシーンに切り替えるための関数change_sceneを実行している。引数には"res://scene/GameStartView.tscn"を指定しており、スタート画面に戻るように指定している。

ここまでできたら、シーンを実行して動作確認してみよう。下のGIF画像は次の順序で画面遷移を確認している。

  1. シーンを実行して「GameOverView」シーンが表示されたところから開始
  2. 「Enter」キーを押下
  3. 「GameStartView」シーンが表示される
  4. 「Space」キーを押下
  5. 「Game」シーンが表示される
  6. デバッグパネルを一度終了して、再度シーンを実行して「GameOverView」シーンを表示
  7. 「Q」キーを押下
  8. デバッグパネルが閉じて終了
    デバッグパネルで画面遷移の確認


プレイ画面からゲームオーバー画面に遷移させる

一般的に、ゲームオーバー画面が表示されるのは、プレイ画面で何らかの失敗をした時だ。ブロック崩しではボールが画面下に落ちたら失敗というわかりやすいルールなので、その条件でゲームオーバー画面が表示されるように更新していこう。


ボールが画面下に落ちた瞬間に検知させる

画面下にボールが落ちた瞬間にそのことを検知する必要がある。そのためにはまず「Game.tscn」シーンに切り替えて、「Ball」ノードに「VisibilityNotifier2D」クラスのノードを子ノードとして追加しよう。
VisibilityNotifier2Dノードを追加

このノードを追加したのは、このノードのシグナルが便利だからだ。
さっそく、シーンドックで「VisibilityNotifier2D」ノードを選択した状態から、「ノード」ドックの「シグナル」タブを開こう。使用したいのは「screen_exited()」というシグナルだ。これはノードがスクリーンの外に出た瞬間に発信される。

「screen_exited()」をダブルクリック、または右下の「接続」をクリックする。
screen_exitedシグナルを接続

「Ball」ノードのスクリプトを選択して「接続」をクリックする。なお、受信側メソッドの名前はそのままで良い。
接続先を選択

スクリプトエディタに切り替わり、メソッドが追加され、左側にシグナルとの接続を示す緑色のアイコンが表示されているのが確認できるだろう。ではこのメソッドの中身を編集していこう。
スクリプトエディタで接続されたメソッドを確認

_on_VisibilityNotifier2D_screen_exitedメソッドを編集した結果、「Ball.gd」スクリプト全体は以下のようなコードになる。追加したメソッドは一番下にある。

extends RigidBody2D


const MAX_SPEED = 300.0
export (float) var ball_speed = 150.0
export (float) var speed_up = 4.0
var direction: Vector2
var velocity: Vector2
onready var paddle = get_node("../Paddle")


func _ready():
	direction = Vector2(1, -1).normalized()
	velocity = direction * ball_speed


func _process(delta):
	if mode == 3:
		position.x = paddle.position.x
		if Input.is_action_just_pressed("launch_ball"):
			mode = 2
			apply_impulse(Vector2.ZERO, velocity)


func _on_Ball_body_entered(body):
	ball_speed += speed_up
	print("ball_speed: "+str(ball_speed))
	direction = linear_velocity.normalized()
	velocity = direction * min(ball_speed, MAX_SPEED)
	
	if body.is_in_group("Bricks"):
		body.queue_free()
	
	if body.get_name() == "Paddle":
		direction = (position - body.position).normalized()
		velocity = direction * min(ball_speed, MAX_SPEED)
	
	linear_velocity = velocity


func _on_VisibilityNotifier2D_screen_exited():
	queue_free()
	get_tree().change_scene("res://scene/GameOverView.tscn")

_on_VisibilityNotifier2D_screen_exitedメソッドが実行されると、まずqueue_freeという関数が実行される。これはスクリプトがアタッチされているノード自体を開放(シーンツリーから消す)する。つまり、ボールが画面下に落ちたら、「Ball」ノードそのものが消えるということだ。これを実行しないと、画面上には表示されてなくとも、「Ball」ノードは生きたまま落下し続けている状態になる。ただし、開放するタイミングは次のフレームだ。だから「Ball」ノードが消える前に次の行のコードもきっちり処理される。

そして次の最後の行だが、すっかりお馴染みのchange_scene関数だ。引数に"res://scene/GameOverView.tscn"をとって、ゲームオーバー画面に遷移するようにしている。

これで、ボールが画面下に落ちたらゲームオーバー画面に切り替わるようになったはずだ。プロジェクトを実行して最初から画面遷移を確認してみよう。
デバッグパネルで画面遷移を確認



おわりに

以上で Part 6 は完了だ。今回はスタート画面、ゲームオーバー画面を作って、プレイ画面を含めた画面遷移を完成させた。よりゲームらしさが演出できたのではないだろうか。

次回 Part 7 では HUD(ヘッドアップディスプレイ)を追加していく。