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 ワークスペースで作業しやすくなったはずだ。
シーンドックでNextScreenを非表示にする



Powerup シーンを作る

パワーアップアイテムは、ブロックを崩した時に一定の確率でドロップするようにしたい。となると、パワーアップアイテムが常に「Game」シーンに存在するわけではない。このような場合は、パワーアップアイテムの設計図となるシーンを一つ作成しておき、ブロックを崩した時に、一定の確率で「Game」シーン内にそのインスタンスを生成する、という仕組みを構成していけば良い。


シーンを新規作成する

まずはパワーアップアイテムのシーンを新規作成する。ルートノードとして「Area2D」クラスのノードを作成し、その名前を「Powerup」に変更しておこう。そして、そのままシーンを「res://scene/Powerup.tscn」として保存しておこう。

ここで、なぜ「Area2D」を選んだのか説明しておく。

パワーアップアイテムがブロックを消した時に画面上に現れ、そこから画面下方へ落ちていき、パドルに衝突したらアイテムは消え、パワーアップ処理が実行されるようにしたい。他の物理オブジェクトと衝突した時にシグナルを発することができるのが「Area2D」か「RigidBody2D」のどちらかだ。「StaticBody2D」や「KinematicBody2D」はそのようなシグナルは用意されていないのでここでは使えない。さらに「RigidBody2D」は物理処理を自動的にしてくれるが、ただ下方向に移動するだけのオブジェクトにとっては自動の物理演算は不要だ。このような理由から「Area2D」ノードを選択した。


ノードを追加する

次に必要な子ノードを追加していく。

まず「AnimatedSprite」クラスのノードを追加する。このクラスは、通常の「Sprite」クラスとは異なり、「Texture」プロパティの代わりに「Frames」というプロパティがある。これは画像ファイル単体を適用するプロパティではなく、複数の画像ファイルや画像をまとめたシート形式のファイルを適用して、アニメーションを作成するためのプロパティだ。パワーアップアイテムがドロップしている最中に簡単なアニメーションをさせるために、こちらのクラスを採用した。

次は、例によって「CollisionShape2D」ノードを追加しておこう。名前はそのままで良い。

2つの子ノードを追加した結果、シーンドックは以下のようになったはずだ。
Powerupシーンのノード


プロパティを編集する

「Powerup.tscn」シーンは、ドロップするアイテムによって、そのインスタンスのプロパティが異なる部分がある。どのアイテムがドロップする場合でも共通して設定が必要なプロパティだけ、インスペクタドックで編集していく。アイテムによって値が異なるプロパティについては、スクリプトで変更していく予定だ。

AnimatedSprite のプロパティを編集する

まずは「Frames」プロパティで「新規 SpriteFrames」を選択する。「Resource」セクションを展開して、「Local to Scene」プロパティにチェックを入れてオンにしておこう。これをしていないと、ドロップしたアイテムのアニメーションフレームが同じリソースを参照することになり、ドロップした全てのアイテムの画像が追加された同一のアニメーションになってしまうので注意してほしい。「Path」プロパティは「res://scene/Powerup.tscn::1」になっていればOKだ。
Powerupシーンのインスペクタ1

ここで、「SpriteFrames」を編集する。シーンドックで「AnimatedSprite」を選択した状態で画面下部のスプライトフレームエディタを開こう。最初から「default」という名前のアニメーションがあるので、その名前を「drop」に変更しておこう。速度は「4 フレーム」にする。ループはそのまま有効にしておく。

今はアニメーションフレームは空っぽのままで良い。あくまでこのシーンはインスタンスのための雛形なので、パワーアップアイテムによって内容が変わる部分はそのままにしなければならない。
アニメーションフレーム

インスペクタに戻り、「Animation」プロパティで、今しがた作成した「drop」アニメーションを選択しよう。アニメーションはパワーアップアイテムが画面上に出現した時点で再生したいので、「Playing」プロパティをオンにしておこう。
インスペクタ

CollisionShape2D のプロパティを編集する

今度はインスペクタドックで「CollisionShape2D」ノードのプロパティを編集していこう。

まずは「Shape」プロパティで「新規 RectangleShape」を設定する。あとは、2D ワークスペースでハンドルをドラッグしてサイズをちょうど縦横 20 px の正方形になるように調整する。上と左の目盛りを参考にサイズを合わせると良い。

ただ、今回は Sprite の画像がなくてやりにくいと感じるかもしれない。一旦この「RectangleShape」を調整するために、「AnimatedSprite」ノードの「drop」アニメーションに、アニメーションフレームとして画像を一つ暫定的に追加しても良い。例えば下のスクリーンショットでは「Heart3.png」を追加している。「RectangleShape」のサイズ調整が終わったら、忘れず追加した画像を消しておこう。
アニメーションフレーム
2D ワークスペース

これでインスペクタドックでのプロパティの編集はひとまず完了だ。


コリジョンレイヤーを編集する

実は、先ほど作成したパワーアップアイテムのコリジョンシェイプのサイズはブロックのそれより大きい。このままだとどうなるかというと、パワーアップアイテムはゲームに出現すると、すぐに上下左右の隣り合っているブロックと衝突してしまう。このあとスクリプトで「衝突したら画面から消える」コードを実装したら、出てきた瞬間に消えることになる。

