Part 11 の今回は、ブロック崩しにパワーアップ機能を実装していく。前回の Part 11 でブロックを消すとパワーアップアイテムが落ちてきて、パドルとアイテムが衝突するとパワーアップが適用される、という仕組みの部分を作ったので、今回は個々のパワーアップ機能自体を実装する。

具体的には以下のパワーアップ機能をそれぞれ作っていく。

  • Slow: ボールのスピードを初期値に戻す(遅くする)
  • Expand: 一定時間、パドルを横に伸ばす
  • Multiple: 一定時間、複数のボールを発射できるようにする
  • Laser: 一定時間、レーザービームを発射できるようにする
  • Life: ライフを一つ増やす(ライフ数の最大は 5)

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


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


パワーアップ機能のための全体的なコード更新

個々のパワーアップの実装をする前に、スクリプト全体を通して必要な更新を先にやってしまおう。「Game.gd」スクリプトのコード量がだんだん多くなってきたが、頑張ろう。では、順番に更新箇所を見ていく。

シーンドックから Ball と Level1 を削除する

「Ball」ノードと「Level1」ノードをシーンドックで最初から「Game」シーンのルートノードに追加しておくのではなく、_readyメソッド実行時にスクリプトによって追加するように変更する。このチュートリアルの中盤で、パワーアップ「Multiple」が有効になった時に、スクリプトで「Ball」ノードを追加する必要がある。ならば、そのような消したり追加したりするノードは、最初からすべてスクリプトでコントロールする方がシンプルだ。「Game」シーン上では、「Ball」ノード、「Level_」ノード、そしてあとで登場予定の「Laser」ノードだ。

ではまずシーンドックで「Game」シーンから「Ball」ノードと「Level1」ノードを削除しよう。

Ballsグループを作ってBallノードを追加

onready 変数を追加・削除する

#onready var level = $Level1 # 削除
# 中略
#onready var ball = $Ball # 削除
# 中略
onready var paddle_scale = paddle.scale # 追加
#onready var ball_position = ball.position # 削除
onready var ball = preload("res://scene/Ball.tscn") # 追加
onready var powerup = preload("res://scene/Powerup.tscn") # 追加
onready var level = null # 変更

今シーンドックから削除したノードを示す変数levelballを削除した。さらに「Ball」ノードのプロパティpositionを示すball_positionの変数も削除した。

代わりに、preloadした「Ball.tscn」と「Powerup.tscn」の PackedScene それぞれの変数ballpowerupを新たに追加した。levelは読み込む PackedScene ファイルの名前につく数字が実際のレベルによって異なるので、preloadできない。いったん、nullとしておき、必要な場面でloadで読み込んだ PackedScene を代入することになる。

あとはpaddle_scaleを新たに追加した。値はpaddle.scaleで、scaleプロパティは「Paddle」ノードの座標位置を Vector2 型データとして保持するプロパティだ。つまり、ゲーム開始前にパドルの初期位置をこの変数に代入している。

_ready メソッドを更新する

func _ready():
	randomize()
	add_new_level() # 追加
	add_new_ball() # 追加
	update_hud_life()
	# For debug
	leave_one_brick(43, level) # ラインを移動
	#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])

新しいメソッドadd_new_leveladd_new_ballを2つ追加した。これらの定義は後ほど説明する。コードの行の順番はこの通りにする必要がある。それぞれのメソッドで「Game」ルートノードの子ノードとしての順番を「Level_」ノードが先、「Ball」ノードが後になるように指定しているためだ。

デバッグ時にブロックを残り1つにするメソッドleave_one_brickは別のラインに移動した。これはadd_new_levelメソッドで現在の「Level」ノード内の各ブロックが全部準備できてからでないと処理の対象がないからだ。

「Ball」ノードはシーンドック上で「Game」シーンから削除済みなので、ball.connect("tree_exited", self, "_on_Ball_tree_exited")_ready内からは削除した。この処理はのちに定義するadd_new_ballメソッド内に追加する。

同じくfor brick in level.get_children():のブロックを削除した。理由は「Level1」ノードはシーンドック上で「Game」シーンから削除済みで、かつadd_new_levelメソッド内で、このforブロックと全く同じシグナル接続処理を含んでいるからだ。

is_playing 変数を追加する

var is_playing = true

今回、新たにis_playingという変数を追加した。これは、ゲームプレイ中であればtrueの値をもつようにする。

使い所だが、ブロックをすべて消してそのレベルをクリアした時に、パワーアップ「Multiple」により、複数のボールが画面上に存在する場面が想定できる。この時、一旦画面上のボールをすべて消してから、新しいボールを追加する処理を、このあとの_on_Ball_tree_exitedメソッドにて実装するのだが、画面上からボールが消えるとライフが一つ減るルールなので、ゲームクリア時はこのis_playing変数をfalseに変更し、falseの場合はライフが減らないように修正していく。

_on_Ball_tree_exited メソッドを更新する

次はボールが画面から消えたら呼ばれる_on_Ball_tree_exitedメソッドだ。今までは画面下に落ちる場合くらいしかこのメソッドが呼ばれることはなかったが、今回のチュートリアルでパワーアップ「Multiple」を実装するにあたり、レベルクリア時に一旦画面上のボールを一掃することになる。その場合もボールが消されるタイミングでこのメソッドが呼ばれる。

ではどのように編集すべきか見ていこう。先ほど定義した変数is_playingも利用する。

func _on_Ball_tree_exited():
	print("_on_Ball_tree_exited() called")
	var no_ball = true # 追加
	for child in get_children(): # 追加
		if child.is_in_group("Balls"):
			print("found ball")
			no_ball = false
			break
			
	if no_ball: # 追加
		if is_playing: # 追加
			life -= 1
			if life <= 0:
				get_tree().change_scene("res://scene/GameOverView.tscn")
			else:
				update_hud_life()
		else:
			is_playing = true
		# Clear powerup items
		for child in get_children():
			if child.is_in_group("PowerupItems"):
				child.queue_free()
		# Set Paddle and Balls as default
		paddle.position = paddle_position
		add_new_ball() # 追加
		#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") # 削除

そこで先に変数no_balltrueとしてBool型の変数として定義しておく。get_childrenメソッドで「Game」の子ノードをforループで順番にチェックしていき、子ノードが「Ball」インスタンスに該当する場合は、メソッド内で新しく定義したno_ballfalseにする(つまり画面上にボールが存在するという意味)。そのあとそのままbreakでループから抜ける。そして、if構文でno_balltrue(つまり「Ball」インスタンスがない)の場合はifブロック内のコードが実行される。

