Part 10 の今回は、ブロック崩しにパワーアップアイテムを追加していく。ブロックを崩すとアイテムが落ちてきて、パドルとアイテムが衝突するとパワーアップが適用される、という仕組みの部分を実装していこう。
なお、パドルを大きくしたり、複数のボールを発射できるなど、いくつかのパワーアップを用意していく予定だが、個々のパワーアップの実装については、次回の Part 11 で説明させていただくこととする。
それでは前回に引き続きブロック崩しを開発していこう。
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るブロック崩し
本題の前に
2D ワークスペースで作業がしやすいように、「NextScreen」ノードをシーンドック上は「非表示」にしておき、ゲームが開始して「NextScreen」の_ready
メソッドが読み込まれたら「表示」に切り替わるようにしておこう(毎回シーンドックで調整するのは面倒だ)。
では以下のように「NextScreen.gd」スクリプトの_ready
メソッドにshow
メソッドを追加しておこう。
func _ready():
show() # 追加
pause_screen.pause_mode = 1
get_tree().paused = true
シーンドックで「Game.tscn」を開き、「NextScreen」ノードを非表示にしてしまおう。これで 2D ワークスペースで作業しやすくなったはずだ。
Powerup シーンを作る
パワーアップアイテムは、ブロックを崩した時に一定の確率でドロップするようにしたい。となると、パワーアップアイテムが常に「Game」シーンに存在するわけではない。このような場合は、パワーアップアイテムの設計図となるシーンを一つ作成しておき、ブロックを崩した時に、一定の確率で「Game」シーン内にそのインスタンスを生成する、という仕組みを構成していけば良い。
シーンを新規作成する
まずはパワーアップアイテムのシーンを新規作成する。ルートノードとして「Area2D」クラスのノードを作成し、その名前を「Powerup」に変更しておこう。そして、そのままシーンを「res://scene/Powerup.tscn」として保存しておこう。
ここで、なぜ「Area2D」を選んだのか説明しておく。
パワーアップアイテムがブロックを消した時に画面上に現れ、そこから画面下方へ落ちていき、パドルに衝突したらアイテムは消え、パワーアップ処理が実行されるようにしたい。他の物理オブジェクトと衝突した時にシグナルを発することができるのが「Area2D」か「RigidBody2D」のどちらかだ。「StaticBody2D」や「KinematicBody2D」はそのようなシグナルは用意されていないのでここでは使えない。さらに「RigidBody2D」は物理処理を自動的にしてくれるが、ただ下方向に移動するだけのオブジェクトにとっては自動の物理演算は不要だ。このような理由から「Area2D」ノードを選択した。
ノードを追加する
次に必要な子ノードを追加していく。
まず「AnimatedSprite」クラスのノードを追加する。このクラスは、通常の「Sprite」クラスとは異なり、「Texture」プロパティの代わりに「Frames」というプロパティがある。これは画像ファイル単体を適用するプロパティではなく、複数の画像ファイルや画像をまとめたシート形式のファイルを適用して、アニメーションを作成するためのプロパティだ。パワーアップアイテムがドロップしている最中に簡単なアニメーションをさせるために、こちらのクラスを採用した。
次は、例によって「CollisionShape2D」ノードを追加しておこう。名前はそのままで良い。
2つの子ノードを追加した結果、シーンドックは以下のようになったはずだ。
プロパティを編集する
「Powerup.tscn」シーンは、ドロップするアイテムによって、そのインスタンスのプロパティが異なる部分がある。どのアイテムがドロップする場合でも共通して設定が必要なプロパティだけ、インスペクタドックで編集していく。アイテムによって値が異なるプロパティについては、スクリプトで変更していく予定だ。
AnimatedSprite のプロパティを編集する
まずは「Frames」プロパティで「新規 SpriteFrames」を選択する。「Resource」セクションを展開して、「Local to Scene」プロパティにチェックを入れてオンにしておこう。これをしていないと、ドロップしたアイテムのアニメーションフレームが同じリソースを参照することになり、ドロップした全てのアイテムの画像が追加された同一のアニメーションになってしまうので注意してほしい。「Path」プロパティは「res://scene/Powerup.tscn::1」になっていればOKだ。
ここで、「SpriteFrames」を編集する。シーンドックで「AnimatedSprite」を選択した状態で画面下部のスプライトフレームエディタを開こう。最初から「default」という名前のアニメーションがあるので、その名前を「drop」に変更しておこう。速度は「4 フレーム」にする。ループはそのまま有効にしておく。
今はアニメーションフレームは空っぽのままで良い。あくまでこのシーンはインスタンスのための雛形なので、パワーアップアイテムによって内容が変わる部分はそのままにしなければならない。
インスペクタに戻り、「Animation」プロパティで、今しがた作成した「drop」アニメーションを選択しよう。アニメーションはパワーアップアイテムが画面上に出現した時点で再生したいので、「Playing」プロパティをオンにしておこう。
CollisionShape2D のプロパティを編集する
今度はインスペクタドックで「CollisionShape2D」ノードのプロパティを編集していこう。
まずは「Shape」プロパティで「新規 RectangleShape」を設定する。あとは、2D ワークスペースでハンドルをドラッグしてサイズをちょうど縦横 20 px の正方形になるように調整する。上と左の目盛りを参考にサイズを合わせると良い。
ただ、今回は Sprite の画像がなくてやりにくいと感じるかもしれない。一旦この「RectangleShape」を調整するために、「AnimatedSprite」ノードの「drop」アニメーションに、アニメーションフレームとして画像を一つ暫定的に追加しても良い。例えば下のスクリーンショットでは「Heart3.png」を追加している。「RectangleShape」のサイズ調整が終わったら、忘れず追加した画像を消しておこう。
これでインスペクタドックでのプロパティの編集はひとまず完了だ。
コリジョンレイヤーを編集する
実は、先ほど作成したパワーアップアイテムのコリジョンシェイプのサイズはブロックのそれより大きい。このままだとどうなるかというと、パワーアップアイテムはゲームに出現すると、すぐに上下左右の隣り合っているブロックと衝突してしまう。このあとスクリプトで「衝突したら画面から消える」コードを実装したら、出てきた瞬間に消えることになる。
このような状況を回避するため、一般的なお作法として、オブジェクトの種類ごとに衝突用のレイヤーを分け、衝突反応を有効にする他のレイヤーを指定する。このレイヤーをコリジョンレイヤーと言う。それでは実際にやってみよう。
まずはわかりやすくゲームで使用するコリジョンレイヤーに名前をつける。「プロジェクト」メニュー>「プロジェクト設定」>「一般」タブ>サイドバーから「Layer Names」カテゴリの「2d Physics」を開き、必要な分だけレイヤーに名前をつけよう。このブロック崩しでは以下のレイヤーを使用する。
- Paddle
- Ball
- Wall
- Brick
- Item
では順番にコリジョンレイヤーの設定をしていこう。基本的にはコリジョンを持つノードで以下の手順を同じようにやっていくだけで良い。
- シーンドックでコリジョンレイヤーを設定したいノード(ここでは「Paddle」ノード)を選択する。
- インスペクタドックで「Collision」セクションを開く
- 「Layer」でそのノードが所属するレイヤーを選択する。
「…」をクリックするとレイヤー名を見ながらチェックして選択できるのでおすすめだ。 - 「Mask」でそのノードとの衝突反応を有効にしたいレイヤーを同様に選択する。例えば「Paddle」ノードの場合、衝突を有効にしたいのは「Ball」と「Wall」と「Item」だ。
以上の手順で、コリジョンを持つノードに対して順番に設定していくと以下のようになったはずだ。
- 「Paddle」ノード
- 「Ball」ノード
- 「Wall」ノード
- 「Brick」ノード(「Brick.tscn」に切り替えて)
- 「Item」ノード(「Powerup.tscn」に切り替えて)
これでパワーアップアイテムはパドルとしか衝突しなくなった。
スクリプトで Powerup シーンを制御する
「Powerup」ルートノードにスクリプトにアタッチしよう。スクリプトは「res://scripts/Powerup.gd」として保存しておこう。このスクリプトでは主に、どのパワーアップアイテムが出るかをランダムで確定させ、確定したアイテムによって異なるアニメーションフレームを設定する。
スクリプト全体の最終形
スクリプト全体を見る
extends Area2D
signal item_collided(item)
enum Powerup {
SLOW,
EXPAND,
MULTIPLE,
LASER,
LIFE,
}
var chosen_item = null
onready var sprite = $AnimatedSprite
onready var slow_1 = preload("res://sprites/Slow1.png")
onready var slow_2 = preload("res://sprites/Slow2.png")
onready var slow_3 = preload("res://sprites/Slow3.png")
onready var slow_frames = [slow_1, slow_2, slow_3]
onready var expand_1 = preload("res://sprites/Expand1.png")
onready var expand_2 = preload("res://sprites/Expand2.png")
onready var expand_3 = preload("res://sprites/Expand3.png")
onready var expand_frames = [expand_1, expand_2, expand_3]
onready var multiple_1 = preload("res://sprites/Multiple1.png")
onready var multiple_2 = preload("res://sprites/Multiple2.png")
onready var multiple_frames = [multiple_1, multiple_2]
onready var laser_1 = preload("res://sprites/Laser1.png")
onready var laser_2 = preload("res://sprites/Laser2.png")
onready var laser_3 = preload("res://sprites/Laser3.png")
onready var laser_frames = [laser_1, laser_2, laser_3]
onready var life_1 = preload("res://sprites/Heart1.png")
onready var life_2 = preload("res://sprites/Heart2.png")
onready var life_3 = preload("res://sprites/Heart3.png")
onready var life_frames = [life_1, life_2, life_3]
func _ready():
randomize()
add_sprite_frames()
func add_sprite_frames():
sprite.frames.clear("drop")
var random_num = randf()
var item_list = []
if random_num <= 0.3:
item_list += slow_frames
chosen_item = Powerup.SLOW
elif random_num <= 0.55:
item_list += expand_frames
chosen_item = Powerup.EXPAND
elif random_num <= 0.75:
item_list += multiple_frames
chosen_item = Powerup.MULTIPLE
elif random_num <= 0.9:
item_list += laser_frames
chosen_item = Powerup.LASER
else:
item_list += life_frames
chosen_item = Powerup.LIFE
print("Powerup node: ", chosen_item)
for item in item_list:
# add to the head
sprite.frames.add_frame("drop", item, 0)
func _physics_process(_delta):
position.y += 0.75
func _on_Powerup_body_entered(body):
emit_signal("item_collided", chosen_item)
queue_free()
ではここから小分けにスクリプトを解説していく。
シグナルを定義する
オリジナルのシグナルitem_collided
を用意した。
signal item_collided(item)
これは主に「Game.gd」スクリプトの方で利用するためのものだ。このシグナルの定義の段階では、特に引数を記述する必要はないが、わかりやすいので記述した。item
でアイテムの種類を「Game」ノードへ渡せるようにするつもりだ。
enum を定義する
次にPowerup
という名前のenum
を定義している。
enum Powerup {
SLOW,
EXPAND,
MULTIPLE,
LASER,
LIFE,
}
これは要素を一つずつconst
キーワードで定数として、値を0
、1
、2
… と順番に定義しているのと同義だ。例えば以下のような感じだ。
const SLOW = 0
const EXPAND = 1
enum
というのはプルダウンメニューのようなものだ。その要素として大文字でSLOW
やEXPAND
などいくつか並んでいるが、これらはパワーアップアイテムの種類だ。ここで一度、実装を予定しているパワーアップアイテムを紹介しておく。
- SLOW:ボールのスピードが初期値に戻る(ゆっくりになる)
- EXPAND:バーが横に2倍拡大する
- MULTIPLE:一定時間ボールを複数発射できるようになる
- LASER:レーザービームを発射してブロックを消すことができる
- LIFE:ライフが 1 増える(最大 5)
変数 chosen_item を定義する
そして、enum のあとは、変数chosen_item
を定義している。
var chosen_item = null
ブロックを一つ消した時に、ランダムで決定されるアイテムの種類を保存する。その種類は先に定義したenum Powerup
から選択される。初期値はひとまず何もないことを示すnull
としている。あとで、どのアイテムがドロップしたかを、それぞれのアイテムが一定の割合で選択されるようにコーディングし、このchosen_item
にそのアイテムを値として入れることになる。
_physics_process メソッドを定義する
func _physics_process(_delta):
position.y += 0.75
_physics_process
メソッドを追加した。このメソッドでは、毎フレーム、「Powerup」ノードのposition
プロパティのy
座標を 0.5 ずつ加算している。一般的にゲームのy
座標は画面の下方向にいくほど増加する。したがって、パワーアップアイテムがドロップしている時の動きがこれで実装できたはずだ。
ここまでで、一旦スクリプトの内容を網羅した。次に、ドロップしたアイテムにパドルが衝突したら、アイテムが画面上から消える動きを作る。これにはシグナルを利用する。
シーンドックで「Powerup」ノードを選択し、ノードドック>シグナルを表示するとbody_entered(body: Node)
というシグナルが見つかるはずだ。
これを「Powerup.gd」スクリプトに接続しよう。すると、スクリプトに_on_Powerup_body_entered
メソッドが追加される。
パワーアップアイテムに何らかのオブジェクトが衝突するとシグナルbody_entered
が発信され、メソッド_on_Powerup_body_entered
が実行される。これを利用して、このメソッド内にオブジェクトを開放するためのメソッドqueue_free
メソッドを追加すれば、パワーアップアイテムがパドルに当たった瞬間に画面から消えてくれるようになる。さらにその手前に、emit_signal
メソッドを追加する。これで先に用意したシグナルitem_collided
もパドルとアイテムが衝突した時に発信されるようになる。引数に変数chosen_item
を入れることも忘れずに。
func _on_Powerup_body_entered(body):
emit_signal("item_collided", chosen_item)
queue_free()
シーンを実行してドロップ時の動きを確認する
では「Powerup.gd」スクリプトの編集ができたところで、一度シーンを実行してみよう。実行前に「デバッグ」メニューで「コリジョン形状の表示」にチェックを入れておこう。2D ワークスペースで「Powerup」ノードを画面中央上部に一時的に移動してから確認するとわかりやすいだろう。
想定通りに落ちてきてくれた。ブロックを消してパワーアップアイテムがドロップするときは、このような動きになる。
onready キーワードの変数でアニメーションフレーム用画像を定義
さあここで大量のonready
キーワード付きの変数を定義している。臆さずよく見てみよう。ほとんどが、似たようなコードの繰り返しになっているのがなんとなくわかるだろう。
onready var sprite = $AnimatedSprite
onready var slow_1 = preload("res://sprites/Slow1.png")
onready var slow_2 = preload("res://sprites/Slow2.png")
onready var slow_3 = preload("res://sprites/Slow3.png")
onready var slow_frames = [slow_1, slow_2, slow_3]
onready var expand_1 = preload("res://sprites/Expand1.png")
onready var expand_2 = preload("res://sprites/Expand2.png")
onready var expand_3 = preload("res://sprites/Expand3.png")
onready var expand_frames = [expand_1, expand_2, expand_3]
onready var multiple_1 = preload("res://sprites/Multiple1.png")
onready var multiple_2 = preload("res://sprites/Multiple2.png")
onready var multiple_frames = [multiple_1, multiple_2]
onready var laser_1 = preload("res://sprites/Laser1.png")
onready var laser_2 = preload("res://sprites/Laser2.png")
onready var laser_3 = preload("res://sprites/Laser3.png")
onready var laser_frames = [laser_1, laser_2, laser_3]
onready var life_1 = preload("res://sprites/Heart1.png")
onready var life_2 = preload("res://sprites/Heart2.png")
onready var life_3 = preload("res://sprites/Heart3.png")
onready var life_frames = [life_1, life_2, life_3]
最初の1行だけ別物で、子ノード「AnimatedSprite」にアクセスしやすいように、変数に代入している。
それ以外の変数は4行ごとにほぼ似たような定義を繰り返している。その4行ひと塊について説明していこう。
preload
で読み込んでいるのは、パワーアップアイテムのアニメーションフレーム用の Sprite 画像だ。これはpreload()
と記述したあと、()
の中に、ファイルシステムドックから Sprite 画像ファイルをドラッグ&ドロップするとファイルパスに変換してくれる。変数slow_1
、slow_2
、slow_3
までは Sprite 画像をPreload
している。
そのあとの変数slow_frames
は[]
で括っているので、これは配列(Array)だ。配列の要素として、先に定義したslow_1
、slow_2
、slow_3
が入っている。この配列は、あとでアニメーションフレームをコードで設定するための下準備となっている。
同様にしてexpand
、multiple
、laser
、life
の順番で画像をpreload
して、それらを配列の要素として定義している。
_ready メソッドを定義する
では次におなじみの_ready
メソッドを確認しておこう。
func _ready():
randomize()
add_sprite_frames()
randomize
というメソッドを実行しているが、これはあとで登場する、ランダムな数値を取得するために事前に必要になるメソッドだ。randomize
を事前に実行しておかないと、ランダムな数値を取得するはずのメソッドを実行しても、毎回同じ数値を取得することになるので要注意だ。
次に実行しているのがadd_sprite_frames
というメソッドだ。このメソッドの目的は、ランダムで決定したパワーアップアイテムの種類によって、適切なアニメーションフレームを設定する役割を担っている。
add_sprite_frames メソッドを定義する
そして、スクリプトの最後に定義しているのがadd_sprite_frames
というメソッドで、これが_ready
メソッド内で実行されているメソッドだ。ではその中身を見ていこう。少しコードが長いので、さらに小分けにして説明する。
func add_sprite_frames():
var random_num = randf()
var item_list = []
まずメソッド内でrandom_num
という変数を定義しており、その中身はrandf
というメソッドになっている。このメソッドはランダムな数値(float型)を0
から1
の間で返してくれる。例えば0.07
や0.8993
のような数値だ。ただし、先にrandomize
メソッドを実行している必要がある。
次にitem_list
という変数を定義している。その値は、一旦、空の配列[]
のみになっている。これは後ほど使用する。ではadd_sprite_frames
内の次のコードを見ていこう。
if random_num <= 0.3:
item_list += slow_frames
chosen_item = Powerup.SLOW
elif random_num <= 0.55:
item_list += expand_frames
chosen_item = Powerup.EXPAND
elif random_num <= 0.75:
item_list += multiple_frames
chosen_item = Powerup.MULTIPLE
elif random_num <= 0.9:
item_list += laser_frames
chosen_item = Powerup.LASER
else:
item_list += life_frames
chosen_item = Powerup.LIFE
この部分のコードはif / elif / else
構文になっている。
if random_num <= xxx
という形式のコードは、もし「random_num
の数値がxxx
以下だったら」という意味になる。そして、elif
は「その前のif
やelif
に当てはまらなかったら」という条件も含む。最後のelse
は「その前までのif
やelif
全てに当てはまらなかったら」という意味だ。つまり、このブロックの概要としては、先にメソッド内で定義した変数random_num
のランダムな値によって、落ちるパワーアップアイテムが変わるようになっている。
そしてitem_list += xxx_frames
という形のコードについては、先に定義したitem_list
という空っぽの配列に、例えばslow_frames
やmultiple_frames
などの、アニメーションフレーム用の画像ファイルを含む配列の要素を足すという意味になる。
ちなみに、random_num
の最大値は1
、最大値は1
であることを踏まえると、このサンプルコードでは以下の確率で落ちるアイテムが決まる。もちろん、あなたの好きな確率を割り振っていただいて全く問題ない。
0
以上0.3
以下(30%の確率):「Slow」のアイテムが落ちる0.3
より大きく0.55
以下(25%の確率):「Expand」のアイテムが落ちる0.55
より大きく0.75
以下(20%の確率):「Multiple」のアイテムが落ちる0.75
より大きく0.9
以下(15%の確率):「Laser」のアイテムが落ちる0.9
より大きく1
以下(10%の確率):「Life」のアイテムが落ちる
ところで、アイテムが落ちるかどうかの確率はまた別で設定することになる。つまり、それぞれのアイテムが実際のゲーム中に現れる確率は、アイテム出現率とそれぞれのアイテムの出現割合の掛け算になるので、さらに低くなる。
そのあとのchosen_item = Powerup.xxx
の形式のコードも見ておこう。変数chosen_item
に enum Powerup
の要素のうちrandom_num
の値によって確定したパワーアップアイテムが入る形だ。例えば、random_num
の値が0.789
だった場合はLASER
がchosen_item
に代入される。これにより「Powerup」ノードのchosen_item
という変数から、どのアイテムに確定したのかがわかるようになる。これは「Game.tscn」シーンでパワーアップの個々の機能を実装するときに役に立つだろう。
add_sprite_frames メソッドを定義する
それではadd_sprite_frames
メソッドの最後の部分を見てみよう。
for item in item_list:
# add to the head
frames.add_frame("drop", item, 0)
for
ループだ。文法上、for ... in xxx
のxxx
には配列や値の範囲を記述するが、このコードでは配列item_list
が記述されている。item_list
にはすでに、random_num
の値によって選ばれたパワーアップアイテムのアニメーションフレーム用画像が含まれている。そして...
は、配列の要素一つ一つを順番に当てはめていく。そして、要素を当てはめるごとに、for
ループのブロック内のコードを実行する。
このコードの具体的な話をすると、まず、配列item_list
内のアニメーションフレームの画像一つ一つが順番にitem
に当てはめられる。そして、frames
プロパティのadd_frame
メソッドが第一引数にアニメーション「drop」を、第二引数にitem
つまりアニメーションフレーム用画像を、第三引数に0
を指定して実行されている。
つまり、このメソッドは、アニメーション「drop」のアニメーションフレームの0
の位置に画像を追加している。追加位置は0
は最初(先頭)だ。
デフォルトでは第三引数の値は-1
、つまり最後(末尾)と決まっているのに、なぜ敢えて先頭にしているのか。これは少しややこしく、細かい話だが説明しておく。
今回ファイルシステムドックを見ていただくとわかる通り、画像のファイル名の末尾の数字が、実際にアニメーションで表示したい順番とは逆なのだ。例えば「Expand」アイテムのアニメーションとしては、両端が左右に向いた矢印が小さい状態から左右に伸びるようなアニメーションにしたいが、「expand_1」の画像が一番左右に伸びていて「expand_3」は一番短くなっている。配列expand_frames
内の画像ファイルは名前の順番になっており、item_list
に画像ファイルが追加された後も同じだ。その結果、for
ループはexpand_1
から順にアニメーションフレームに追加されるため、expand_3
がアニメーションの先頭に来るようにするには、for
ループで毎回アニメーションフレームの先頭に追加する必要があるというわけだ。
シーンを実行して確認する
それでは、ランダムでドロップするパワーアップアイテムが変わるかどうか、シーンを実行して確認してみよう。今回も「Powerup」ノードを画面の上部中央に配置してから確認するとわかりやすいだろう。
時々デバッグパネルが開くのに時間がかかっているが、ランダムでアイテムが変わることが確認できた。
次はいよいよ「Game」シーンにパワーアップアイテムを登場させる。
スクリプトで Game シーンにパワーアップアイテムをドロップさせる
実際のゲーム画面にパワーアップアイテムが表示されるように更新していこう。パワーアップアイテムのノードは、ブロックを消したタイミングでシーンに追加されるものなので、「Game.gd」スクリプトで出現と解放をコントロールする必要がある。
それでは「Game.gd」スクリプトを編集していこう。
変数を2つ追加する
まずは、新たに定義したプロパティから。
extends Node2D
const POINT = 100
export var drop_rate= 1 # 追加
# 中略
onready var powerup = preload("res://scene/Powerup.tscn") # 追加
最初に追加した変数はdrop_rate
だ。これはブロックを消した時の出現率だ。デバッグのために値は一旦1
としている。これは 100 % と同義だ。本番ではこれを 0.05 ~ 0.25 くらいに収めるのが適当だろう。export
キーワードをつけて、インスペクタで簡単に編集できるようにしている。
次にonready
付きの変数powerup
を定義している。内容は PackedScene の「Powerup.tscn」、つまり先に作成しておいた「Powerup」シーンだ。ブロックを消してアイテムがドロップした時に、このシーンを継承する形で「Game」ルートノードに追加しやすいように、先にシーンを変数に代入している。
_ready メソッドを編集する
次に_ready
メソッド内に1行追加し、1行更新した。
func _ready():
# For debug
#leave_one_brick(43)
randomize() # 追加
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", [brick.global_position]) # 更新
まずは「# 追加」のコメントがある行だ。randomize
メソッドを実行している。これは以前にも登場したが、以降にrandf
やrandom_range
などのランダムな値を返すメソッドを実行する際には事前に必ず必要な処理だ。これがないと、毎回同じ値が出力されてしまう。「Game.gd」スクリプトでは、ブロックを消した時にパワーアップアイテムを一定確率で出現させる処理で必要になる。
次に一番下のfor
ループ内の処理で実行しているbrick
のconnect
メソッドだ。これはシグナルをコードで接続するためのメソッドであることは以前にも説明していたが、今回そのメソッドに第四引数を追加した。それが[brick.global_position]
だ。個々のブロックのglobal_position
プロパティの値、つまりゲーム画面上のブロックの座標位置を渡している。
ちなみに、第四引数は必ず配列として[]
で括る決まりになっている。この配列に複数のプロパティを追加することが可能で、その値を接続先のメソッドの引数として使用することができる。今回、ブロックを消したらそのブロックの座標位置からパワーアップアイテムをドロップさせる必要があるので、このglobal_position
を引数として追加した。
さてここで、コードの順番通りに_on_Ball_tree_exited
の追加を説明したいところだが、更新内容的には後回しにしたほうが良いだろう。
_on_Brick_tree_exited メソッドを編集する
続いては_on_Brick_tree_exited
メソッドの更新内容を見てみよう。
# Method receiving Brick signal
func _on_Brick_tree_exited(brick_position):
# 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()
# Drop powerup item
drop_powerup(brick_position) # 追加
追加したのは一番下の1行だ。drop_powerup
というメソッドを実行し、引数には元の_on_Brick_tree_exited
の引数であるbrick_position
をとっている。そして、この引数brick_position
の値というのは、先に_ready
メソッド内のconnect
メソッドの第四引数として追加した個々のブロックのglobal_position
とイコールだ。
drop_powerup メソッドを追加する
そしてdrop_powerup
メソッドはこの後に定義されているので続けて見ていこう。
func drop_powerup(brick_position: Vector2):
if randf() <= drop_rate:
var powerup_instance = powerup.instance()
powerup_instance.position = brick_position
call_deferred("add_child", powerup_instance)
powerup_instance.connect("item_collided", self, "_on_Powerup_item_collided")
パワーアップアイテムの処理のために今回新しく定義したのがこのdrop_powerup
メソッドだ。
メソッドの内容はif
ブロックになっている。randf
メソッドは0
から1
の間の数値(float 型)を返す組み込み関数であることは先に説明した。これによって得られた値が、冒頭で定義した変数drop_rate
以下であれば、if
ブロックの中のコードが実行される。ちなみに、今はデバッグのためdrop_rate
を1
にしているので、パワーアップアイテムが 100 % 出現するが、デバッグが済んで、drop_rate
を仮に0.1
にすれば、ブロックを消した時のアイテムの出現率は 10 % になるというわけだ。
ではif
ブロックの中を見ていこう。
var powerup_instance = powerup.instance()
で、まず事前にpreload
していた「Powerup.tscn」シーンのインスタンスを、変数powerup_instance
として定義している。
そのあと、powerup_instance.position = brick_position
で、そのインスタンスの座標位置をブロックの座標位置と一致させている。
続けて、call_deferred("add_child", powerup_instance)
により、「Powerup」シーンのインスタンスを「Game.tscn」シーンに登場させるために、「Game」ルートノードの子ノードとして追加した。この時、処理が追いつかなくなる可能性があるため、call_dererred
メソッドからadd_child
メソッドを実行している。
そして最後に、powerup_instance.connect("item_collided", self, "_on_Powerup_item_collided")
で、「Game」ノードの子になった「Powerup」ノードのitem_collided
シグナルをこのスクリプトの_on_Powerup_item_collided
メソッドに接続している。
ちなみに、「Powerup.gd」スクリプトの編集で、パワーアップアイテムがパドルに衝突したらシグナルによって_on_Powerup_body_entered
メソッドが実行され、シグナルitem_collided
を発信するようにしていたことを思い出してほしい。
ということで、_on_Powerup_item_collided
メソッドにアイテムを獲得したあとのパワーアップ処理を追加していくことになるが、ここはアイテムの数だけコーディングする必要があるので、ボリューム的な問題で、次回のチュートリアルに回したいと思う。ひとまず引数には「Powerup.gd」で定義したchosen_item
を指すitem
を引数にしてメソッドだけ定義しておこう。中にpass
だけ記述しておけばエラーは表示されない。
func _on_Powerup_item_collided(item):
pass
次にボールが画面下に落ちてしまった時に、画面上にドロップしているパワーアップアイテムを全て消去するようにする。そうしないと、例えば、次の新しいボールをパドルに乗せたまま「Laser」アイテムを取得し、レーザーで好きなだけブロックを消せるような状況が発生してしまう。このようなゲーム性を損なうパターンは避けるべきだ。
この処理をコーディングしやすくするために、先に「Powerup」ノードを追加するための新しいノードグループを作成する。シーンドックで「Powerup.tscn」を開いて、「Powerup」ノードを選択し、ノードドックの「グループ」タブで「PowerupItems」グループを追加しよう。
では「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:
for child in get_children(): # 以下3行追加
if child.is_in_group("PowerupItems"):
child.queue_free()
paddle.position = paddle_position
ball = load("res://scene/Ball.tscn").instance()
call_deferred("add_child", ball)
call_deferred("move_child", ball, 3)
ball.connect("tree_exited", self, "_on_Ball_tree_exited")
if / else
構文のelse
ブロックの最初にfor
ループのブロックを3行追加した。
まずget_children
メソッドが「Game」ノードの全ての子ノードを配列の要素に追加した形で返してくれる。よって、このfor
ループは、この配列に対してのループ処理になる。child
には順次「Game」ノードの子ノードが入る形になる。
ループ内の処理としては、まず最初にif
構文で『その子ノードが先ほど作成した「PowerupItems」グループのメンバーだったら』という条件を定義し、それに当てはまればqueue_free
メソッドでノードを解放する、という処理だ。つまり、画面下にボールが落ちた時に、パワーアップアイテムに該当するノードは全て消去される、というわけだ。
プロジェクトを実行して最後の動作確認をする
それでは最後にプロジェクトを実行して動作確認しておこう。まずはdrop_rate
を1
にしたままで確認する。
以下について問題ないことを確認できた。
- ブロックを消すと一定の割合でそれぞれのパワーアップアイテムがドロップする。
- パドルにパワーアップアイテムが衝突するとパワーアップアイテムは画面上から消える。
- ボールを画面下に落とすと、その時点でドロップしているパワーアップアイテムは画面上から消える。
では次に、drop_rate
を0.1
にして再度プロジェクトを実行してみよう。
少しハードモードな印象もあるが、割とありそうなアイテムのドロップ率だ。これくらいの確率なら、アイテムが落ちた時にプレイヤーに嬉しい気持ちを感じてもらえるのではないだろうか。もちろん、あなたのお好みの確率に調整していただければ問題ない。
おわりに
以上で Part 10 は完了だ。今回はブロックを消したらランダムでパワーアップアイテムが落ちる仕組みを作った。
次回は個々のパワーアップ処理を実装していく。