このような状況を回避するため、一般的なお作法として、オブジェクトの種類ごとに衝突用のレイヤーを分け、衝突反応を有効にする他のレイヤーを指定する。このレイヤーをコリジョンレイヤーと言う。それでは実際にやってみよう。

まずはわかりやすくゲームで使用するコリジョンレイヤーに名前をつける。「プロジェクト」メニュー>「プロジェクト設定」>「一般」タブ>サイドバーから「Layer Names」カテゴリの「2d Physics」を開き、必要な分だけレイヤーに名前をつけよう。このブロック崩しでは以下のレイヤーを使用する。

  • Paddle
  • Ball
  • Wall
  • Brick
  • Item
    コリジョンレイヤーのプロジェクト設定

では順番にコリジョンレイヤーの設定をしていこう。基本的にはコリジョンを持つノードで以下の手順を同じようにやっていくだけで良い。

  1. シーンドックでコリジョンレイヤーを設定したいノード(ここでは「Paddle」ノード)を選択する。
  2. インスペクタドックで「Collision」セクションを開く
  3. 「Layer」でそのノードが所属するレイヤーを選択する。
    「…」をクリックするとレイヤー名を見ながらチェックして選択できるのでおすすめだ。
    インスペクタでコリジョンレイヤーの設定
  4. 「Mask」でそのノードとの衝突反応を有効にしたいレイヤーを同様に選択する。例えば「Paddle」ノードの場合、衝突を有効にしたいのは「Ball」と「Wall」と「Item」だ。

以上の手順で、コリジョンを持つノードに対して順番に設定していくと以下のようになったはずだ。

  • 「Paddle」ノード
    Paddleのコリジョンレイヤーの設定
  • 「Ball」ノード
    Ballのコリジョンレイヤーの設定
  • 「Wall」ノード
    Wallのコリジョンレイヤーの設定
  • 「Brick」ノード(「Brick.tscn」に切り替えて)
    Brickのコリジョンレイヤーの設定
  • 「Item」ノード(「Powerup.tscn」に切り替えて)
    Powerupのコリジョンレイヤーの設定

これでパワーアップアイテムはパドルとしか衝突しなくなった。


スクリプトで 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キーワードで定数として、値を012… と順番に定義しているのと同義だ。例えば以下のような感じだ。

const SLOW = 0
const EXPAND = 1

enumというのはプルダウンメニューのようなものだ。その要素として大文字でSLOWEXPANDなどいくつか並んでいるが、これらはパワーアップアイテムの種類だ。ここで一度、実装を予定しているパワーアップアイテムを紹介しておく。

  • 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)というシグナルが見つかるはずだ。
シグナルbody_entered

これを「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_1slow_2slow_3までは Sprite 画像をPreloadしている。

そのあとの変数slow_frames[]で括っているので、これは配列(Array)だ。配列の要素として、先に定義したslow_1slow_2slow_3が入っている。この配列は、あとでアニメーションフレームをコードで設定するための下準備となっている。

同様にしてexpandmultiplelaserlifeの順番で画像を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.070.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は「その前のifelifに当てはまらなかったら」という条件も含む。最後のelseは「その前までのifelif全てに当てはまらなかったら」という意味だ。つまり、このブロックの概要としては、先にメソッド内で定義した変数random_numのランダムな値によって、落ちるパワーアップアイテムが変わるようになっている。

そしてitem_list += xxx_framesという形のコードについては、先に定義したitem_listという空っぽの配列に、例えばslow_framesmultiple_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だった場合はLASERchosen_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 xxxxxxには配列や値の範囲を記述するが、このコードでは配列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メソッドを実行している。これは以前にも登場したが、以降にrandfrandom_rangeなどのランダムな値を返すメソッドを実行する際には事前に必ず必要な処理だ。これがないと、毎回同じ値が出力されてしまう。「Game.gd」スクリプトでは、ブロックを消した時にパワーアップアイテムを一定確率で出現させる処理で必要になる。

次に一番下のforループ内の処理で実行しているbrickconnectメソッドだ。これはシグナルをコードで接続するためのメソッドであることは以前にも説明していたが、今回そのメソッドに第四引数を追加した。それが[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_rate1にしているので、パワーアップアイテムが 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」グループを追加しよう。
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_rate1にしたままで確認する。
プロジェクトを実行して最終動作確認その1

以下について問題ないことを確認できた。

  • ブロックを消すと一定の割合でそれぞれのパワーアップアイテムがドロップする。
  • パドルにパワーアップアイテムが衝突するとパワーアップアイテムは画面上から消える。
  • ボールを画面下に落とすと、その時点でドロップしているパワーアップアイテムは画面上から消える。

では次に、drop_rate0.1にして再度プロジェクトを実行してみよう。
プロジェクトを実行して最終動作確認その2

少しハードモードな印象もあるが、割とありそうなアイテムのドロップ率だ。これくらいの確率なら、アイテムが落ちた時にプレイヤーに嬉しい気持ちを感じてもらえるのではないだろうか。もちろん、あなたのお好みの確率に調整していただければ問題ない。



おわりに

以上で Part 10 は完了だ。今回はブロックを消したらランダムでパワーアップアイテムが落ちる仕組みを作った。

次回は個々のパワーアップ処理を実装していく。