さらに変数is_playingtrueの場合はライフを一つ減らす。ライフがもし0だったらゲームオーバーの画面に遷移するし、まだライフが残っていれば、HUDを更新する。一方、is_playingfalseの場合はゲームクリア直後で次のレベルの準備段階なので、ライフは減らさず、この時点でis_playingtrueに戻しておくのみだ。

あとのno_balltrueだった場合のコードを少し編集した。まず、以下の処理をする4行のコードは削除した。

  • 新しい「Ball」シーンのインスタンスを生成
  • そのインスタンスを「Game」ノードの子ノードにする
  • そのインスタンスの子の順番を3番目にする
  • そのインスタンスのシグナルtree_exitedを「Game.gd」内の_on_Ball_tree_exitedメソッドに接続する

代わりに同様の処理を実行するadd_new_ballというメソッドを追加した。このメソッドの定義はちょうどこのあと説明するところだ。

add_new_ball メソッドを追加する

ではここで「Gamde.gd」スクリプトに新しくadd_new_ballメソッドを以下のように定義して追加しよう。

# Add a new ball
func add_new_ball(): # 追加
	var instance = ball.instance()
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 4)
	instance.connect("tree_exited", self, "_on_Ball_tree_exited")
	instance.mode = 3

「Ball」シーンのインスタンスを「Game」シーンに追加する際に必要な処理をまとめた。最後のinstance.mode = 3
は「RigidBody2D」クラスのプロパティ「Mode」の値を3、すなわち「Kinematic」に設定するということだ。これはゲーム開始時の初期値であり、このモードの間はパドルから勝手にボールが離れることはない。

_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
	if level.get_child_count() <= 0:
		#level.queue_free() # 削除
		#print("level queue free") # 削除
		#print("PowerupItems / Balls queue free") # 削除
		set_next_level()
	else: # 追加
		# Drop powerup item
		drop_powerup(brick_position) # else ブロック内に移動

このメソッドの中のif / else構文のifブロック内にはset_next_levelメソッドのみを残し他の行は、このあとすべてset_next_levelメソッドの中に移動させる。

次にelseブロックだ。このelseブロックは元々はなく、if / else構文が終わった後、その外側の最後の行でdrop_powerupメソッドを実行してパワーアップアイテムを出現させていた。今回elseブロックを用意し、その中にdrop_powerupを移動した。この更新によって「ブロックを消した時、画面上の残りブロック数が0より大きかったら(つまり、まだそのレベルのクリアではなかったら)、パワーアップアイテムを出現させる」という意味になった。逆を言うと、そのレベルをクリアした時はアイテムは出現させない、という意味になる。

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)
		call_deferred("move_child", powerup_instance, 5) # 追加
		powerup_instance.connect("item_collided", self, "_on_Powerup_item_collided") 

追加したのは、6行目のcall_deferred("move_child", powerup_instance, 5)だ。call_deferredを利用しているが、実際の処理はmove_childメソッドによる子ノードの順番指定だ。

add_childメソッドでPowerup.tscnシーンのインスタンスを「Game」ノードの子ノードにした時、デフォルトでは、子ノードの一番最後の順番に配置される。ここで問題になるのは、画面上にパワーアップアイテムがドロップしている最中に、そのレベルをクリアした場合、デフォルトでは「NextScreen」ノードより全面にパワーアップアイテムが配置されてしまうため、レベルをクリアした時に「NextScreen」の上にパワーアップアイテムが表示され、奇妙な画面になってしまう。そこで、それぞれの子ノードが以下の順番になるように「Powerup」のインスタンスノードの順番を5に指定している。

  1. HUD
  2. Paddle
  3. Wall
  4. Level_(インスタンス)
  5. Ball(インスタンス)
  6. Powerup(インスタンス)
  7. PauseScreen
  8. NextScreen

もちろん、ゲームプレイ中にドロップ中のパワーアップアイテムが複数あることもあれば、パワーアップアイテム「Multiple」の機能によって、複数のボールが生成されることもあり、上記のような綺麗な順番にはならないが、少なくとも毎回5番目を指定すれば「PauseScreen」や「NextScreen」より全面に表示されることはない。

set_next_level メソッドを更新する

set_next_levelメソッドは、あるレベルのブロックを全て消してクリアした時に実行される、次のレベルの諸々の準備をするメソッドだ。

func set_next_level():
	print("set_next_level() called")
  # Change status
	is_playing = false
	print("is_playing: ", is_playing)
  
	# Clear left objects # 追加
	level.queue_free() # 移動
	print("level.queue_free() called") # 移動
	for child in get_children(): # 追加
	if child.is_in_group("Balls"): # 追加
		child.queue_free()
	print("PowerupItems/Balls/Lasers group: child.queue_free() called") # 追加

	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 # 削除
	#add_new_ball() # 削除
	#ball.position = ball_position # 削除
	#ball.mode = 3 # 削除

	# Set next Level node
	add_new_level() # 追加
	#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

「# Clear left objects」とコメントしている下の6行のコードをまるまる追加した(print関数はデバッグようなのでお好みで消して欲しい)。

このうちlevel.queue_freeは別のメソッド_on_Brick_tree_exitedから移動したコードだ。クリアして不要になった「Level_」ノードを消すことも、このメソッドの役割とした。

続いてget_childrenメソッドで得られる「Game」ルートノードの子ノードで回すforループのブロック3行は新たに追加した。まずifブロックは『ブロックがすべて消えたら(つまり、そのレベルをクリアしたら)』という条件になっている。この状況では、次のレベルの設定を開始する前に、画面上に残っているボールを全て消す必要がある。それをこのforループで行っている。ボールをすべて消すとシグナルによって呼ばれる_on_Ball_tree_exitedメソッドによって、画面上に残っているドロップ中のパワーアップアイテムも一掃されるため、このメソッドに記述する必要はない。

あとadd_new_levelメソッドを新たに追加し、その下にあるコードを削除しているが、これらの削除したコードはこの新しいメソッドに移動させてまとめた形だ。

では、これらのメソッドの定義を見ていこう。

add_new_level メソッドを追加する

add_new_levelメソッドを新たに定義した。

# Add new level
func add_new_level(): # 追加
	level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
	add_child(level)
	move_child(level, 3) # 5 から 3 に変更
	for child in level.get_children():
		child.connect("tree_exited", self, "_on_Brick_tree_exited", [child.global_position]) # 第四引数追加 

このメソッドの内容は、さきほどのset_next_levelメソッド内で元々処理していた部分をメソッドにまとめたにすぎない。しかし、少し細かいところを調整している。

