Part 12 の今回は、ブロック崩しにBGMとサウンドエフェクト(効果音)を追加していく。今回のチュートリアルにはいつもの GIF ではなく mp4 の動画を載せている。この記事の閲覧環境は様々だと思われるため、デフォルトでは音をミュートしている。動画のコントローラでミュート解除していただきつつ、確認いただければと思う。


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


サウンドのリソースを用意する

ゲームを開発するにあたり、すべての分野のスキルを十分に持ち合わせている人はごく少数だろう。ゲーム会社など組織でゲームを開発する場合なら、企画、プログラム、グラフィック、サウンドなど分野ごとにチームを構成して作業を進めることがほとんどだ。

筆者は作曲ができないため、このチュートリアルでは、他の人が作ってくれている素材を使わせていただいたり、手軽にサウンドを生成できるアプリケーションを利用することで、サウンド素材を用意していく。もちろん自分で作曲できる人は、素材を一から自分で作っていただいても構わない。


必要なサウンドを洗い出す

ブロック崩しで必要なサウンドをリストアップしておこう。

まずはサウンドエフェクトから。サウンドエフェクトは 12 種類用意する。

  • スタート画面やポーズ画面でキーを押した時のサウンド
  • ボールを発射する時のサウンド
  • ボールがブロックに衝突した時のサウンド
  • ボールがパドルに衝突した時のサウンド
  • ボールが壁に衝突した時のサウンド
  • レーザーを発射する時のサウンド
  • すべてのボールが画面から消えてライフが一つ減った時のサウンド
  • パワーアップ「Slow」が有効になってボールの速度が落ちた時のサウンド
  • パワーアップ「Expand」が有効になってパドルが伸びた時のサウンド
  • パワーアップ「Multiple」が有効になった時のサウンド
  • パワーアップ「Laser」が有効になった時のサウンド
  • パワーアップ「Life」が有効になってライフが一つ増えた時のサウンド

次に BGM だ。今回は以下の 3 種類を用意する。

  • スタート画面のBGM
  • プレイ中のBGM
  • ゲームオーバー画面のBGM

サウンドエフェクトを作る

今回サウンドエフェクトを作成するにあたり「Bfxr」というアプリケーションを利用した。アプリケーションは以下のリンク先のサイトからダウンロード可能だ。

Memo:
Bfxr のサイト

BfxrのUI

このアプリケーションは非常に優秀だ。レトロゲームでありそうなサウンドエフェクトを手軽に生成してくれる。

アプリケーションを立ち上げると、UI上左上にカテゴリ(例えば「Pickup/Coin」や「Laser/Shoot」など)ごとのボタンがあり、それを押せばランダムでサウンドを作り出してくれる。もちろん気に入ったサウンドができたら、右下の「Export Wav」ボタンで .wav 形式のファイルとして出力もできる。
BfxrのUI

今回サンプルとして作成したサウンドエフェクトの素材を 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ファイル名変更

続けて、サウンドエフェクト素材ファイルと BGM 素材ファイルをまとめて一つのフォルダに入れておこう。Dropbox から「sounds」フォルダごとダウンロードした場合は、そのフォルダに BGM ファイルを追加するだけで良い。

サウンドエフェクトとBGMを1つのフォルダにまとめる



ゲームにサウンドを実装する

素材が用意できたので、次はそれらの素材をプロジェクトに追加して、それぞれのサウンドが適切なタイミングで流れるように実装していこう。



ゲームにサウンド素材を追加する

まずは用意した素材を Godot エンジンのファイルシステムに追加しよう。しばらくやっていなかったが手順は覚えているだろうか。

先にコンピュータ上で(Mac なら Finder、Windows なら Explorer で)すべての BGM とサウンドエフェクトの素材ファイルを 1 つのフォルダにまとめておこう。
サウンド素材を一つのフォルダに

一つにまとめたフォルダを Godot エンジンのファイルシステムドックめがけてドラッグ&ドロップするだけだ。
素材をフォルダごとファイルシステムにD&D

このような感じで、ファイルシステムに反映されればプロジェクトへの素材の追加は完了だ。
素材が正しく追加された時のファイルシステム

ところで、このあと Godot でサウンドを実装するには、大まかに次の3つの手順を行う必要がある。

  1. サウンドが直接関係するノードに「AudioStreamPlayer」クラスの子ノードを追加する
  2. 追加した「AudioStreamPlayer」クラスのノードにサウンド素材を追加する
  3. スクリプトで「AudioStreamPlayer」クラスのplayメソッドによりサウンドが鳴るタイミングを制御する

ということで、このあとの作業は上記手順を繰り返して、それぞれのサウンドを実装していくことになる。では順番にやっていこう。



サウンドエフェクトを実装する

まずはサウンドエフェクトのほうからやっていこう。


スタート画面でキーを押した時のサウンド

まずはスタート画面から更新する。

「GameStartView」シーンを開いて、ルートノード「GameStartView」に「AudioStreamPlayer」クラスのノードを追加して、名前を「KeySound」とする。
KeySoundノードを追加

シーンドックで「KeySound」ノードを選択した状態で、インスペクタドックにてプロパティ「Stream」にサウンドエフェクト素材を追加する。ここで使用するファイルは「res://sounds/key_pressed.wav」だ。このファイルを、ファイルシステムドックからインスペクタの「Stream」プロパティめがけてドラッグ&ドロップしよう。
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」にしておく。

GameOverViewにAudioStreamPlayer追加

「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」に変更する。

GameOverViewにAudioStreamPlayer追加

