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」ノードを削除しよう。
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 # 変更
今シーンドックから削除したノードを示す変数level
とball
を削除した。さらに「Ball」ノードのプロパティposition
を示すball_position
の変数も削除した。
代わりに、preload
した「Ball.tscn」と「Powerup.tscn」の PackedScene それぞれの変数ball
、powerup
を新たに追加した。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_level
とadd_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_ball
をtrue
としてBool型の変数として定義しておく。get_children
メソッドで「Game」の子ノードをfor
ループで順番にチェックしていき、子ノードが「Ball」インスタンスに該当する場合は、メソッド内で新しく定義したno_ball
をfalse
にする(つまり画面上にボールが存在するという意味)。そのあとそのままbreak
でループから抜ける。そして、if
構文でno_ball
がtrue
(つまり「Ball」インスタンスがない)の場合はif
ブロック内のコードが実行される。
さらに変数is_playing
がtrue
の場合はライフを一つ減らす。ライフがもし0
だったらゲームオーバーの画面に遷移するし、まだライフが残っていれば、HUDを更新する。一方、is_playing
がfalse
の場合はゲームクリア直後で次のレベルの準備段階なので、ライフは減らさず、この時点でis_playing
をtrue
に戻しておくのみだ。
あとのno_ball
がtrue
だった場合のコードを少し編集した。まず、以下の処理をする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
に指定している。
- HUD
- Paddle
- Wall
- Level_(インスタンス)
- Ball(インスタンス)
- Powerup(インスタンス)
- PauseScreen
- 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 アイテムを実装することを想定して、複数のボールが画面上にある場合にコードを作りやすくするためだ。
次に「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_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)
パワーアップ「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」アイテムと衝突しても増えない
- ボールが落ちたり、ブロックをすべて消してレベルをクリアしたら通常の長さに戻る
パワーアップ「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_on
がfalse
だったら、という条件定義だ。これに当てはまる場合、以下の処理を順番に実施していく。
add_new_ball
メソッドで、新しいボールを1つだけパドルの上に生成するis_multiple_on
をtrue
にするyield
で3秒間のワンショットタイマーを作成し、タイムアウトまで次のコードを待機させる- タイムアウトしたら、再び変数
is_multiple_on
をfalse
に戻す
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_on
がtrue
であること - インプットマップに登録された「launch_ball」アクションに当てはまるキー入力がされたこと
これで、パドルが「Multiple」アイテムに衝突してから3秒間は「space」キーを押すたびに新しいボールを発射できるようになった。
プロジェクトを実行して動作確認
ではパワーアップ「Multiple」がうまく実装できたか確認してみよう。
- パワーアップアイテム「Multiple」がパドルに衝突したら、パドルの上に1つボールが追加された
- そこから3秒間、spaceキーを連打した分だけボールが連射された
- 3秒後に連射機能は無効になり、画面上に発射したボールだけが残った
- 一番最初のボールが落ちても、他のボールが残っているのでライフは減らない
もし3秒という時間がしっくりこない場合は、お好みで適宜調整していただければと思う。
パワーアップ「Laser」を実装する
ドロップしたパワーアップアイテム「Laser」と衝突後、一定時間、レーザービームを発射できるようにする。具体的な仕様は以下のようにする。
- 3秒間パドルからレーザーを打ち放題になる
- レーザーがブロックに当たるとレーザーとブロックの両方が消える
- レーザーが画面外に出た場合はレーザーのインスタンスが消去される
- ブロックをすべて消してそのレベルをクリアすると、残ったレーザーは消える(次のレベルに持ち越さない)
発射されるレーザーオブジェクトは「Game」シーンに複数現れては消えることになる。ということは雛形となる「Laser」シーンを先に作成し、「Game」シーンではそのインスタンスをノードとして追加したり削除したりすれば良い。
「Laser」シーンを作る
「Game」シーンにレーザーを登場させるためには、その雛形となるシーンを新たに作成する必要がある。そのインスタンスを「Game」シーンにノードとして追加していくパターンだ。
シーンを新規作成する
それではシーンを新しく作成していく。例によって、「シーン」メニュー>「新規シーン」から開始しよう。
まずは「Area2D」クラスのノードをルートノードとして追加する。この時点でシーンを保存しておこう。ファイル名はそのまま「Laser.tscn」で問題ない。
続いて子ノードを追加していく。
子ノードを追加する
「Laser」ノードに以下の3つのノードを追加しよう。
- Line2D
- CollisionShape2D
- VisibilityNotifier2D
追加したら、次はそれぞれのノードをインスペクタドックやノードドックで編集していく。
Laser ノードを新しいグループに追加する
まず、ルートノードの「Laser」については、プロパティはデフォルトのままで良いが、ノードグループを新たに作成して追加しておこう。ノードドック>グループから「Lasers」の名前でグループを作成して追加する。
これは、画面上に複数のレーザーが表示されている状況で、まとめてそれらに対して処理を施す場合にとても便利になる。これまでも「Ball」シーンや「Brick」シーンで利用してきた手法だ。
コリジョンレイヤーの設定
「Laser」ノード用のコリジョンレイヤーがまだないので、設定していく。まずは「プロジェクト」メニュー>「プロジェクト設定」>「一般」タブ>サイドバーから「2d physics」を開く。次に「Layser 6」に「Laser」と言う名前をつける。
次にシーンドックで「Laser」ノードを選択し、インスペクタで「Collision」プロパティを編集する。「Layer」がそのノードが属するレイヤー、「Mask」は衝突反応を有効にするレイヤーだ。
まずは「Layer」プロパティで「Laser」レイヤーを選択、続いて「Mask」プロパティはブロックだけ衝突して欲しいので「Brick」レイヤーのみを選択した。
Line2D ノードのプロパティを編集する
次にインスペクタドックで「Line2D」ノードのプロパティを編集する。
まず「Points」プロパティの値になっている「PoolVector2Array(size 0)」をクリックしよう。すると、「サイズ」を調整できるので 2 に変更してみると、「Line2D」の線にポイントが追加される。折線にする場合は、この「サイズ」を複数追加して、それぞれのポイントの座標を指定するわけだ。今回は短い直線でレーザービームを表現するため、ポイント 0 の座標を(0, 0)、ポイント 1 の座標を(0, -24)にする。すると(0, 0)から真っ直ぐ上に長さが - 24 ピクセルの直線ができるはずだ。
続いてプロパティ「Width」は 4 ピクセルを指定してやや細いレーザーにする。「Default Color」は「77bfff」でちょっと水色っぽい青を指定する。このあたりはお好みで調整して欲しい。
そして「Capping」カテゴリの「Joint Mode」プロパティについて。これは線のポイントがもっと複数ある場合に、ポイントとポイントをどのような形状でつなぐかを指定する。今回はポイントが 2 つしかないのでデフォルトの「Sharp」ままにしておく。「Begin Cap Mode」と「End Cap Mode」はどちらも「Round」にして、線の両端を丸い形状にしておこう。
CollisionShape2D ノードのプロパティを編集する
例によってインスペクタドックにて「Shape」プロパティを編集する。今回は「CapsuleShape2D」を選択しよう。続けて 2D ワークスペースで、ハンドルをドラッグして、さっき作った「Line2D」ノードと全く同じ形にしてきれいに重なるように調整しよう。調整するときはツールバーで「ピクセルスナップを使用」を有効にすると作業しやすくなる。
VisibilityNotifier2D ノードのプロパティを編集する
このゲーム開発では2回目の登場である「VisibilityNotifier2D」ノードは、ノードが画面外に出た時にそれを検知してシグナルを発信することができる。これを利用して、レーザーが画面外に出たらそのレーザーのノードを開放するのが狙いだ。
こちらのノードも「Rect」プロパティを他のノードのサイズに合わせる形で以下の通りに調整していく。
- x: -2
- y: -26
- w: 4
- h: 28
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_on
がfalse
の間はパワーアップ「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_on
がfalse
の場合のみ実行され、true
の場合は何もせずに終了する。ではif
構文がtrue
だった場合に実行される処理を見ていこう。
まずis_laser_on = true
のコードでis_laser_on
をtrue
に変更する。true
の間だけレーザーを好きなだけ発射できるように別のメソッドで実装するが、このメソッドではとにかくこの変数の値を変更してステータス管理のみを行う。yield
で3
秒間のタイマーをセットし、タイムアウトしたら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
やっていることはボールの追加と同様で、以下の通りである。
preload
しておいた「Laser.tscn」ファイルのインスタンスの作成する- 作成したインスタンスを
add_child
で「Game」ルートノードの子にする - 「Game」ルートノードの子ノード内の順番を
6
番目に変更する - インスタンスの
position
プロパティの x 座標を「Paddle」のそれと同じにする - インスタンスの
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」に割り当てられているのはキーボードの「上矢印」キーだ。キーを叩いただけレーザーが発射される。
ここまでの内容を少しまとめておこう。
- パワーアップアイテムがパドルと衝突すると
_on_Powerup_item_collided
メソッドが呼ばれる - パワーアップアイテムが「Laser」だった場合、
_on_Powerup_item_collided
メソッド内でenable_laser
メソッドが呼ばれる enable_laser
メソッド内で、変数is_laser_on
が3秒間だけtrue
になる- 変数
is_laser_on
がtrue
の間だけ上矢印キーが入力をされるたびに_process
メソッド内でfire_laser
メソッドが呼ばれる 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
メソッドを以下のように一時的に編集してからプロジェクトを実行しよう。
パワーアップ「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 の場合はそれ以上増えないこと
全体を通して
最後に全体を通して調整と確認をしておこう。
細かなバグの修正
この時点で確認できた細かなバグを修正する。
「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)に戻そう。
あとは念の為程度の修正だが、再度「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
の間くらいで適当に調整しておこう。
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 とサウンドエフェクトを追加する。