まずmove_childメソッドの第二引数を5から3に変えた。これは新しい「Level_」シーンのインスタンスを「Game」ルートノードの子に追加した時の子ノードの順番である。4番目は「Ball」シーンのインスタンスノードが、5番目は「Powerup」シーンのインスタンスノードが使用するためだ。この順番が少し違っていても大した支障はないだろうが、「Ball」および「Powerup」のインスタンスノードは、ゲーム中に複数生成されることになるので、順番を後ろに持ってきた方が「Level_」ノードが常に同じ順番になって、後々問題にならなくて良い。

ひとまず全体的な更新はここまでだ。なかなか骨の折れる作業だったのではないだろうか。いよいよ個別のパワーアップ機能の実装をしていく。



パワーアップ「Slow」を実装する。

パワーアップアイテム「Slow」の機能はボールのスピードを初期値に戻すことだ。では実装していこう。

新しいノードグループ「Balls」を作って「Ball」ノードを追加する

まずは、新しく「Balls」というノードグループを作成して「Ball」ノードを追加しておこう。これは、あとで Multiple アイテムを実装することを想定して、複数のボールが画面上にある場合にコードを作りやすくするためだ。
Ballsグループを作ってBallノードを追加

次に「Game.gd」スクリプトを更新していこう。

ボールのスピードを初期値に戻すメソッドを定義する

まずボールのスピードを初期値にするメソッドを追加した。以下のslow_ballだ。

func slow_balls():
	for child in get_children():
		if child.is_in_group("Balls"):
			child.ball_speed = child.first_speed

get_childrenメソッドで、「Game」ノードの子ノード全てにアクセスする。これはのちにパワーアップアイテム「Multiple」の機能が有効になった時に、ゲーム画面上にボールがたくさん存在する状態になるので、それら全てにアクセスするための布石だ。

次にif構文で、そのアクセスした子ノードそれぞれに対して、順次「Balls」ノードグループのメンバーかどうかをチェックし、メンバーだったら、次の行のコードが実行される。そして実行されるのが、それぞれの Ball ノードの変数ball_speed(現在のスピード)に、別の変数first_speed(最初のスピード)を代入する、というコードだ。これによって、ボールがゲーム開始時と同じスピードに戻る、という流れになっている。

Slow が有効になったら slow_balls メソッドを実行するようにする

_on_Powerup_item_collided メソッドを編集しよう。

func _on_Powerup_item_collided(item):
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			print("Game: ", item)
		2: # MULTIPLE
			print("Game: ", item)
		3: # LASER
			print("Game: ", item)
		4: # LIFE
			print("Game: ", item)

_on_Powerup_item_collidedメソッドの中のmatchブロック内で、引数itemの値が0(つまり enum の要素Powerup.SLOW)と一致した場合に実行するコードとして、先に定義したslow_ballsメソッドを追加した。

プロジェクトを実行して動作確認

では一度プロジェクトを実行して「Slow」の動作を確認しておこう。
パワーアップ Slow の動作確認

ちなみにslow_ballsメソッド内のchild.ball_speed = child.first_speedのコードの前後にprint(child.ball_speed)というコードを2つ用意することで、「出力」コンソールで数値として本当にスピードが初期値の150に戻っているのかを確認できる。あくまでデバッグ用だ。

print(child.ball_speed)
child.ball_speed = child.first_speed
print(child.ball_speed)

出力コンソールでball_speedの初期化を確認



パワーアップ「Expand」を実装する

次はパワーアップアイテム「Expand」の機能だ。「Expand」は一定時間、パドルを横に伸ばす機能だ(Stretch という名前の方が意味は正確だったかもしれない)。このアイテムは以下の仕様で実装していく。

  • 伸びた後の長さは通常の2倍
  • 長さが元に戻るまでの時間は 10 秒
  • 長さが元に戻るまでは何回「Expand」アイテムと衝突しても長さは伸びない
  • 長さが元に戻るまでの時間は何回「Expand」アイテムと衝突しても増えない
  • ボールが落ちたり、ブロックをすべて消してレベルをクリアしたら通常の長さに戻る

ではまた「Game.gd」スクリプトを編集していこう。

onready 変数でパドルの最初の長さを定義する

まずonreadyつきの変数を一つ追加した。

onready var paddle_scale = paddle.scale # Added @ P11

これは「Paddle」ノードのscaleプロパティの初期値を格納しておく変数だ。パドルのサイズを元に戻すときに役に立つ。

ボールが画面から消えた時にパドルの長さを初期値に戻す

次に_on_Ball_tree_exitedメソッドにコードを1行追加した。

# Method receiving Ball signal
func _on_Ball_tree_exited():
	var ball_count = 0 
	
	for child in get_children():
		if child.is_in_group("Balls"):
			ball_count += 1
			
	if ball_count <= 0:
		life -= 1
		update_hud_life()
		
		if life <= 0:
			get_tree().change_scene("res://scene/GameOverView.tscn")
		else:
			for child in get_children(): 
				if child.is_in_group("PowerupItems"):
					child.queue_free()
			paddle.position = paddle_position
			paddle.scale = paddle_scale # 追加
			add_new_ball()

追加したのはif / else構文のelseブロックのpaddle.scale = paddle_scaleという1行だ。このメソッドはボールが画面下に落ちた時にシグナルによって実行される。この時、ライフがまだ残っている状態では、パドルを初期位置に戻すついでに、「Expand」で2倍に伸びていたサイズも元に戻すようにしているのだ。

レベルをクリアしたらパドルの長さを初期値に戻す

次にset_next_levelメソッドにコードを1行追加した。

func set_next_level():	
	#(中略)
	
	# Set Paddle and Ball the first position
	paddle.position = paddle_position
	paddle.scale = paddle_scale # 追加
	add_new_ball()
	# Set next Level node
	add_new_level()
	# Pause game until NextScreen is hidden
	get_tree().paused = true

このメソッドは、すべてのブロックを消してそのレベルをクリアした時に実行されるメソッドだが、その中で「Paddle」ノードの位置を初期値に戻すコードを実行しているので、その次にサイズも初期値に戻すコードを追加した。これで、パドルサイズが2倍になっている状態でレベルをクリアしても、次のレベルでは通常サイズに戻ることになる。

パドルの長さを 10 秒間 2 倍にするメソッドを定義する

そして次に、expand_paddleメソッドを新たに追加した。

func expand_paddle(): # 追加
	if paddle.scale <= paddle_scale:
		paddle.scale.x *= 2
		yield(get_tree().create_timer(10), "timeout")
		paddle.scale = paddle_scale