スタート画面、ゲームオーバー画面と同様に「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」に変更しよう。

BallにAudioStreamPlayer追加

「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」としておこう。
BallにAudioStreamPlayer追加

「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」に変更する。

BallにAudioStreamPlayer追加

「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」としておこう。

BallにAudioStreamPlayer追加

「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」に変更しておく。

LaserにAudioStreamPlayer追加

「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ノードDrop Rateを一時的に編集


ではプロジェクトを実行して確認しよう。確認できたら、さっきの一時的な変更は戻しておこう。



すべてのボールが画面から消えてライフが一つ減った時のサウンド

ライフが一つ減る時の処理は「Game.gd」スクリプトにあるので、まず「AudioStreamPlayer」クラスのノードを「Game」ルートノードに追加する。名前は「LifeDownSound」とする。

AudioStreamPlayerを追加してLifeDownSoundと命名

「LifeDwonSound」ノードの「Stream」プロパティに素材「res://sounds/life_down.wav」を追加する。今回はもう一つプロパティを編集する。「Volume Db」プロパティだ。デシベルという音量の単位はなんとなくでも聞いたことがあるだろう。ここで使用する素材のサウンドが少し音量が小さくて聞き取りにくいため、「Volume Db」プロパティを10に設定して、音量を少し大きくした。

AudioStreamPlayerのVolume Dbを編集

ではスクリプト「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」クラスのノードを追加する必要があるので、ここは以下の手順で一気にやってしまおう。

  1. シーンドックで「Game」ノードを選択したら、cmd + A のショートカットキー操作で「AudioStreamPlayer」を一つ追加する
  2. 「AudioStreamPlayer」ノードを選択したまま、cmd + D のショートカットキー操作を 4 回繰り返して、ノードを 4 つ複製する。

今、シーンドックはこのようになっているはずだ。

Gameシーンのシーンドック表示1

続いて以下のように、複数用意した「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シーンのシーンドック表示2

ここまでできたら、次は「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 % にしておこう。

GameノードのDrop Rateを編集



ここまでで、サウンドエフェクトの追加が一通り終わった。似たような作業をひたすら繰り返したので、手順には慣れていただけたことと思う。

次はいよいよ BGM を追加していく。


BGM を実装する

BGM はサウンドエフェクトのような短い音ではないので、playメソッドで再生して放っておくわけにはいかない。状況に応じて、適切なタイミングで音楽を停止する必要がある。それを念頭に、作業を進めていこう。


スタート画面に BGM を実装する

まずはスタート画面の BGM から実装していく。

まずは例によって、「GameStartView.tscn」シーンを開き、「GameStartView」ルートノードに「AudioStreamPlayer」クラスのノードを追加する。名前を「StartBGM」としておこう。

GameStartViewにAudioStreamPlayer追加

「StartBGM」ノードの「Stream」プロパティには素材「res://sounds/bgm_start.ogg」を追加しよう。

音量がやや大きいので「Volume Db」プロパティを-5に変更し、ゲーム開始時から自動的に再生させたいので「Autoplay」プロパティを「オン」する。

StartBGMのVolume DbとAutoplayを編集

一度シーンを実行して、音量と自動再生だけ確認しておこう。
シーンが変われば自動的にそのシーンにある「AudioStreamPlayer」クラスのplayメソッドも停止されるので、スクリプトの編集は不要だ。

プレイ画面に BGM を実装する

ではプレイ画面中のBGMを実装していこう。

「Game.tscn」シーンに切り替えて、「Game」ルートノードに新たに「AudioStreamPlayer」を追加しよう。併せて、名前を「PlayBGM」に変更しておこう。

PlayBGMの追加

続けて「PlayBGM」ノードの「Stream」プロパティに素材「res://sounds/bgm_play.ogg」を追加する。

これでシーンを実行してみるとわかるが、「NextScreen」ノードの青い画面が表示されている間は、このノード以外はプロセスを停止しているので、音楽が再生されない。スタート画面からプレイ画面に切り替わった時にこの無音状態になるのは少し違和感がある。

そこで「PlayBGM」ノードの「Pause Mode」プロパティを「Process」に変更する。これで、親ノードのの「Pause Mode」の値に関係なく、常にプロセスは生きた状態になる。

PlayBGMのPauseModeを編集

これでシーンを実行してみると「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」ノードのvisibilityfalseで、「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」ノードのプロパティを以下の手順で変更していく。

  1. 「Stream」プロパティに素材「res://sounds/bgm_gameover.ogg」を追加する
  2. やや音量を抑えるために「Volume Db」プロパティの値を-10に変更する
  3. よりダークな印象にするため「Pitch Scale」プロパティを0.8に変更して、音程とテンポを落とす
  4. 「Autoplay」プロパティをオンにして、ゲームオーバー画面に切り替わり次第 BGM が再生されるようにする

PlayBGMのPauseModeを編集

なお、ここではスクリプトを編集する必要はない。ではシーンを実行して確認しよう。



全体を通してにサウンドを確認する

では最後に、プロジェクトを実行して、プロジェクト全体のサウンドのバランスなどを確認しておこう。パワーアップアイテムのドロップ率は2.0に戻しておく。



概ね問題なさそうなので、今回のサウンドの追加作業はこれにて終了だ。



おわりに

以上で Part 12 は完了だ。今回は BGM やサウンドエフェクトを追加するという内容だった。ほとんど同じ作業だったのですぐに慣れていただけたのではないだろうか。

次の Part 13 では HUD にハイスコアの要素を追加し、達成したスコアやレベルのデータを保存する機能、またそれを読み込む機能を実装する。