Part 6 の今回は、ゲームスタート画面とゲームオーバーの画面を作り、それらとプレイ画面との間で適宜、画面が遷移するようにしていく。
それでは前回に引き続きブロック崩しを開発していこう。
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るブロック崩し
スタート画面を作る
ゲームを開始した時、ゲームタイトルが表示され、ボタンを押すなり何らかの操作によってプレイ画面に遷移するのが一般的だし、プレイヤーにとってもそのような流れが馴染み深く、わかりやすいはずだ。では実際にブロック崩しでもスタート画面を作ってプレイ画面へ遷移するようにアップデートしていこう。
スタート画面のシーンを作って必要なノードを追加する
「シーン」メニュー>「新規シーン」を選択して新しいシーンを作る。
続けて緑色の「ユーザーインターフェース」を選択すると、シーンのルートノードとして「Control」クラスのノードが生成される。
名前を「GameStartView」に変更する。
続いて「GameStartView」に「VBoxContainer」ノードを追加する。
名前を「VBox」に変更しよう。
この「VBoxContainer」クラスのノードは、単にその子ノードを縦に自動的に配置してくれる箱のようなものだ。頭文字の V は Vertical(垂直方向)の V だ。似たようなクラスで「HBoxContainer」もあり、こちらは Horizontal(水平方向)に子ノードを並べたいときに使う。
まずは、シーンドックで「VBox」ノードを選択して、ツールバーの「レイアウト」をクリックし、「Rect全面」を選択しよう。
すると「VBox」ノードが親ノード「GameStartView」の大きさまで目一杯拡大する。
インスペクタで「Alignment」プロパティを「Center」に変更しておこう。これで「VBox」ノードの子ノードは全て中央に合わせて配置される。
「VBox」ノードができたら、その子ノードとして「Label」クラスのノードを3つ追加していく。
追加できたら、それぞれ名前を「Title」、「Message」、「SIL」に変更する。
ここまでできたら、一度シーンを保存しておこう。保存先フォルダを「res://scene/」にして、「GameStartView.tscn」という名前で保存する。
それぞれのLabelノードの設定を変更する
次にインスペクタで「Title」ノードの以下のプロパティを変更する。
- Text: 画面に表示したい文言を入力する。ここでは「breakout」と入力。一旦小文字で良い。
- Align: テキストの位置を決める。今回は中央に配置したいので「Center」を選択。
- Uppercase: これは大文字という意味だ。文字全てを大文字にしたい場合に有効にする。では有効にしよう。先ほど小文字で入力した「Text」プロパティが大文字で表示されているのがわかるだろう。
同様に「Message」ノードも以下の内容で設定する。
- Text: 「Press Any Key」と入力。大文字、小文字はお好みで。
- Align: 「Center」を選択。
2D ワークスペースにはこのように表示されるはずだ。
さらに「SIL」ノードも編集していこう。ちなみに、SIL とはフォントのライセンスの一つだ。詳しくはGoogle検索で調べてほしいが、大事なこと部分お伝えすると、このライセンスを保持するフォントデータは、利用は比較的自由だが、ソフトウェアなどに利用する場合は書作権とライセンスの情報を明記する必要がある。後ほどこのライセンスのフォントを一つ利用するので、著作権とSILの情報を明記するためにこのラベルを用意した。
設定すべき内容は以下のとおりだ。
- 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.
- Align: 「Center」を選択。
2D ワークスペースの表示はこのようになる。
ここまでできたら、一度シーンを実行してみよう。デバッグパネルは以下のようなに表示されただろうか。
カスタムフォントを設定する
全ての文字がデフォルトのフォントのままでは、とても味気ないので、Google fontsから SIL のフォントを一つダウンロードし、それをゲームに適用していく。
まずは Google Fonts のサイト
から「Press Start 2P」というフォントをダウンロードしよう。
ダウンロードしたら.zipファイルを展開して、フォントファイルをファイルシステムドックへドラッグ&ドロップする。
次にシーンドックで「Title」ノードを選択したら、インスペクタで「Custom Fonts」を開き、「Font」プロパティの**[空]**のプルダウンをクリックして「新規 DynamicFont」を選択する。
続けてインスペクタ上で「Font」を開いておき「Font Data」プロパティの**[空]**を目掛けて、さっきファイルシステムに追加したフォントファイル「PressStart2P-Regular.ttf」をドラッグ&ドロップする。
これでフォントファイルが「Title」ノードのテキストに反映したはずだ。
さらにフォントサイズをタイトルらしく大きく表示したいので、そのままインスペクタにて「Custom Fonts」>「Settings」>「Size」プロパティの値を56
に変更する。
ここまでカスタムフォントの設定ができたら、シーンを実行してみて実際の画面表示を確認してみよう。ゲームがレトロなのでフォントもレトロにしたが、タイトルは一旦これで良い塩梅になっただろう。
同様に「Message」ノードの「Custom Fonts」プロパティも同様に「ressStart2P-Regular.ttf」をフォントとして読み込み、設定していこう。ただし、「Settings」>「Size」プロパティの値は16
とする。
さらに「Custom Colors」>「Font Color」プロパティで文字の色を変える。ここでは「41b0ff」の水色っぽいカラーを指定したがお好みの色に変えていただいて問題ない。
「VBox」の子ノード(Label ノード)同志、間隔が狭すぎるので広げよう。「VBox」ノードの「Custom Constants」>「Separation」プロパティにチェックを入れ、値を入力する。値が大きいほど間隔が広くなる。ここでは48
とする。
ここで再度シーンを実行して確認しよう。これでさっきよりスッキリした印象だ。
スタート画面の背景色を変える
それでは最後にスタート画面の背景の色を変えていこう。
まず、背景用のノードを「GameStartView」ノードに追加する必要がある。「ColorRect」ノードを追加しよう。ちなみに Rect は Rectanble(長方形)の略だ。
名前を「Background」に変更し、シーンドック上「GameStartView」ノードのすぐ下(子ノードの中で一番上)に移動させる。シーンドック上、上にあるほど画面上では背面に位置することになるので、「Background」ノードが一番後ろに移動した形だ。
そのまま「Background」を選択した状態で、ツールバーの「レイアウト」>「Rect前面」を選択しよう。「VBox」ノードの時と同様に、「Background」ノードが画面全体にフィットして拡大する。
最後に「Background」ノードの色をデフォルトの白から黒に変えてみよう。これは「Backgournd」の「Color」プロパティをインスペクタから 黒(#000000) に変更するだけだ。ここもご自身で設定したい色があれば、その色にしていただいて全く問題ない。ただし、各 Label ノードとのコントラストには要注意だ。
シーンを実行して確認してみると、グッと引き締まった印象になった。これでスタート画面の見た目は出来上がった。次はスタート画面からプレイ画面への画面遷移を作っていく。
スタート画面からプレイ画面に遷移させる
ゲーム開始時のシーンとして、現在「Game.tscn」ファイルが設定されている。これを先ほど作った「GameStartView.tscn」ファイルに変更する。
「プロジェクト」メニュー>「プロジェクト設定」>「一般」タブを開き、サイドバーから「Application」>「Run」を選択する。「Main Scene」項目が現在「res://scene/Game.tscn」に設定されているのがわかるだろう。
「フォルダ」アイコンをクリックし、「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」とする。
ここまででシーンドックは以下のスクリーンショットのようになったはずだ。
では「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」キーを割り当てよう。
それではスクリプトの方の作業に移ろう。
まずはシーンドックで「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」はデフォルトで用意されているインプットマップで、スペースキーやエンターキーが割り当てられている。
その次の最後の行のコードはスタート画面の時と同様の処理で、get_tree
関数でシーンツリーにアクセスし、そこから別のシーンに切り替えるための関数change_scene
を実行している。引数には"res://scene/GameStartView.tscn"
を指定しており、スタート画面に戻るように指定している。
ここまでできたら、シーンを実行して動作確認してみよう。下のGIF画像は次の順序で画面遷移を確認している。
- シーンを実行して「GameOverView」シーンが表示されたところから開始
- 「Enter」キーを押下
- 「GameStartView」シーンが表示される
- 「Space」キーを押下
- 「Game」シーンが表示される
- デバッグパネルを一度終了して、再度シーンを実行して「GameOverView」シーンを表示
- 「Q」キーを押下
- デバッグパネルが閉じて終了
プレイ画面からゲームオーバー画面に遷移させる
一般的に、ゲームオーバー画面が表示されるのは、プレイ画面で何らかの失敗をした時だ。ブロック崩しではボールが画面下に落ちたら失敗というわかりやすいルールなので、その条件でゲームオーバー画面が表示されるように更新していこう。
ボールが画面下に落ちた瞬間に検知させる
画面下にボールが落ちた瞬間にそのことを検知する必要がある。そのためにはまず「Game.tscn」シーンに切り替えて、「Ball」ノードに「VisibilityNotifier2D」クラスのノードを子ノードとして追加しよう。
このノードを追加したのは、このノードのシグナルが便利だからだ。
さっそく、シーンドックで「VisibilityNotifier2D」ノードを選択した状態から、「ノード」ドックの「シグナル」タブを開こう。使用したいのは「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(ヘッドアップディスプレイ)を追加していく。