このメソッドは、「Expand」のパワーアップ機能そのものだ。if構文で、パドルのscaleプロパティが初期値だったら、scaleプロパティ(Vector2型)の x 要素を2倍にするようにしている。

さらにyieldでワンショットタイマーを10秒でセットし、タイムアウトしたタイミングでまたscaleプロパティを初期値に戻すようにしている。つまり、これで10秒間だけパドルが長くなる機能を実装しているのだ。

このメソッドは冒頭のif構文により、すでにパドルが伸びている状態で「Expand」アイテムをゲットしても効果や秒数が追加されないようにしている。

Expand が有効になったら expand_paddle メソッドを実行するようにする

そして、今作成したexpand_paddleメソッドを、_on_Powerup_item_collidedメソッドのmatchブロック内で、衝突したアイテムが1(つまり enum Powerup.EXPAND)と一致した場合に実行されるように追加した。

func _on_Powerup_item_collided(item): # Updated @ P11
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			expand_paddle()
		2: # MULTIPLE
			print("Game: ", item)
		3: # LASER
			print("Game: ", item)
		4: # LIFE
			print("Game: ", item)

プロジェクトを実行して動作確認

ではプロジェクトを実行して、「EXPAND」機能が以下の条件で実装できているか確認しておこう。

  • 伸びた後の長さは通常の2倍
  • 長さが元に戻るまでの時間は 10 秒
  • 長さが元に戻るまでは何回「Expand」アイテムと衝突しても長さは伸びない
  • 長さが元に戻るまでの時間は何回「Expand」アイテムと衝突しても増えない
  • ボールが落ちたり、ブロックをすべて消してレベルをクリアしたら通常の長さに戻る

パワーアップ Expand の動作確認



パワーアップ「Multiple」を実装する

次はパワーアップ「Multiple」を実装していく。このパワーアップの具体的な仕様は以下のようにしていく。

  • 3秒間、複数のボールを発射できるようにする。
  • 無効になるまでの3秒間で、新たにこのパワーアップアイテムと衝突しても時間は加算されない。
  • 3秒後、新しくボールを発射することはできなくなるが、発射済みのボールが強制的に消されることはない。

では「Game.gd」スクリプトを編集していこう。

変数 is_multiple_on を追加する

新しい変数is_multiple_onを定義した。

var is_multiple_on = false # 追加

これは Bool 値をとり、初期値はfalseとする。これは、パワーアップ「Multiple」が有効かどうかの情報を保持する変数だ。「Multiple」アイテムと衝突したらtrueになるように別途コーディングする。

enable_multiple_balls メソッドを追加する

次にパドルが「Multiple」アイテムと衝突した時に実行する新しいメソッドenable_multiple_ballsを追加した。

func enable_multiple_balls(): # 追加
	if not is_multiple_on:
		add_new_ball()
		is_multiple_on = true
		yield(get_tree().create_timer(3), "timeout")
		is_multiple_on = false

このメソッドの中身はif構文になっていて、さきほど適宜した変数is_multiple_onfalseだったら、という条件定義だ。これに当てはまる場合、以下の処理を順番に実施していく。

  1. add_new_ballメソッドで、新しいボールを1つだけパドルの上に生成する
  2. is_multiple_ontrueにする
  3. yieldで3秒間のワンショットタイマーを作成し、タイムアウトまで次のコードを待機させる
  4. タイムアウトしたら、再び変数is_multiple_onfalseに戻す

Multiple が有効になったら enable_multiple_balls メソッドを実行するようにする

さきほど定義したenable_multiple_ballsメソッドをドロップした「Multiple」アイテムとパドルが衝突した時に実行したい。そのためには、_on_Powerup_item_collidedメソッドのmatchブロック内で、引数item(衝突したアイテム)が2(Multiple)だった場合に実行されるコードとして追加すれば良い。コードは以下のようになる。

func _on_Powerup_item_collided(item): # Updated @ P11
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			expand_paddle()
		2: # MULTIPLE
			enable_multiple_balls()
		3: # LASER
			print("Game: ", item)
		4: # LIFE
			print("Game: ", item)

_process メソッドでキー入力で複数のボールを発射できるようにする

enable_multiple_ballsメソッドはパドルの上に1つだけボールを追加するが、その後、複数のボールを発射可能にする処理はまた別で記述していく必要がある。それが以下_processメソッドのコードだ。

func _process(_delta): # 追加
	if is_multiple_on and Input.is_action_just_pressed("launch_ball"):
		add_new_ball()

このメソッドは毎フレーム実行されることは以前に説明済みかもしれない。今回このメソッド内はif構文になっている。以下の2つの条件が揃った時にadd_new_ballメソッドが実行される。

  • 変数is_multiple_ontrueであること
  • インプットマップに登録された「launch_ball」アクションに当てはまるキー入力がされたこと

これで、パドルが「Multiple」アイテムに衝突してから3秒間は「space」キーを押すたびに新しいボールを発射できるようになった。

プロジェクトを実行して動作確認

ではパワーアップ「Multiple」がうまく実装できたか確認してみよう。
パワーアップ Multiple の動作確認

  • パワーアップアイテム「Multiple」がパドルに衝突したら、パドルの上に1つボールが追加された
  • そこから3秒間、spaceキーを連打した分だけボールが連射された
  • 3秒後に連射機能は無効になり、画面上に発射したボールだけが残った
  • 一番最初のボールが落ちても、他のボールが残っているのでライフは減らない

もし3秒という時間がしっくりこない場合は、お好みで適宜調整していただければと思う。



パワーアップ「Laser」を実装する

ドロップしたパワーアップアイテム「Laser」と衝突後、一定時間、レーザービームを発射できるようにする。具体的な仕様は以下のようにする。

  • 3秒間パドルからレーザーを打ち放題になる
  • レーザーがブロックに当たるとレーザーとブロックの両方が消える
  • レーザーが画面外に出た場合はレーザーのインスタンスが消去される
  • ブロックをすべて消してそのレベルをクリアすると、残ったレーザーは消える(次のレベルに持ち越さない)

発射されるレーザーオブジェクトは「Game」シーンに複数現れては消えることになる。ということは雛形となる「Laser」シーンを先に作成し、「Game」シーンではそのインスタンスをノードとして追加したり削除したりすれば良い。


「Laser」シーンを作る

「Game」シーンにレーザーを登場させるためには、その雛形となるシーンを新たに作成する必要がある。そのインスタンスを「Game」シーンにノードとして追加していくパターンだ。


シーンを新規作成する

それではシーンを新しく作成していく。例によって、「シーン」メニュー>「新規シーン」から開始しよう。

