Part 12 の今回は、ブロック崩しにBGMとサウンドエフェクト(効果音)を追加していく。今回のチュートリアルにはいつもの GIF ではなく mp4 の動画を載せている。この記事の閲覧環境は様々だと思われるため、デフォルトでは音をミュートしている。動画のコントローラでミュート解除していただきつつ、確認いただければと思う。
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るブロック崩し
サウンドのリソースを用意する
ゲームを開発するにあたり、すべての分野のスキルを十分に持ち合わせている人はごく少数だろう。ゲーム会社など組織でゲームを開発する場合なら、企画、プログラム、グラフィック、サウンドなど分野ごとにチームを構成して作業を進めることがほとんどだ。
筆者は作曲ができないため、このチュートリアルでは、他の人が作ってくれている素材を使わせていただいたり、手軽にサウンドを生成できるアプリケーションを利用することで、サウンド素材を用意していく。もちろん自分で作曲できる人は、素材を一から自分で作っていただいても構わない。
必要なサウンドを洗い出す
ブロック崩しで必要なサウンドをリストアップしておこう。
まずはサウンドエフェクトから。サウンドエフェクトは 12 種類用意する。
- スタート画面やポーズ画面でキーを押した時のサウンド
- ボールを発射する時のサウンド
- ボールがブロックに衝突した時のサウンド
- ボールがパドルに衝突した時のサウンド
- ボールが壁に衝突した時のサウンド
- レーザーを発射する時のサウンド
- すべてのボールが画面から消えてライフが一つ減った時のサウンド
- パワーアップ「Slow」が有効になってボールの速度が落ちた時のサウンド
- パワーアップ「Expand」が有効になってパドルが伸びた時のサウンド
- パワーアップ「Multiple」が有効になった時のサウンド
- パワーアップ「Laser」が有効になった時のサウンド
- パワーアップ「Life」が有効になってライフが一つ増えた時のサウンド
次に BGM だ。今回は以下の 3 種類を用意する。
- スタート画面のBGM
- プレイ中のBGM
- ゲームオーバー画面のBGM
サウンドエフェクトを作る
今回サウンドエフェクトを作成するにあたり「Bfxr」というアプリケーションを利用した。アプリケーションは以下のリンク先のサイトからダウンロード可能だ。
Memo:
Bfxr のサイト
このアプリケーションは非常に優秀だ。レトロゲームでありそうなサウンドエフェクトを手軽に生成してくれる。
アプリケーションを立ち上げると、UI上左上にカテゴリ(例えば「Pickup/Coin」や「Laser/Shoot」など)ごとのボタンがあり、それを押せばランダムでサウンドを作り出してくれる。もちろん気に入ったサウンドができたら、右下の「Export Wav」ボタンで .wav 形式のファイルとして出力もできる。
今回サンプルとして作成したサウンドエフェクトの素材を Dropbox の共有フォルダに保存しているので、自分で作る工程はスキップしたい場合は、以下のリンク先から「sounds」フォルダごとダウンロードしてほしい。
Memo:
サウンドエフェクトのファイルを含む、このブロック崩しのチュートリアルの素材は以下のリンク先にあります。サウンドエフェクトは「sounds」フォルダにまとまっていますので、そちらをダウンロードしてください。
Dropbox: breakout 共有フォルダ
BGM 用に音楽素材を入手する
今回、BGM用の音楽素材として、有名な「魔王魂」さんから 3 種類の BGM 素材を使わせていただくことにした。素材をそのまま配布することはできないので、今回選んだ素材のダウンロード元のリンクをこちらに貼らせていただく。
Memo:
今回使用させていただいた 3 つの楽曲のリンクは以下です。
スタート画面BGM
魔王魂 - サイバー22
プレイ中BGM
魔王魂 - サイバー21
ゲームオーバー画面BGM
魔王魂 - ヒーリング11 雨音無し他の楽曲も確認されたい場合は、BGM ページから探してみてください。
魔王魂 - BGM
ダウンロードした楽曲は、それぞれ何に使う楽曲かわかりやすくするためにファイル名を変更しておこう。
- bgm_start.ogg
- bgm_play.ogg
- bgm_gameover.ogg
続けて、サウンドエフェクト素材ファイルと BGM 素材ファイルをまとめて一つのフォルダに入れておこう。Dropbox から「sounds」フォルダごとダウンロードした場合は、そのフォルダに BGM ファイルを追加するだけで良い。
ゲームにサウンドを実装する
素材が用意できたので、次はそれらの素材をプロジェクトに追加して、それぞれのサウンドが適切なタイミングで流れるように実装していこう。
ゲームにサウンド素材を追加する
まずは用意した素材を Godot エンジンのファイルシステムに追加しよう。しばらくやっていなかったが手順は覚えているだろうか。
先にコンピュータ上で(Mac なら Finder、Windows なら Explorer で)すべての BGM とサウンドエフェクトの素材ファイルを 1 つのフォルダにまとめておこう。
一つにまとめたフォルダを Godot エンジンのファイルシステムドックめがけてドラッグ&ドロップするだけだ。
このような感じで、ファイルシステムに反映されればプロジェクトへの素材の追加は完了だ。
ところで、このあと Godot でサウンドを実装するには、大まかに次の3つの手順を行う必要がある。
- サウンドが直接関係するノードに「AudioStreamPlayer」クラスの子ノードを追加する
- 追加した「AudioStreamPlayer」クラスのノードにサウンド素材を追加する
- スクリプトで「AudioStreamPlayer」クラスの
play
メソッドによりサウンドが鳴るタイミングを制御する
ということで、このあとの作業は上記手順を繰り返して、それぞれのサウンドを実装していくことになる。では順番にやっていこう。
サウンドエフェクトを実装する
まずはサウンドエフェクトのほうからやっていこう。
スタート画面でキーを押した時のサウンド
まずはスタート画面から更新する。
「GameStartView」シーンを開いて、ルートノード「GameStartView」に「AudioStreamPlayer」クラスのノードを追加して、名前を「KeySound」とする。
シーンドックで「KeySound」ノードを選択した状態で、インスペクタドックにてプロパティ「Stream」にサウンドエフェクト素材を追加する。ここで使用するファイルは「res://sounds/key_pressed.wav」だ。このファイルを、ファイルシステムドックからインスペクタの「Stream」プロパティめがけてドラッグ&ドロップしよう。
次にスクリプトを編集して、追加したサウンドエフェクトが適切なタイミングで鳴るようにしていく。「GameStartView.gd」ファイルを開いたら、コードを以下のように更新しよう。
extends Control
onready var sound = $KeySound # 追加
func _input(event):
if event is InputEventKey and event.is_pressed(): # 更新
sound.play() # 追加
yield(sound, "finished") # 追加
get_tree().change_scene("res://scene/Game.tscn")
まず、先ほど追加した「KeySound」ノードの参照を変数sound
を定義して渡しておく。
_input
メソッドも編集する。キーボードの(いずれかの)キーが押されたら、という意味になるようにif
構文の条件式にand event.is_pressed()
を追加して、play
メソッドで効果音を鳴らすようにした。ちなみにevent.is_pressed()
の条件がなかったら、キーを押した時と離した時の2回鳴ってしまう。
yield
で、「KeySound」ノードのサウンドの再生が終わったら発信されるfinished
シグナルを待つようにした。これがないと、サウンドエフェクトが最後まで鳴りきる前に「Game」シーンに切り替わってしまうからだ。
ではシーンを実行してスタート画面でキーを押した時にサウンドエフェクトが正しく鳴るか確認しよう。
Memo:
このチュートリアルのすべての動画の音は、デフォルトで ミュート されています。音を確認するにはミュートを解除してください。
ゲームオーバー画面でキーを押した時のサウンド
ほとんど全く同じ手順で、ゲームオーバー画面もやっていこう。「GameOverView」シーンを開いて、ルートノード「GameOverView」に「AudioStreamPlayer」クラスのノードを追加して、名前はさっきと同じく「KeySound」にしておく。
「KeySound」ノードの「Stream」プロパティに素材ファイル「res://sounds/key_pressed.wav」を追加する。
続いてスクリプト「GameOverView.gd」を編集するが、こちらも「GameStartView.gd」の時とほとんど同じだ。
extends Control
onready var sound = $KeySound
func _input(event):
if event is InputEventKey:
print("Input at Game Over: ", event.as_text())
if event.is_action_released("Quit"):
sound.play()
yield(sound, "finished")
get_tree().quit()
elif event.is_action_released("ui_accept"):
sound.play()
yield(sound, "finished")
get_tree().change_scene("res://scene/GameStartView.tscn")
ではシーンを実行して確認しよう。
*動画ではゲームオーバー画面で Enter キーを押しています。
ポーズ画面でキーを押した時のサウンド
ポーズ画面は個別のシーンとしては作っていないので、ポーズ画面がノードとして存在する「Game.tscn」シーンを開こう。
「PauseScreen」ノードに「AudioStreamPlayer」クラスのノードを追加して、名前を「PauseKeySound」に変更する。
スタート画面、ゲームオーバー画面と同様に「PauseKeySound」ノードの「Stream」プロパティにサウンドエフェクト素材「res://sounds/key_pressed.wav」を追加する。
「PauseScreen」ノードにアタッチしているスクリプト「PauseScreen.gd」を編集する。編集内容はスタート画面、ゲームーオーバー画面と似たようなものだ。
extends Control
onready var sound = $PauseKeySound # 追加
func _ready():
hide()
func _input(event):
if event.is_action_released("Pause"):
sound.play() # 追加
visible = not visible
get_tree().paused = not get_tree().paused
elif event.is_action_released("Quit") and visible == true: # 修正
sound.play() # 追加
yield(sound, "finished") # 追加
get_tree().paused = false
get_tree().change_scene("res://scene/GameStartView.tscn")
ただし、インプットマップの「Pause」アクション実行時(つまり P キーを押した場合)にはyield
でサウンド再生が終わるのを待つ処理は不要だ。
一方、ポーズ画面表示中に「Quit」アクション実行時(つまり Q キーを押した場合)は、ゲームを終了する前にサウンドエフェクトが鳴りきるようにyield
を使っている。
ちなみに「# 修正」のコメントがあるelif
ブロックの条件式を修正した。元々、ゲームプレイ中、ポーズ画面でなくても Q キーでゲーム終了できてしまっていたが、この操作は重大であるため、いつでもできる仕様は避けた方が良い。このことからand visible == true
を条件式に追加し、ポーズ画面でないと Q キーを受け付けない仕様にした。
ではプロジェクトを実行して確認しよう。
*動画ではプレイ画面で P キーを押してポーズ/ポーズ解除をしています。
ボールを発射する時のサウンド
次はボールを発射した時のサウンドエフェクトを追加する。
「Ball.tscn」のシーンを開いて、ルートノード「Ball」に「AudioStreamPlayer」クラスのノードを追加したら、名前を「LaunchSound」に変更しよう。
「LaunchSound」ノードの「Stream」プロパティに素材「res://sounds/ball_Shoot.wav」を追加する。
次に「Ball.gd」スクリプトを編集していく。
まずは、以下のように「LaunchSound」ノードの参照をlaunch_sound
変数で定義しよう。
onready var launch_sound = $LaunchSound
続いて、_process
メソッド内の「# 追加」とコメントしている行のコードを追加した。インプットマップの「launch_ball」アクションにあたる「space」キーを押したら、ボールがパドルから飛んでいくと同時にサウンドも鳴るようにした。
func _process(delta):
if mode == 3:
position.x = paddle.position.x
if Input.is_action_just_pressed("launch_ball"):
mode = 2
apply_impulse(Vector2.ZERO, velocity)
launch_sound.play() # 追加
プロジェクトを実行して確認しよう。
ボールがブロックに衝突した時のサウンド
「Ball.tscn」シーンにブロックに衝突した時のサウンドを追加しても良いのだが、今回は「Brick.tscn」シーンの方に追加することにした。イメージ的に音を出すのはボールではなくブロックである、と考えればしっくりくるだろう。
では「Brick.tscn」を開き、「Brick」ルートノードに「AudioStreamPlayer」クラスのノードを追加して、名前を「CollideSound」としておこう。
「CollideSound」ノードの「Stream」プロパティには、素材のうち「res://sounds/brick_collided.wav」を追加する。
しかし、スクリプトは「Ball.gd」の方に編集を加える。
func _on_Ball_body_entered(body):
ball_speed += speed_up
direction = linear_velocity.normalized()
velocity = direction * min(ball_speed, MAX_SPEED)
if body.is_in_group("Bricks"):
var collide_sound = body.get_node("CollideSound") # 追加
collide_sound.play() # 追加
var animation = body.get_node("AnimationPlayer")
animation.play("collided")
yield(animation, "animation_finished")
body.queue_free()
#(後略)
_on_Ball_body_entered
メソッド内の一つ目のif
構文の冒頭にコードを 2 行追加した。これで、ボールがブロックに当たったらサウンドエフェクトが再生されるようになる。
ところで、今回yield
のコードでサウンド再生の終了を待つ処理を入れていない。これはサウンド再生のメソッドplay
を実行した直後に「AnimationPlayer」ノードのアニメーション「collided」の再生が始まるが、このアニメーションが終了するまでの時間が、サウンドエフェクトが鳴り終わるまでの時間より確実に長いからだ。しかも、音の再生が終わってからブロックが消える時のアニメーションが再生されると、若干の違和感が生じるが、これを回避する意味もある。
では、プロジェクトを実行して確認しておく。
ボールがパドルに衝突した時のサウンド
先ほどと同じ理屈で、ボールがパドルに衝突した時のサウンドは、「Paddle」ノードに追加する。
では「Paddle」は個別のシーンではないので、「Game.tscn」を開いて「Paddle」ノードに「AudioStreamPlayer」クラスのノードを追加し、名前を「CollideSound」に変更する。
「CollideSound」の「Stream」プロパティに素材ファイル「res://sounds/paddle_collided.wav」を追加する。
そしてスクリプトを編集していくが、このサウンドエフェクトも「Ball.gd」スクリプトにて制御する。
func _on_Ball_body_entered(body):
ball_speed += speed_up
direction = linear_velocity.normalized()
velocity = direction * min(ball_speed, MAX_SPEED)
if body.is_in_group("Bricks"):
var collide_sound = body.get_node("CollideSound")
collide_sound.play()
var animation = body.get_node("AnimationPlayer")
animation.play("collided")
yield(animation, "animation_finished")
body.queue_free()
elif body.get_name() == "Paddle": # elifに変更
var collide_sound = body.get_node("CollideSound") # 追加
collide_sound.play() # 追加
var animation = body.get_node("AnimationPlayer")
if animation.is_playing():
animation.stop()
animation.play("hit")
direction = (position - body.position).normalized()
velocity = direction * min(ball_speed, MAX_SPEED)
#(後略)
先ほどボールがブロックに衝突した時のサウンドエフェクトで編集したばかりの_on_Ball_body_entered
メソッドだが、ボールがパドルに衝突した場合のサウンドエフェクトもこちらを編集していく。
この機にバラけていたif
構文を一つにまとめて、二つ目のif
ブロックをelif
に変更した。そして、そのelif
ブロックにコードを追加した。
elif
ブロックの冒頭に、さっきのブロックに衝突した時のサウンドの処理と全く同じコードを2行追加した。
では、プロジェクトを実行して確認してみよう。
ボールが壁に衝突した時のサウンド
いよいよボールが xxx に衝突した時のサウンドシリーズがこれで最後になる。だがやることは同じだ。
「Wall」ノードも個別のシーンはないので、「Game.tscn」を開いて作業していく。
「Wall」ノードを選択して、「AudioStreamPlayer」を追加し、名前をブロックやパドルの時と同様に「CollideSound」としておこう。
「CollideSound」のプロパティ「Stream」には、素材「res://sounds/wall_collided.wav」を追加する。
こちらも編集するのは「Ball.gd」スクリプトだ。
func _on_Ball_body_entered(body):
ball_speed += speed_up
direction = linear_velocity.normalized()
velocity = direction * min(ball_speed, MAX_SPEED)
if body.is_in_group("Bricks"):
var collide_sound = body.get_node("CollideSound")
collide_sound.play()
var animation = body.get_node("AnimationPlayer")
animation.play("collided")
yield(animation, "animation_finished")
body.queue_free()
elif body.get_name() == "Paddle":
var collide_sound = body.get_node("CollideSound")
collide_sound.play()
var animation = body.get_node("AnimationPlayer")
if animation.is_playing():
animation.stop()
animation.play("hit")
direction = (position - body.position).normalized()
velocity = direction * min(ball_speed, MAX_SPEED)
elif body.get_name() == "Wall": # 追加
var collide_sound = body.get_node("CollideSound")
collide_sound.play()
linear_velocity = velocity
ここでもまた_on_Ball_body_entered
メソッドを更新した。if / elif
構文にもう一つelif
を追加した。
このelif
は、衝突したオブジェクトが「Wall」ノードだったら、という条件式になっている。条件の通り、ボールが「Wall」ノードに衝突した場合に、サウンドエフェクトを再生するコードになっている。
ではプロジェクトを実行して確認しよう。
レーザーを発射する時のサウンド
レーザーを発射する時のサウンドエフェクトを追加しよう。レーザーは「Laser.tscn」シーンを都度、「Game.tscn」シーンでインスタンス化し、ノードとして追加している。ノードとして追加された瞬間から画面を上方向に飛んでいく。サウンドエフェクトも、インスタンス化された瞬間から再生されるようにすれば良いというわけだ。
ではさっそく「Laser.tscn」を開こう。「Laser」ノードに新規で「AudioStreamPlayer」クラスのノードを追加して、名前を「ShootSound」に変更しておく。
「ShootSound」ノードの「Stream」プロパティには素材「res://sounds/laser_shoot.wav」を追加しよう。
続いて「Laser.gd」スクリプトを編集する。
extends Area2D
export (float) var laser_speed = 500
onready var line = $Line2D
onready var sound = $ShootSound # 追加
func _ready(): # 追加
sound.play()
#(後略)
例によって、「ShootSound」ノードへの参照を変数sound
で定義した。そして、_ready
メソッドにplay
メソッドを記述し、インスタンスが生成されたらサウンドエフェクトが再生されるようにした。
シーンを実行して確認しよう。
次に、プロジェクトを実行して、ゲーム中での挙動を確認しよう。ただし、すぐにパワーアップアイテム「Laser」が落ちてくれるように、一時的に以下の編集をしておく。
- 「Powerup.gd」を編集して「Laser」のドロップ率を上げる。
extends Area2D
#(中略)
func add_sprite_frames():
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
for item in item_list:
# add to the head
sprite.frames.add_frame("drop", item, 0)
#(後略)
- インスペクタドックにて、「Game .tscn」シーンの「Game」ルートノードの「Drop Rate」プロパティを
1
に変更する。これでブロックを消した時に 100 % パワーアップアイテムが落ちる。
ではプロジェクトを実行して確認しよう。確認できたら、さっきの一時的な変更は戻しておこう。
すべてのボールが画面から消えてライフが一つ減った時のサウンド
ライフが一つ減る時の処理は「Game.gd」スクリプトにあるので、まず「AudioStreamPlayer」クラスのノードを「Game」ルートノードに追加する。名前は「LifeDownSound」とする。
「LifeDwonSound」ノードの「Stream」プロパティに素材「res://sounds/life_down.wav」を追加する。今回はもう一つプロパティを編集する。「Volume Db」プロパティだ。デシベルという音量の単位はなんとなくでも聞いたことがあるだろう。ここで使用する素材のサウンドが少し音量が小さくて聞き取りにくいため、「Volume Db」プロパティを10
に設定して、音量を少し大きくした。
ではスクリプト「Game.gd」を編集していこう。
#(前略)
onready var life_down_sound = $LifeDownSound # 追加
#(中略)
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()
life_down_sound.play() # 追加
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
paddle.scale = paddle_scale
add_new_ball()
#(後略)
新しく変数life_down_time
を定義した。これは「LifeDownSound」ノード参照をしている。
ライフが減る時の処理を行う_on_Ball_tree_exited
メソッドを編集した。このメソッドはボールが画面から消えた時にtree_exited
シグナルによって呼ばれる。ボールが全て画面から無くなった時にゲームプレイ中で、且つライフが0
ではなかったら、play
メソッドにてサウンドを再生するようにした。
ではプロジェクトを実行して確認しよう。
パドルとパワーアップアイテム衝突時のサウンド用に AudioStreamPlayer を一気に追加する
ここからはパドルがドロップしたパワーアップアイテムに衝突して、そのパワーアップが有効になった時のサウンドエフェクトを順番に追加していく。すべて「Game」ルートノードに「AudioStreamPlayer」クラスのノードを追加する必要があるので、ここは以下の手順で一気にやってしまおう。
- シーンドックで「Game」ノードを選択したら、cmd + A のショートカットキー操作で「AudioStreamPlayer」を一つ追加する
- 「AudioStreamPlayer」ノードを選択したまま、cmd + D のショートカットキー操作を 4 回繰り返して、ノードを 4 つ複製する。
今、シーンドックはこのようになっているはずだ。
続いて以下のように、複数用意した「AudioStreamPlayer」ノードの名前をそれぞれ変更して、対応するサウンド素材を「Stream」プロパティに追加しよう。
- ノード名:SlowCollideSound / 素材:res://sounds/powerup_slow.wav
- ノード名:ExpandCollideSound / 素材:res://sounds/powerup_expand.wav
- ノード名:MultipleCollideSound / 素材:res://sounds/powerup_multiple.wav
- ノード名:LaserCollideSound / 素材:res://sounds/powerup_laser.wav
- ノード名:LifeCollideSound / 素材:res://sounds/powerup_life.wav
ここまでできたら、次は「Game.gd」スクリプトを編集していこう。
まずは、それぞれの「AudioStreamPlayer」クラスのノードを参照する変数を定義しておこう。これらの変数をそれぞれのパワーアップが発動する時に呼ばれる各メソッドで利用していく。
onready var slow_collide_sound = $SlowCollideSound
onready var expand_collide_sound = $ExpandCollideSound
onready var multiple_collide_sound = $MultipleCollideSound
onready var laser_collide_sound = $LaserCollideSound
onready var life_collide_sound = $LifeCollideSound
では、ここからは個別のメソッドを編集していく。
パワーアップ「Slow」が有効になってボールの速度が落ちた時のサウンド
まずは「Slow」から着手する。
func slow_balls():
slow_collide_sound.play() # 追加
for child in get_children():
if child.is_in_group("Balls"):
child.ball_speed = child.first_speed
slow_balls
メソッドの冒頭に「SlowCollideSound」ノードのplay
メソッドを挿入した。
パワーアップ「Expand」が有効になってパドルが伸びた時のサウンド
次に「Expand」についてやっていこう。
func expand_paddle():
expand_collide_sound.play() # 追加
if paddle.scale <= paddle_scale:
paddle.scale.x *= 2
yield(get_tree().create_timer(10), "timeout")
paddle.scale = paddle_scale
expand_paddle
メソッドの冒頭に「ExpandCollideSound」ノードのplay
メソッドを挿入した。パドルのサイズが伸びる時だけサウンドが鳴る方が良いだろうか、と迷うところではあるが、今回はとにかくドロップしたパワーアップアイテムと衝突すればサウンドエフェクトが再生される仕様にした。もし、パドルが伸びる時だけサウンドを鳴らしたい場合は、if
ブロックの冒頭に挿入すれば良い。
パワーアップ「Multiple」が有効になった時のサウンド
続いて「Multiple」だ。
func enable_multiple_balls():
multiple_collide_sound.play() # 追加
if not is_multiple_on:
is_multiple_on = true
yield(get_tree().create_timer(3), "timeout")
is_multiple_on = false
enable_multiple_balls
メソッドの冒頭に「MultipleCollideSound」ノードのplay
メソッドを追加した。こちらもすでに「Multiple」が有効な場合は音を鳴らさないような仕様にしたい場合は、if
ブロック内の冒頭にplay
メソッドを追加すれば良い。
パワーアップ「Laser」が有効になった時のサウンド
作業パターンが同じで単調だが、「Laser」についても同様にやっていこう。
func enable_laser():
laser_collide_sound.play() # 追加
if not is_laser_on:
is_laser_on = true
yield(get_tree().create_timer(3), "timeout")
is_laser_on = false
enable_laser
メソッドの冒頭に「LaserCollideSound」ノードのplay
メソッドを追加した。すでに「Laser」が有効な場合は音を鳴らさない仕様がお好みの場合は、play
メソッドをif
ブロック冒頭に追加しよう。
パワーアップ「Life」が有効になってライフが一つ増えた時のサウンド
サウンドエフェクト最後の項目だが、やることは同じだ。
func add_life():
life_collide_sound.play() # 追加
if life < MAX_LIFE:
life += 1
update_hud_life()
add_life
メソッドの冒頭に「LifeCollideSound」ノードのplay
メソッドを追加した。ライフが5の場合はそれ以上ライフが増えないので、その場合にサウンドを鳴らしたくない場合はif
ブロックの冒頭にplay
メソッドを追加するようにしてほしい。
プロジェクトを実行してパワーアップアイテム衝突時のサウンドエフェクトを確認する
プロジェクトを実行する前に、「Game」ノードの「Drop Rate」プロパティを1
に変更して、パワーアップアイテムのドロップ率を 100 % にしておこう。
ここまでで、サウンドエフェクトの追加が一通り終わった。似たような作業をひたすら繰り返したので、手順には慣れていただけたことと思う。
次はいよいよ BGM を追加していく。
BGM を実装する
BGM はサウンドエフェクトのような短い音ではないので、play
メソッドで再生して放っておくわけにはいかない。状況に応じて、適切なタイミングで音楽を停止する必要がある。それを念頭に、作業を進めていこう。
スタート画面に BGM を実装する
まずはスタート画面の BGM から実装していく。
まずは例によって、「GameStartView.tscn」シーンを開き、「GameStartView」ルートノードに「AudioStreamPlayer」クラスのノードを追加する。名前を「StartBGM」としておこう。
「StartBGM」ノードの「Stream」プロパティには素材「res://sounds/bgm_start.ogg」を追加しよう。
音量がやや大きいので「Volume Db」プロパティを-5
に変更し、ゲーム開始時から自動的に再生させたいので「Autoplay」プロパティを「オン」する。
一度シーンを実行して、音量と自動再生だけ確認しておこう。
シーンが変われば自動的にそのシーンにある「AudioStreamPlayer」クラスのplay
メソッドも停止されるので、スクリプトの編集は不要だ。
プレイ画面に BGM を実装する
ではプレイ画面中のBGMを実装していこう。
「Game.tscn」シーンに切り替えて、「Game」ルートノードに新たに「AudioStreamPlayer」を追加しよう。併せて、名前を「PlayBGM」に変更しておこう。
続けて「PlayBGM」ノードの「Stream」プロパティに素材「res://sounds/bgm_play.ogg」を追加する。
これでシーンを実行してみるとわかるが、「NextScreen」ノードの青い画面が表示されている間は、このノード以外はプロセスを停止しているので、音楽が再生されない。スタート画面からプレイ画面に切り替わった時にこの無音状態になるのは少し違和感がある。
そこで「PlayBGM」ノードの「Pause Mode」プロパティを「Process」に変更する。これで、親ノードのの「Pause Mode」の値に関係なく、常にプロセスは生きた状態になる。
これでシーンを実行してみると「NextScreen」表示中でも BGM が流れている状態になるので、無音の違和感は解消される。
次にポーズ画面になったら音楽も一時停止するように更新していく。
「PauseScreen」ノードにはvisibility_changed
というシグナルがある。これを利用して、「Game.gd」スクリプト上で、BGMの一時停止/再開を制御していく。
まずはシーンドックで「PauseScreen」を選択した状態で、ノードドック>シグナルタブを開き、visibility_changed
シグナルを「Game.gd」スクリプトに接続する。接続先のメソッドの名前は_on_PauseScreen_visibility_changed
のままで良い。
次に「Game.gd」スクリプトに変数play_bgm
を追加して「PlayBGM」ノードへの参照を定義する。
onready var play_bgm = $PlayBGM
続けて、先ほど追加した_on_PauseScreen_visibility_changed
メソッドの中身を記述する。
func _on_PauseScreen_visibility_changed():
play_bgm.stream_paused = not play_bgm.stream_paused
「PlayBGM」ノードのstream_paused
プロパティがtrue
の場合はサウンドが一時停止し、false
で再開される。
ゲームプレイを開始した時点では「PauseScreen」ノードのvisibility
はfalse
で、「PlayBGM」ノードのstream_paused
プロパティもfalse
だ。「PauseScreen」ノードのvisibility
プロパティが変更された時にvisibility_changed
シグナルが発信され、_on_PauseScreen_visibility_changed
メソッドが実行され、「PlayBGM」ノードのstream_paused
プロパティもまた変更される(現在false
ならtrue
に、true
ならfalse
になる)。
では、シーンを実行して挙動を確認しておこう。
これでプレイ画面の BGM 実装は完了だ。次はゲームオーバー画面に BGM を追加していこう。
ゲームオーバー画面に BGM を実装する
それでは「GameOverView.tscn」シーンに手を加えていこう。まずは「GameOverView」ルートノードに「AudioStreamPlayer」クラスのノードを追加し、名前を「GameOverBGM」に変更しよう。
「GameOverBGM」ノードのプロパティを以下の手順で変更していく。
- 「Stream」プロパティに素材「res://sounds/bgm_gameover.ogg」を追加する
- やや音量を抑えるために「Volume Db」プロパティの値を
-10
に変更する - よりダークな印象にするため「Pitch Scale」プロパティを
0.8
に変更して、音程とテンポを落とす - 「Autoplay」プロパティをオンにして、ゲームオーバー画面に切り替わり次第 BGM が再生されるようにする
なお、ここではスクリプトを編集する必要はない。ではシーンを実行して確認しよう。
全体を通してにサウンドを確認する
では最後に、プロジェクトを実行して、プロジェクト全体のサウンドのバランスなどを確認しておこう。パワーアップアイテムのドロップ率は2.0
に戻しておく。
概ね問題なさそうなので、今回のサウンドの追加作業はこれにて終了だ。
おわりに
以上で Part 12 は完了だ。今回は BGM やサウンドエフェクトを追加するという内容だった。ほとんど同じ作業だったのですぐに慣れていただけたのではないだろうか。
次の Part 13 では HUD にハイスコアの要素を追加し、達成したスコアやレベルのデータを保存する機能、またそれを読み込む機能を実装する。