まずは「Area2D」クラスのノードをルートノードとして追加する。この時点でシーンを保存しておこう。ファイル名はそのまま「Laser.tscn」で問題ない。

続いて子ノードを追加していく。

子ノードを追加する

「Laser」ノードに以下の3つのノードを追加しよう。

  • Line2D
  • CollisionShape2D
  • VisibilityNotifier2D

Lasersグループの追加

追加したら、次はそれぞれのノードをインスペクタドックやノードドックで編集していく。

Laser ノードを新しいグループに追加する

まず、ルートノードの「Laser」については、プロパティはデフォルトのままで良いが、ノードグループを新たに作成して追加しておこう。ノードドック>グループから「Lasers」の名前でグループを作成して追加する。
Lasersグループの追加

これは、画面上に複数のレーザーが表示されている状況で、まとめてそれらに対して処理を施す場合にとても便利になる。これまでも「Ball」シーンや「Brick」シーンで利用してきた手法だ。

コリジョンレイヤーの設定

「Laser」ノード用のコリジョンレイヤーがまだないので、設定していく。まずは「プロジェクト」メニュー>「プロジェクト設定」>「一般」タブ>サイドバーから「2d physics」を開く。次に「Layser 6」に「Laser」と言う名前をつける。

次にシーンドックで「Laser」ノードを選択し、インスペクタで「Collision」プロパティを編集する。「Layer」がそのノードが属するレイヤー、「Mask」は衝突反応を有効にするレイヤーだ。

まずは「Layer」プロパティで「Laser」レイヤーを選択、続いて「Mask」プロパティはブロックだけ衝突して欲しいので「Brick」レイヤーのみを選択した。
コリジョンレイヤー追加後Collisionプロパティの設定

Line2D ノードのプロパティを編集する

次にインスペクタドックで「Line2D」ノードのプロパティを編集する。

まず「Points」プロパティの値になっている「PoolVector2Array(size 0)」をクリックしよう。すると、「サイズ」を調整できるので 2 に変更してみると、「Line2D」の線にポイントが追加される。折線にする場合は、この「サイズ」を複数追加して、それぞれのポイントの座標を指定するわけだ。今回は短い直線でレーザービームを表現するため、ポイント 0 の座標を(0, 0)、ポイント 1 の座標を(0, -24)にする。すると(0, 0)から真っ直ぐ上に長さが - 24 ピクセルの直線ができるはずだ。
Line2Dプロパティ1

続いてプロパティ「Width」は 4 ピクセルを指定してやや細いレーザーにする。「Default Color」は「77bfff」でちょっと水色っぽい青を指定する。このあたりはお好みで調整して欲しい。
Line2Dプロパティ2

そして「Capping」カテゴリの「Joint Mode」プロパティについて。これは線のポイントがもっと複数ある場合に、ポイントとポイントをどのような形状でつなぐかを指定する。今回はポイントが 2 つしかないのでデフォルトの「Sharp」ままにしておく。「Begin Cap Mode」と「End Cap Mode」はどちらも「Round」にして、線の両端を丸い形状にしておこう。
Line2Dプロパティ3

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

例によってインスペクタドックにて「Shape」プロパティを編集する。今回は「CapsuleShape2D」を選択しよう。続けて 2D ワークスペースで、ハンドルをドラッグして、さっき作った「Line2D」ノードと全く同じ形にしてきれいに重なるように調整しよう。調整するときはツールバーで「ピクセルスナップを使用」を有効にすると作業しやすくなる。
CollisionShape2DのShapeを編集

VisibilityNotifier2D ノードのプロパティを編集する

このゲーム開発では2回目の登場である「VisibilityNotifier2D」ノードは、ノードが画面外に出た時にそれを検知してシグナルを発信することができる。これを利用して、レーザーが画面外に出たらそのレーザーのノードを開放するのが狙いだ。

こちらのノードも「Rect」プロパティを他のノードのサイズに合わせる形で以下の通りに調整していく。

  • x: -2
  • y: -26
  • w: 4
  • h: 28

VisibilityNotifier2DのRectを編集

2D ワークスペースを見ながら枠が他のノードと一致しているか確認しておこう。
VisibilityNotifier2Dを2Dワークスペースで確認

インスペクタドックでの編集作業は一旦これで終わりだ。


スクリプトでレーザーを動かす

インスペクタで調整しきれない部分はスクリプトで制御していく。特にレーザーの動きの部分だ。

ではここで「Laser」ノードにスクリプトをアタッチする。スクリプトファイルは「res://scripts/Laser.gd」として保存しておく。

スクリプトに以下のコードを記述する。

extends Area2D


export (float) var laser_speed = 500


func _physics_process(delta):
	position += Vector2.UP * laser_speed * delta

まずlaser_speedという変数を定義している。これは名前の通り、レーザーの速度だ。500 ピクセル/秒とした。exportキーワードでインスペクタドックでも編集できるようにした。

続いて、_physics_processメソッドを定義した。

やっていることは、単純にpositionプロパティを毎フレーム変更して上方向に移動させているだけである。細かく分解すると、まずVector2.UPと言うのは方向だけを示した長さが 1 のベクトルで(0, -1)にあたる。これはゲーム画面上で真っ直ぐ上の方向だ。これがレーザーの飛んでいく向きを決めている。これにさっき定義したlaser_speedを乗じて方向を持った速度にし、さらにdeltaを乗じることで、1フレーム当たりの移動距離を計算している。最終的にこれを現在のpositionプロパティの値に加算して、positionそのものを更新している。


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

ここで一度、ルートノードを画面の中央下部のあたりに一時的に移動させて、動作確認しておこう。
シーンを実行してレーザーの動きを確認

レーザーの速度が早いと感じたら、適宜プロパティ「laser_speed」を小さくして確認してみて欲しい。


シグナルを接続する

「Game.tscn」シーンに、「Laser」シーンのインスタンスノードを追加するにあたって、以下の2つのシグナルが必要になる。

  • ブロックに衝突した時に発信するシグナル
  • 画面外に出た時に発信するシグナル

ではシグナルを接続していこう。
まずは「Laser」ルートノードの「body_entered」というシグナルを「Laser.gd」スクリプトに接続しよう。そして、接亜属先のメソッド_on_body_entered(body)を編集していこう。

func _on_body_entered(body):
	if body.is_in_group("Bricks"):
		body.queue_free()
		queue_free()

メソッドの内容は『もし衝突したのが「Bricks」グループのメンバーだったら、ブロックを消す、そしてレーザー自身も消す』という内容になっている。

次は「VisibilityNotifier2D」ノードのシグナル「screen_exited」というシグナルを「Laser.gd」スクリプトに接続しよう。接続先のメソッド_on_VisibilityNotifier2D_screen_exitedを編集していく

func _on_VisibilityNotifier2D_screen_exited():
	queue_free()

このメソッドでやるべきことは、画面外に出たレーザーを消すことだ。つまりqueue_freeメソッドを実行するのみである。余計なメモリを消費させないためにも、不要なオブジェクトはできるだけ速やかに消した方がいい。

これで「Laser_tscn」シーンの編集は完了だ。シーンドックは以下のようになっているだろう。
シーンドックの確認


Game シーンにパワーアップ Laser の機能を実装する

ここからは「Game」シーンにパワーアップアイテム「Laser」の機能を追加していく。やるべきことは「Multiple」の時と似たようなものなので、楽な気持ちで進めていこう。

それでは「Game.gd」スクリプトを開いて順番にアップデートしていこう。

今 Laser が有効かどうかを示す変数を追加する

以下の変数を追加する。

var is_laser_on = false 

この変数is_laser_onfalseの間はパワーアップ「Laser」が無効の状態、trueの場合は有効の状態を示すものとして定義した。

Laser シーンの PackedScene ファイルを Preload する

次はlaserという変数で、preloadした「Laser.tscn」の PackedScene ファイルを値として定義する。

onready var laser = preload("res://scene/Laser.tscn")

preloadしたファイルからレーザーのインスタンスを複数生成することになるため、変数を定義しておくと便利だ。

Laser が有効になったら enable_laser メソッドを実行するようにする

この_on_Powerup_item_collidedメソッドはパワーアップアイテムがパドルに衝突した時のシグナルによって実行され、match構文によって、アイテムの種類ごとに異なる処理を実行する。

func _on_Powerup_item_collided(item): # Updated @ P11
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			expand_paddle()
		2: # MULTIPLE
			enable_multiple_balls()
		3: # LASER
			enable_laser() # 追加
		4: # LIFE
			print("Game: ", item)

パワーアップアイテムが「Laser」だった場合は、enable_laserというメソッドを実行している。続いて、このメソッドを定義しよう。

enable_laser メソッドでパワーアップ Laser を有効にする処理を実装する

こちらが新たに定義したメソッドenable_laserだ。

func enable_laser():
	if not is_laser_on:
		is_laser_on = true
		print("is laser on") # デバッグ用
		yield(get_tree().create_timer(3), "timeout")
		is_laser_on = false
		print("is laser off") # デバッグ用

このメソッドはまず全体が一つのifブロックになっていて、is_laser_onfalseの場合のみ実行され、trueの場合は何もせずに終了する。ではif構文がtrueだった場合に実行される処理を見ていこう。

まずis_laser_on = trueのコードでis_laser_ontrueに変更する。trueの間だけレーザーを好きなだけ発射できるように別のメソッドで実装するが、このメソッドではとにかくこの変数の値を変更してステータス管理のみを行う。yield3秒間のタイマーをセットし、タイムアウトしたらis_laser_on = falseで、また「Laser」機能が無効の状態に戻るようにしている。

次は、先に作っていおいた「Laser.tscn」シーンのインスタンスを作成して「Game」シーンに追加するメソッドを定義していく。

fire_laser メソッドでレーザーを発射できるようにする

以下のようにfire_laserというメソッドを定義しよう。

func fire_laser():
	var instance = laser.instance()
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 6)
	instance.position.x = paddle.position.x
	instance.position.y = paddle.position.y - 16

やっていることはボールの追加と同様で、以下の通りである。

  1. preloadしておいた「Laser.tscn」ファイルのインスタンスの作成する
  2. 作成したインスタンスをadd_childで「Game」ルートノードの子にする
  3. 「Game」ルートノードの子ノード内の順番を6番目に変更する
  4. インスタンスのpositionプロパティの x 座標を「Paddle」のそれと同じにする
  5. インスタンスのpositionプロパティの y 座標を「Paddle」より 16 ピクセル上にする

「Laser」ノードは生成された瞬間から、それ自身がqueue_freeで消されるまで、画面の上方向へ飛んでいく。

このfire_laserメソッドが実行されるのは、プレイヤーがキー入力した時だけにしたい。そこで「Multiple」の時と同様に_process内にInputのメソッドを使って記述していく。

_process メソッドでキー入力によるレーザー発射を実装する

_processメソッドにはすでに、パワーアップ「Multiple」でボールを複数連射するコードが2行あるので、その下に記述していく。

func _process(_delta):
	if is_multiple_on and Input.is_action_just_pressed("launch_ball"):
		add_new_ball()
	if is_laser_on and Input.is_action_just_pressed("ui_up"): # 追加
		fire_laser()

レーザーはインプットマップにデフォルトで登録されている「ui_up」アクションのキー操作で発射されることにした。「ui_up」に割り当てられているのはキーボードの「上矢印」キーだ。キーを叩いただけレーザーが発射される。

ここまでの内容を少しまとめておこう。

  1. パワーアップアイテムがパドルと衝突すると_on_Powerup_item_collidedメソッドが呼ばれる
  2. パワーアップアイテムが「Laser」だった場合、_on_Powerup_item_collidedメソッド内でenable_laserメソッドが呼ばれる
  3. enable_laserメソッド内で、変数is_laser_onが3秒間だけtrueになる
  4. 変数is_laser_ontrueの間だけ上矢印キーが入力をされるたびに_processメソッド内でfire_laserメソッドが呼ばれる
  5. fire_laserメソッドにより「Laser.tscn」シーンのインスタンスが生成され、レーザーが発射される

set_next_level のブロックをすべて消した時にレーザーも全て消す処理を追加する

ではset_next_levelに処理を追加していく。

func set_next_level():
	print("set_next_level() called")
	
	# Change status
	is_playing = false
	print("is_playing: ", is_playing)

	# Clear left objects
	level.queue_free()
	print("level.queue_free() called")
	for child in get_children():
		if child.is_in_group("Balls") or child.is_in_group("Lasers"): # 追加
			child.queue_free()
  #(後略)

コメントで「# 追加」と記載しているところが変更点だ。

ブロックをすべて消して、次の新しいレベルの諸々の準備をする前に、「Game」ノードの子ノードをチェックする。その時、「Balls」グループと一緒に「Lasers」グループもチェックして、グループに属するノードが残っていれば、すべて消すようにした。

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

他のアイテムがドロップすると確認の効率が悪いので、「Powerup/gd」スクリプトのadd_sprite_framesメソッドを以下のように一時的に編集してからプロジェクトを実行しよう。

パワーアップ Laser の動作確認 1

パワーアップ Laser の動作確認 2



パワーアップ「Life」を実装する

最後はパワーアップ「Life」の機能を実装する。ここまでやってきた他のパワーアップアイテムに比べてかなり簡単な作業なので気楽にやっていこう。

定数でライフの上限を設定する

MAX_LIFEという定数を定義しよう。

const MAX_LIFE = 5

UI上、ライフは5つまでしか表示できないようにしているので、その上限をスクリプト上で設定する必要がある。変わることがない値なので、変数ではなく定数としてキーワードconstを使って定義した。

上限に達してなければライフが1つ増えるメソッドを作る

単純にライフが 1 増えるメソッドを定義しよう。名前はadd_lifeにする。

func add_life():
	if life < MAX_LIFE:
		life += 1
		update_hud_life()

if構文で、変数lifeの現在の値が先ほど定義した定数MAX_LIFEの値5より小さければ、ライフを 1 増やす処理をしている。変数の値だけ増えてもプレイヤーはわからないので、update_hud_lifeメソッドで、HUDのライフの表示も更新させている。

Life が有効になったら add_life メソッドを実行するようにする

パワーアップアイテム「Life」とパドルが衝突したら、add_lifeメソッドを実行するように、_on_Powerup_item_collidedメソッドのmatch構文を編集していこう。

matchブロック内で、引数item(衝突したアイテム)が4(LIFE)だった場合に実行されるコードとして追加すれば良い。コードは以下のようになる。

func _on_Powerup_item_collided(item): 
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			expand_paddle()
		2: # MULTIPLE
			enable_multiple_balls()
		3: # LASER
			enable_laser()
		4: # LIFE
			add_life() # 追加

パワーアップ「Life」の実装はこれだけだ。とても簡単だったのではないだろうか。

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

では、最後にライフ追加の機能がきちんと実装されているかどうかチェックをしておこう。

  • ドロップした「Life」アイテムにパドルが衝突したらライフが 1 増えること
  • ライフが 5 の場合はそれ以上増えないこと

パワーアップ Life の動作確認



全体を通して

最後に全体を通して調整と確認をしておこう。


細かなバグの修正

この時点で確認できた細かなバグを修正する。

「Multiple」「Laser」が残るバグを修正する

2021/12/23 追加

ボールが画面上からすべて消えた時やレベルをクリアして次のレベルに切り替わった時に、まだ「Multiple」のボールや「Laser」のビームが残っていることがある。これはパワーアップが有効な 3 秒の時間がまだ残っている場合に発生する。

「Game.gd」スクリプトを以下のように編集する。

func _on_Ball_tree_exited():
	print("_on_Ball_tree_exited() called")
	#(中略)
			
	if no_ball: # Added and Edited @ P11
		#(中略)
		# Set Paddle and Balls as default
		is_multiple_on = false # 追加
		is_laser_on = false # 追加
		paddle.position = paddle_position
		paddle.scale = paddle_scale
		add_new_ball()

まずはボールが画面上から消えた時に実行される_on_Ball_tree_exitedメソッドだ。if no_ballのブロックで、ボールが画面上からすべて無くなったら、という条件の下、is_multiple_on = falseおよびis_laser_on = falseの処理により、「Multiple」や「Laser」の有効時間3秒がまだ残っていても、強制的に無効になるようにした。

func set_next_level():
	print("set_next_level() called")
	# Change status
	is_playing = false
	is_multiple_on = false # 追加
	is_laser_on = false # 追加
	
	#(後略)

続いて編集するのは、次のレベルに切り替えるためのset_next_levelメソッドだ。これも先ほどと考え方は同様で、メソッドが呼ばれたらすぐにis_multiple_on = falseおよびis_laser_on = falseの処理で、「Multiple」と「Laser」を無効化している。


ボール生成時に一瞬だけ x 座標 0 に表示されるバグを修正する

2021/12/23 追加

最後に、「Multiple」が有効な時に、ボールが生成される一瞬だけ、その新しいボールが x 座標0の位置に見える現象を修正する。

まずは「Ball.tscn」を開き、インスペクタで「Ball」ルートノードの「Position」プロパティをデフォルトの(0, 0)に戻そう。
BallのPositionを初期値に戻す

あとは念の為程度の修正だが、再度「Game.gd」スクリプトを見てほしい。

func add_new_ball(): # Added @ P11
	print("add_new_ball() called")
	var instance = ball.instance()
	instance.position = Vector2(paddle.position.x, paddle.position.y - 10) # 追加
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 4)
	instance.connect("tree_exited", self, "_on_Ball_tree_exited")	
	#instance.mode = 3 # 削除

修正するのはadd_new_ballメソッドだ。

instance.position = Vector2(paddle.position.x, paddle.position.y - 10)のコードを1行、インスタンス生成直後に追加した。これでボールの x 座標が画面に表示されるタイミングからパドルのそれに一致するようになる。

ついでにinstance.mode = 3のコードは、元々インスタンス生成時に3になっており不要だったので削除した。


アイテムのドロップ率を通常に戻す

アイテムのドロップ率を1にしていた。これは100%という意味だ。パワーアップアイテムのデバッグを終えたので、通常のドロップ率に戻そう。お好みで0.5~0.25の間くらいで適当に調整しておこう。
パワーアップ Life の動作確認

Game.gd スクリプト全体の確認

「Game.gd」スクリプトの全体が最終的にどうなったのか、念の為、最後にここで公開しておく。確認されたい方は展開して確認していただければ結構だ。

「Game.gd」スクリプト全体のコード
extends Node2D

const POINT = 100
const MAX_LIFE = 5 # Added @ P11

export var drop_rate = 0.2

var level_num = 1
var score = 0
var bonus_rate = 1.0
var life = 3
var is_playing = true
var is_multiple_on = false # Added @ P11
var is_laser_on = false # Added @ P11

#onready var level = $Level1 # Removed @ P11
onready var next_screen = $NextScreen
onready var next_screen_level = $NextScreen/VBox/Level
onready var next_screen_score = $NextScreen/VBox/Score
onready var next_screen_life = $NextScreen/VBox/HBox/Life
onready var hud_level = $HUD/LeftBox/Level
onready var hud_score = $HUD/LeftBox/Score
onready var hud_rightbox = $HUD/RightBox
onready var paddle = $Paddle
#onready var ball = $Ball # Removed @ P11
onready var pause_screen = $PauseScreen

onready var paddle_position = paddle.position
onready var paddle_scale = paddle.scale # Added @ P11
#onready var ball_position = ball.position # Removed @ P11
onready var ball = preload("res://scene/Ball.tscn") # Added @ P11
onready var laser = preload("res://scene/Laser.tscn") # Added @ P11
onready var powerup = preload("res://scene/Powerup.tscn") # Added @ P10
onready var level = null # Updated @ P11

func _ready():
	randomize() # Added @ P10
	add_new_level() # Added @ P11
	add_new_ball() # Added @ P11
	update_hud_life()
	# For debug
	#leave_one_brick(43) # Moved @ P11
	#ball.connect("tree_exited", self, "_on_Ball_tree_exited") # Removed @ P11
	#for brick in level.get_children(): # Removed @ P11
		#brick.connect("tree_exited", self, "_on_Brick_tree_exited", [brick.global_position]) # Updated @ P10


func _process(_delta): # Added @ P11
	if is_multiple_on and Input.is_action_just_pressed("launch_ball"):
		add_new_ball()
	if is_laser_on and Input.is_action_just_pressed("ui_up"):
		fire_laser()


# For debug
func leave_one_brick(brick_num: int):
	for child in level.get_children():
		if child.get_name() == "Brick" + str(brick_num):
			continue
		child.queue_free()

# Method receiving Ball signal
func _on_Ball_tree_exited():
	print("_on_Ball_tree_exited() called")
	var no_ball = true # Added @ P11
	for child in get_children(): # Added @ P11
		if child.is_in_group("Balls"):
			print("found ball")
			no_ball = false
			break
			
	if no_ball: # Added and Edited @ P11
		if is_playing:
			life -= 1
			if life <= 0:
				get_tree().change_scene("res://scene/GameOverView.tscn")
			else:
				update_hud_life()
				life_down_sound.play()
		else:
			is_playing = true
		# Clear powerup items
		for child in get_children(): # Added @ P10
			if child.is_in_group("PowerupItems"):
				child.queue_free()
		# Set Paddle and Balls as default
		is_multiple_on = false # Added @ P11
		is_laser_on = false # Added @ P11
		paddle.position = paddle_position
		paddle.scale = paddle_scale # Added @ P11
		add_new_ball() # Added @ P11
		#ball = load("res://scene/Ball.tscn").instance() # Removed @ P11
		#call_deferred("add_child", ball) # Removed @ P11
		#call_deferred("move_child", ball, 3) # Removed @ P11
		#ball.connect("tree_exited", self, "_on_Ball_tree_exited") # Removed @ P11


# Set life nodes shown and hidden as life variable
func update_hud_life():
	var count = 0
	for child in hud_rightbox.get_children():
		count += 1
		if count <= life:
			child.show()
		else:
			child.hide()


# Add a new ball
func add_new_ball(): # Added @ P11
	print("add_new_ball() called")
	var instance = ball.instance()
	instance.mode = 3 # Fixed the order @ P11
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 4)
	instance.connect("tree_exited", self, "_on_Ball_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
	if level.get_child_count() <= 0:
		set_next_level()
	else: # Added @ P11
		# Drop powerup item
		drop_powerup(brick_position) # Added @ P10


func drop_powerup(brick_position: Vector2): # Added @ P10
	if randf() <= drop_rate:
		var powerup_instance = powerup.instance()
		powerup_instance.position = brick_position
		call_deferred("add_child", powerup_instance)
		call_deferred("move_child", powerup_instance, 5) # Added @ P 11
		powerup_instance.connect("item_collided", self, "_on_Powerup_item_collided") 

# Action when powerup item collided
func _on_Powerup_item_collided(item): # Updated @ P11
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			expand_paddle()
		2: # MULTIPLE
			enable_multiple_balls()
		3: # LASER
			enable_laser()
		4: # LIFE
			add_life()

# slow balls
func slow_balls(): # Added @ P11
	for child in get_children():
		if child.is_in_group("Balls"):
			child.ball_speed = child.first_speed

# Stretch paddle
func expand_paddle(): # Added @ P11
	if paddle.scale <= paddle_scale:
		paddle.scale.x *= 2
		yield(get_tree().create_timer(10), "timeout")
		paddle.scale = paddle_scale

# enable powerup Multiple
func enable_multiple_balls(): # Added @ P11
	if not is_multiple_on:
		is_multiple_on = true
		yield(get_tree().create_timer(3), "timeout")
		is_multiple_on = false

# enable powerup Laser
func enable_laser(): # Added @ P11
	if not is_laser_on:
		is_laser_on = true
		yield(get_tree().create_timer(3), "timeout")
		is_laser_on = false

# fire laser beam
func fire_laser():
	var instance = laser.instance()
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 6)
	instance.position.x = paddle.position.x
	instance.position.y = paddle.position.y - 16

# Add a life if less than 5
func add_life(): # Added @ P11
	if life < MAX_LIFE:
		life += 1
		update_hud_life()

# set next level
func set_next_level():
	print("set_next_level() called")
	# Change status
	is_playing = false
	is_multiple_on = false # Added @ P11
	is_laser_on = false # Added @ P11
	
	# Clear left objects
	level.queue_free()
	for child in get_children():
		if child.is_in_group("Balls") or child.is_in_group("Lasers"): # 追加
			child.queue_free()
	
	# Increment level number
	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 # Removed @ P11
	#paddle.scale = paddle_scale # Removed @ P11
	#ball.position = ball_position # Removed @ P11
	#ball.mode = 3 # Removed @ P11
	# Set next Level node
	add_new_level() # Added @ P11
	#level = load("res://scene/Level" + str(level_num) + ".tscn").instance() # Removed @ P11
	#add_child(level) # Removed @ P11
	#move_child(level, 5) # Removed @ P11
	#for child in level.get_children(): # Removed @ P11
		#child.connect("tree_exited", self, "_on_Brick_tree_exited") # Removed @ P11
	# Pause game until NextScreen is hidden
	get_tree().paused = true

# Add new level
func add_new_level(): # Added @ P11
	level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
	add_child(level)
	move_child(level, 3) # Changed from 5 to 3 @ P11
	for child in level.get_children():
		child.connect("tree_exited", self, "_on_Brick_tree_exited", [child.global_position]) # Updated to add th 4th arg @ P11 


func _on_PauseScreen_visibility_changed():
	play_bgm.stream_paused = not play_bgm.stream_paused


おわりに

以上で Part 11 は完了だ。今回はパワーアップアイテムを取得した後の個々のパワーアップ機能を実装した。特に「Laser」の実装はかなりボリュームがあり、また全体を通しても、相当な長文になっている。無理に1日で終える必要はなく、適宜日にちを分けてチャレンジして欲しい。

Part 10、11 とパワーアップ機能の話であったが、次の Part 12 ではブロック崩しに BGM とサウンドエフェクトを追加する。