第13回目の今回は、ゲームにサウンドを追加する。ゲームに使われるサウンドというのは、大きく分けて2種類ある。バックグラウンドミュージック(BGM)と効果音(サウンドエフェクト:SFX)だ。Godot Engine ではどちらも追加する方法に大きな違いはない。素材さえ用意すれば、追加するのは比較的簡単だ。特定のタイミングでサウンドを鳴らしたい時、例えば、プレイヤーキャラクターがジャンプした時にジャンプの効果音を鳴らしたい場合などには、プログラミングして制御する必要があるが、複雑なコードにはならないので安心してほしい。

今回のチュートリアルには GIF 画像ではなく mp4 フォーマットの動画を載せている。この記事の閲覧環境は様々だと思われるため、デフォルトではサウンドをミュートしている。動画のコントローラでミュート解除していただきつつ、確認いただきたい。

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


以下の公式オンラインドキュメントのページに、サウンド関係のリンクがまとまっている。今回このチュートリアルで扱う内容以外の有益な情報も含まれるので、併せて目を通していただくとよりわかりやすいだろう。

公式オンラインドキュメント:
オーディオ



サウンドの素材を準備する


サウンドの素材をファイルシステムに追加する

ゲームにサウンドを追加するには、まずサウンドの素材を用意する必要がある。ご自身で作曲する場合もあれば、アセットストアなどで誰かが作曲したものをダウンロードする場合もあるだろう。

今回はチュートリアル用に音源ファイルを用意しているので、以下のリンクから「Audio.zip」をダウンロードしてほしい。

Download Asset:
Dropboxの共有フォルダ > Audio.zip

ダウンロードできたら、.zip ファイルを展開して中の「Audio」フォルダを取り出してほしい。プロジェクトのファイルシステムドックで「res://Assets/」フォルダを選択した状態にし、そのフォルダ目掛けて「Audio」フォルダごとドラッグ&ドロップで追加しよう。
Audioフォルダを追加

なお、2022/04/06 以降に Part 1 でアセットをダウンロードされた方は、Godot のファイルシステムの「res://Assets/」のフォルダに各種オーディオファイルを格納した「Audio」フォルダが最初から入っているはずだ。


BGM 用の素材のループ設定を調整する

SFX は、特定のタイミングで都度、一回サウンドを鳴らすだけだが、BGM に関しては曲が最後まで終わったらまた頭から再生する、つまりループさせる必要がある。

.ogg や .mp3 フォーマットのリソースだと、デフォルトでループの設定が有効になっており、.wav フォーマットだとループは無効になっているようだ。

ゲームのサウンドに関しては一般的に、BGM 用の曲は長いので、ファイルサイズの関係から、圧縮した .ogg または .mp3 が推奨されている。一方、SFX 用のサウンドは短いので、非圧縮形式の .wav フォーマットであることが多い。ループのデフォルトの設定は、この背景に由来する仕様だと考えられる。

先ほど追加していただいたこのチュートリアル用のサウンド素材についても、BGM 用は .ogg フォーマット、SFX 用は .wav フォーマットにしているので、どれがどっちの素材かはファイル名の拡張子を見ればすぐに判別していただけるだろう。

具体的に、それぞれのサウンド素材のループの設定を確認する手順は以下の通りだ。

  1. ファイルシステムドックで音源ファイルを選択する
    ファイルシステムドックで音源ファイル選択
  2. そのままインポートドックでループの設定を確認する
    • BGM 用 .ogg ファイルの場合:
      .ogg のループの設定を確認
    • SFX 用 .wav ファイルの場合:
      .wav のループの設定を確認
  3. もしループの設定がおかしい(BGM 用なのにループが無効、または SFX 用なのにループが有効)場合は、ループの設定のチェックを切り替えて、「再インポート」ボタンをクリックすれば、設定が変更された状態で素材ファイルが再インポートされる。
    .wav のループの設定を確認

公式オンラインドキュメント:
オーディオサンプルのインポート


ところで、今回インポートしていただいた素材のうち、BGM 用のサウンド素材で 1 つだけループ開始位置を編集する必要がある。「Level1」シーンで使用する予定の「Land.ogg」だ。イントロは初回のみ再生され、それ以降ループされて再生位置が前に戻るときは、曲の頭ではなくイントロ部分を省いた位置から再生されるようにしたい。これも Godot エディタ上で調整できる。以下の手順で実際に編集しよう。

  1. ファイルシステムドックで「res://Assets/Audio/Land.ogg」を選択し、インポートドックを開く
  2. 「Loop Offset」のパラメータをイントロが終わる秒数「9.142」に設定して、「再インポート」ボタンをクリック
    Loop Offset の編集

ちなみに、今後、ご自身で作曲される場合に役立つのでループ開始位置の計算方法もお伝えしておく。

  1. まず曲全体の長さを確認する。Godot エディタ上で確認する場合は、ファイルシステムで該当のファイルをダブルクリックすると、インスペクターにファイルの情報が表示される。この時、一番右下に表示されている秒数が曲の長さだ。「Land.ogg」の場合は 32.00 秒となっている(単位の s は seconds の略)。
    曲の長さ
  2. 「曲の長さ」÷「曲全体の小節数」=「1小節の長さ」が求められる。
  3. 「1小節の長さ」×「イントロの小節数」でイントロの長さが求められる。
  4. 「Land.ogg」の場合「曲全体の小節数」は 28 小節「イントロの小節数」は 8 小節なので
    32.00 ÷ 28 × 8 = 9.142…
    電卓計算結果
    と、割り切れない場合は、小数点以下第3位までで切り捨て、または小数点以下第4位で四捨五入する。0.001秒の差はほとんど気にならないからどちらでも大丈夫だ。

さて、これでサウンド素材の下準備は完了だ。ここからの全体的な作業の流れとしては、先にゲームのパーツになる SFX のみ必要なシーン(「Player」や「Item」など)から編集し、そのあと画面を構成する大きなシーン(「GameStart」や「Level1」など)に BGM、SFX を追加していく。

シーンごとの作業は以下の順番で進めていこう。

  1. ノードの追加
  2. ノードのプロパティの編集
  3. スクリプトのアタッチ&コーディング


Player シーン

それではまずはじめに「Player.tscn」を開いてサウンドを追加していこう。このシーンに必要な SFX は、以下の4種類だ。

  • プレイヤーキャラクターの足音
  • プレイヤーキャラクターがジャンプする音
  • プレイヤーキャラクターが敵に当たってダメージを受けた時の音
  • プレイヤーキャラクターのヘルスがゼロになった時の音

AudioStreamPlayer2D ノードを追加する

「Player」ルートノードに「AudioStreamPlayer2D」クラスのノードを4つ追加して、それぞれ「StepSFX」「JumpSFX」「DamageSFX」「DieSFX」という名前にそれぞれ変更しよう。これらのノードは先述した 4 つの効果音ごとに分けて利用する。
AudioStreamPlayer2Dを追加

なお、「AudioStreamPlayer」は位置に関係なくサウンドを再生するが、「AudioStreamPlayer2D」は画面上の位置を利用して、カメラとの距離が遠ければ音量を小さくするなどできる。このチュートリアルではその辺りの細かな演出をする機会はないが、画面上に配置するオブジェクトのシーンであれば「AudioStreamPlayer2D」を利用するという認識を持っておいて良いだろう。

公式オンラインドキュメント:
AudioStreamPlayer2D
AudioStreamPlayer


AudioStreamPlayer2D ノードを編集する

以下のノードとリソースの組み合わせで、それぞれのノードの「Stream」プロパティに適切なサウンドリソースを適用しよう。ファイルシステムドックから下記のリソースファイルをインスペクター上の「Stream」プロパティめがけてドラッグ&ドロップする。

  • 「StepSFX」ノード:res://Assets/Audio/FootStep.wav
  • 「JumpSFX」ノード:res://Assets/Audio/Jump.wav
  • 「DamageSFX」ノード:res://Assets/Audio/TakeDamage.wav
  • 「DieSFX」ノード:res://Assets/Audio/Die.wav

Streamプロパティ

この「Player」シーンを含めて、基本的に「Stream」プロパティ以外の編集が不要なシーンが多いが、音量が大きすぎる、または小さすぎると感じる場合は、適宜「Volume Db」プロパティの値で調整して欲しい。音量の確認は「Stream」プロパティの適用済みリソースをクリックし、インスペクター下部の再生/停止ボタンを利用するのが良いだろう。
Streamプロパティ

Streamプロパティ


スクリプトを編集して SFX の再生を制御する

「Player.gd」スクリプトを編集してサウンド再生を制御する。

まずは以下の 4 つの「AudioStreamPlayer2D」ノードを参照するプロパティを定義する。

onready var step_sfx = $StepSFX # 追加
onready var jump_sfx = $JumpSFX # 追加
onready var damage_sfx = $DamageSFX # 追加
onready var die_sfx = $DieSFX # 追加

足音

まずはプレイヤーキャラクターが走っている時の足音から追加しよう。

「StepSFX」のサウンドは、「AnimatedSprite」ノードの「run」アニメーションでキャラクターのテクスチャの足が地面についたタイミングでだけ再生したい。アニメーションを確認すると、Frame 4 と 10 がそれに該当することがわかる。
AnimatedSpriteのrunアニメーション

これを実現するには、Frame が変わるたびに発信されるシグナルを利用し、if構文で「run」アニメーション再生時の Frame の数を指定すれば良い。

さっそくシグナルの接続を行おう。「AnimatedSprite」ノードの「frame_changed()」シグナルを「Player.gd」スクリプトに接続すればOKだ。
AnimatedSpriteのrunアニメーション

スクリプトに_on_AnimatedSprite_frame_changedメソッドが追加されたら、それを以下のように編集しよう。

func _on_AnimatedSprite_frame_changed():
	if sprite.animation == "run":
		if sprite.frame == 4  or sprite.frame == 10:
			step_sfx.play()

これで足音の SFX が追加できたはずだ。「Level1」シーンで確認してみよう。


ジャンプする音

次はプレイヤーキャラクターがジャンプした時の音を追加する。これには_physics_processメソッド内でジャンプを制御しているコードの箇所にサウンド再生のコードを追加すれば良い。以下のコードの「# 追加」のコメントがあるたった 1 行が更新箇所だ。

func _physics_process(delta):
	velocity.y += gravity * delta
	var x_input = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	
	if x_input != 0:
		velocity.x += x_input * acceleration
		if Input.is_action_pressed("dash"):
			velocity.x = clamp(velocity.x, -max_dash_speed, max_dash_speed)
		else:
			velocity.x = clamp(velocity.x, -max_speed, max_speed)
		sprite.flip_h = x_input < 0
	
	if is_on_floor():
		if x_input == 0:
			sprite.play("idle")
			velocity.x = lerp(velocity.x, 0, friction)
		else:
			sprite.play("run")
		
		if Input.is_action_just_pressed("jump"):
			sprite.play("jump")
			velocity.y = -jump_force
			# JumpSFXノードのサウンドを再生する(ジャンプ音)
			jump_sfx.play() # 追加

	else:
		if x_input == 0:
			velocity.x = lerp(velocity.x, 0, air_resistance)

		if Input.is_action_just_released("jump") and velocity.y < -jump_force / 2:
			velocity.y = -jump_force / 2

	velocity = move_and_slide(velocity, Vector2.UP)

	# Added @ Part 3
	if position.x < 16:
		position.x = 16

「Level1」シーンで確認してみよう。



敵に当たってダメージを受けた時の音

続いて、プレイヤーキャラクターが敵キャラクターに当たってダメージを受けた時のサウンド再生を制御する。これは既存の_on_HitBox_body_enteredメソッドにサウンド再生のコードを少し足すだけだ。このメソッドは「HitBox」ノードのコリジョン形状に敵キャラクターを含む物理ボディが当たったら呼ばれる。

# HitBox ノードのコリジョン形状に物理ボディが当たった時に呼ばれるメソッド
func _on_HitBox_body_entered(body):
	if body.is_in_group("Enemies"):
		print("Enemy hit player. Damage is ", body.damage)
		emit_signal("enemy_hit", body.damage)
		sprite.play("hit")
		# DamageSFX ノードのサウンドを再生する
		damage_sfx.play() # 追加

「Level1」シーンで確認してみよう。



ヘルスがゼロになって死んだ時の音

プレイヤーキャラクターに実装する最後の SFX は死亡した時のサウンドだ。敵に当たるか、画面下へ落下してヘルスバーが 0 になった場合に再生されるようにする。ヘルスバーの制御は「Game.gd」スクリプトの方でやっているので、そちらを編集しよう。

「Game.gd」スクリプトを開いたらmanage_healthメソッドに 1 行追加するだけだ。「# 追加」のコメントをしている箇所だ。

func manage_health(damage):
	health -= damage
	print("Health updated: ", health)
	hud.update_health(health)
	if health <= 0:
		player.anim_player.play("die")
		# Player インスタンスノードの DieSFX ノードのサウンドを再生
		player.die_sfx.play() # 追加
		yield(player.anim_player, "animation_finished")
		var gameover = load("res://UI/GameOver/GameOver.tscn").instance()
		ui_layer.add_child(gameover)
		print("Game over screen is shown up.")
		get_tree().paused = true
		print("Scene tree paused: ", get_tree().paused)

ヘルスが 0 以下になった時に「die」アニメーションと同時に SFX も再生される。そして、再生が終了するまで待機してからゲームオーバー画面に遷移する。

では実際にプロジェクトを実行して挙動を確認しておこう。




Enemy シーン

続いて敵キャラクターのサウンドを実装する。敵キャラクターのサウンドはプレイヤーキャラクターに踏まれた時の SFX のみだ。では「Enemy.tscn」シーンファイルを開いて編集していこう。


AudioStreamPlayer2D ノードを追加してプロパティを編集する

「Enemy」ルートノードに「AudioStreamPlayer2D」クラスのノードを 1 つ追加しよう。同じクラスのノードは他にないので、名前はそのままにしておこう。
AudioStreamPlayer2Dを追加


「AudioStreamPlayer2D」ノードの「Stream」プロパティに「res://Assets/Audio/HitEnemy.wav」を適用しよう。必要に応じて「Volume Db」プロパティで音量を調整してほしい。


スクリプトを編集して SFX の再生を制御する

「Enemy.gd」スクリプトを開いて、編集していこう。SFX の再生を制御する。

まずはこれまで同様、「AudioStreamPlayer2D」ノードを参照するプロパティを定義しよう。

# AudioStreamPlayer2D ノードの参照
onready var audio_player = $AudioStreamPlayer2D

続けて、プレイヤーキャラクターを含む物理ボディが、「HitBox」のコリジョン形状に入った時に呼ばれる_on_HitBox_body_enteredメソッドを更新する。「# 追加」とコメントしている行が更新箇所だ。

# 物理ボディが HitBox に入ったら呼ばれるメソッド
func _on_HitBox_body_entered(body):
	if body.is_in_group("Players"):
		print("Player entered in ", self.name)
		sprite.play("hit")
		# AudioStreamPlayer2D ノードのサウンドを再生
		audio_player.play() # 追加
		yield(sprite, "animation_finished")
		# AudioStreamPlayer2D のサウンド再生が終了するまで待機
		yield(audio_player, "finished") # 追加
		queue_free()
		print(self.name, " died")

これで、プレイヤーキャラクターが敵キャラクターを踏んづけた時に、SFX が再生され、再生が終了してからqueue_freeメソッドにて敵キャラクターオブジェクトが解放される。

では「Level1」シーンを実行して動作確認をしておこう。




Item シーン

続いてアイテムのサウンドを実装する。アイテムのサウンドはプレイヤーキャラクターが当たった時の SFX のみだ。ちょうどスーパーマリオがコインを取った時の「キラン」という SFX と同様の演出だ。では「Item.tscn」シーンファイルを開いて編集していこう。


AudioStreamPlayer2D ノードを追加してプロパティを編集する

「Item」ルートノードに「AudioStreamPlayer2D」クラスのノードを1つ追加しよう。同じクラスのノードは他にないので、今回も名前はそのままだ。
AudioStreamPlayer2Dを追加


「AudioStreamPlayer2D」ノードの「Stream」プロパティに「res://Assets/Audio/GetItem.wav」を適用しよう。必要に応じて「Volume Db」プロパティで音量を調整しよう。


スクリプトを編集して SFX の再生を制御する

では「Item.gd」スクリプトを編集して SFX を制御していく。

例によって、まずは「AudioStreamPlayer2D」ノードを参照するプロパティを定義しよう。

# AudioStreamPlayer2D ノードの参照
onready var audio_player = $AudioStreamPlayer2D # 追加

続けて、プレイヤーキャラクターを含む物理ボディが、「Item」のコリジョン形状に入った時に呼ばれるメソッドを更新する。「# 追加」とコメントしている行が更新箇所だ。

func hit(body):
	print("Got ", point, " point.")
	body.emit_signal("item_hit", point)
	anim_player.play("hit")
	# AudioStreamPlayer2D ノードのサウンドを再生する
	audio_player.play() # 追加
	yield(anim_player, "animation_finished")
	# AudioStreamPlayer2D のサウンド再生が終了するまで待機
	yield(audio_player, "finished") # 追加
	queue_free()

これで、アイテムを取ったら、SFX が再生されてからアイテムが解放されるようになったはずだ。

このまま続けて「BrokenBox」シーンのサウンドも追加していこう。


BrokenBox シーン

アイテムボックスに追加するサウンドは、壊れた時の SFX だ。「ItemBox.tscn」シーンファイルを編集したいところだが、一番簡単なのは、壊れた時に「ItemBox」シーンツリーにそのインスタンスが追加される「BrokenBox.tscn」の方だ。ではこのシーンを開いて編集していこう。


AudioStreamPlayer2D ノードを追加してプロパティを編集する

「BrokenBox」ルートノードに「AudioStreamPlayer2D」ノードを追加しよう。
AudioStreamPlayer2Dを追加

追加した「AudioStreamPlayer2D」ノードにの「Stream」プロパティにリソース「res://Assets/Audio/BreakBox.wav」を適用しよう。


スクリプトを編集して SFX の再生を制御する

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

まずは「AudioStreamPlayer2D」ノードを参照するプロパティを定義。

# AudioStreamPlayer2D ノードの参照
onready var audio_player = $AudioStreamPlayer2D

次に_readyメソッド内にサウンド再生のコードを追加し、シーンが読み込まれたらサウンを再生するようにする。

func _ready():
	emitting = true
	# ノードが読み込まれたら AudioStreamPlayer2D ノードのサウンドを再生
	audio_player.play()

これで箱が壊れた瞬間にこのシーンが読み込まれ、同時に SFX が再生されるはずだ。

ではアイテムを取った時の音とアイテムボックスが壊れた時の音がそれぞれ正しく再生されるか、「Level1」シーンでまとめて確認してみよう。




StartPoint シーン

スタートポイントはそれぞれのレベルでプレイヤーキャラクターが最初に降り立つ場所に設置している市松模様の台と右向き矢印の標識がテクスチャのシーンだ。市松模様の台の上にコリジョン形状を設定しているので、そこに乗ったら、適当な SFX が鳴るようにする。

では「StartPoint.tscn」シーンファイルを開いて編集していこう。

AudioStreamPlayer ノードを追加してプロパティを編集する

「StartPoint」ルートノードに「AudioStreamPlayer2D」クラスのノードを1つ追加しよう。同じクラスのノードは他にないので、名前はそのままでも良いだろう。
AudioStreamPlayerを追加

次に、インスペクタードックで「AudioStreamPlayer2D」ノードのプロパティを編集する。「Stream」プロパティにリソースファイル「res://Assets/Audio/StartPoint.wav」を適用しよう。


スクリプトを編集して SFX の再生を制御する

「StartPoing.gd」スクリプトを編集してサウンド再生を制御する。

まずは「AudioStreamPlayer2D」ノードを参照するプロパティを定義する。

# AudioStreamPlayer2D ノードの参照
onready var audio_player = $AudioStreamPlayer2D # 追加

次に以下のメソッドを更新しよう。「# 追加」のコメントの行が更新箇所だ。

# 物理ボディが Area2D ノードのコリジョン形状に入ったら呼ばれるメソッド
func _on_StartPoint_body_entered(body):
	if body.name == "Player":
		sprite.play("moving")
		# AudioStreamPlayer2D ノードのサウンドを再生
		audio_player.play() # 追加

これで、スタートポイントの台にプレイヤーキャラクターが乗ったら SFX が再生されるようになったはずだ。

「Level1」シーンを再生して確認してみよう。




Checkpoint シーン

チェックポイントは、各レベルの真ん中くらいに設置している通過点で、最初はただのポールだが、プレイヤーキャラクターが当たるとポールに市松模様のフラグがつく。これも当たった時に適当な SFX が鳴るようにする。

では「Checkpoint.tscn」シーンファイルを開いてサウンドを追加していこう。


AudioStreamPlayer2D ノードを追加してプロパティを編集する

「Checkpoint」ルートノードに「AudioStreamPlayer2D」クラスのノードを1つ追加しよう。
AudioStreamPlayerを追加

続けて「AudioStreamPlayer2D」ノードの「Stream」プロパティにリソース「res://Assets/Audio/Checkpoint.wav」を適用する。


スクリプトを編集して SFX の再生を制御する

「Checkpoint.gd」スクリプトを編集してサウンド再生を制御する。

まずは以下のプロパティを追加する。

# AudioStreamPlayer2D ノードの参照
onready var audio_player = $AudioStreamPlayer2D # 追加

次に以下のメソッドを更新しよう。

# 物理ボディがコリジョン形状に入ったら呼ばれるメソッド
func _on_Checkpoint_body_entered(body):
	if body.name == "Player" and not is_checked:
		sprite.play("flag_out")
		# AudioStreamPlayer2D ノードのサウンドを再生
		audio_player.play() # 追加
		yield(sprite, "animation_finished")
		sprite.play("flag_idle")
		is_checked = true

これで、プレイヤーキャラクターがチェックポイントのフラグに当たると SFX が再生されるはずだ。これも「Level1」シーンで確認してみよう。




EndPoint シーン

エンドポイントは各レベルのゴールとして用意している、優勝カップのようなテクスチャのオブジェクトだ。このカップにプレイヤーキャラクターが入ると次のシーンにワープするような演出のアニメーションが再生される。これに合わせるようにして、ワープっぽい SFX を同時に再生させよう。

では「EndPoint.tscn」シーンファイルを開いて編集していく。


AudioStreamPlayer2D ノードを追加してプロパティを編集する

「EndPoint」ルートノードに「AudioStreamPlayer2D」クラスのノードを1つ追加しよう。
AudioStreamPlayerを追加

「AudioStreamPlayer2D」ノードの「Stream」プロパティにリソース「res://Assets/Audio/Warp2.wav」を適用しよう。ただ、ワープ用の SFX リソースを 3 つ用意している。「Warp2.wav」が(個人的には)一番しっくりくるので、このチュートリアルではこれを採用するが、あなたのお好みのものを適用していただければOKだ。


スクリプトを編集して SFX の再生を制御する

「EndPoint.gd」スクリプトを編集してサウンド再生を制御する。

まずはこれまで同様「AudioStreamPlayer2D」ノードの参照プロパティを定義する。

# AudioStreamPlayer2D ノードの参照
onready var audio_player = $AudioStreamPlayer2D # 追加

そして、プレイヤーキャラクターを含む物理ボディが当たったら呼ばれるメソッド内で、サウンドの再生と再生が終了するまで待つコードを追加する。「# 追加」のコメントがある行だ。

# 物理ボディがコリジョン形状に入ったら呼ばれるメソッド
func _on_EndPoint_body_entered(body):
	if body.name == "Player":
		anim_player.play("clear")
		yield(anim_player, "animation_finished")
		particle.emitting = true
		body.anim_player.play("clear")
		# AudioStreamPlayer2D ノードのサウンドを再生
		audio_player.play() # 追加
		yield(body.anim_player, "animation_finished")
		# AudioStreamPlayer2D ノードのサウンド再生が終了するまで待機
		yield(audio_player, "finished") # 追加
		print("Moving to the next level!")
		get_parent().queue_free()

これでプレイヤーキャラクターがエンドポイントの優勝カップに入ったら、ワープのアニメーションとともにワープっぽい SFX が再生され、アニメーションとサウンドが終了次第、現在のレベルシーンが解放され、次のレベルシーンに画面遷移する。

「Level1」シーンで SFX の再生と再生後の「Level1」シーンの解放まで確認しよう。最後までプレイするのが面倒なので、「EndPoint2」インスタンスノードを「Level1」ルートノードに追加してスタートポイント付近に設置してテストするのが手っ取り早い。テストが終わったら追加したインスタンスノードを削除すればOKだ。



ここまででパーツ的なシーンのサウンド追加作業は完了だ。少し休憩しよう。休憩後は BGM が必要なシーンを順番に編集していく。



GameStart シーン

ここからは BGM の追加が必要なシーンを編集していく。

まずはスタート画面から編集する。「GameStart.tscn」シーンが読み込まれた瞬間から BGM を流したい。ゲームの一番最初に流れる BGM だ。そして、ゲームを開始するためにスペースキーやエンターキーを押した時に再生される SFX も追加しよう。


AudioStreamPlayer ノードを追加する

「GameStart」ルートノードに「AudioStreamPlayer」クラスのノードを2つ追加して、それぞれ「BGMPlayer」「SFXPlayer」という名前に変更しよう。その名の通り、BGM を鳴らすためのノードと SFX を鳴らすためのノードを別々で追加した。理由は、後ろで BGM が鳴っている間に効果音を鳴らす必要があるからだ。
AudioStreamPlayerを追加


BGMPlayer ノードを編集する

「BGMPlayer」ノードのプロパティを以下の手順で編集していこう。

  1. インスペクターの「Stream」プロパティへファイルシステムドックから「res://Assets/Audio/TitleScreen.ogg」をドラッグ&ドロップして素材を追加しよう。
  2. 最後に「Autoplay」プロパティをオンにしよう。シーンが読み込まれた瞬間からBGM が自動的に再生されるようにするためだ。
    Autoplayプロパティ

SFXPlayer ノードを編集する

次は「SFXPlayer」ノードのプロパティを同様の手順で編集していこう。

  1. インスペクターの「Stream」プロパティへファイルシステムドックから「res://Assets/Audio/PressEnter.wav」をドラッグ&ドロップして素材を追加しよう。
  2. もし音量が大きすぎる、または小さすぎると感じる場合は「Volume Db」プロパティの値を調整しよう。
  3. 「Autoplay」プロパティは オフ にしておく。SFX の再生のタイミングはスクリプトで制御するからだ。
    Autoplayプロパティ

スクリプトを編集して SFX の再生を制御する

では SFX の再生をスクリプトで制御する。「GameStart.gd」スクリプトを開いて編集していこう。

まずは「SFXPlayer」ノードを参照するプロパティを追加する。

# SFXPlayer ノードの参照
onready var sfx_player = $SFXPlayer

続いて_on_StartButton_button_upメソッドを編集する。「# 追加」とコメントしている行が更新した箇所だ。

func _on_StartButton_button_up():
	#print("_on_StartButton_button_up called.")
	anim_player.play("press_start")
	sfx_player.play() # 追加
	yield(anim_player, "animation_finished")
	yield(sfx_player, "finished") # 追加
	queue_free()
	print("GameStart scene is now freed.")

「SFXPlayer」ノードのplayメソッドで「Stream」プロパティにセットされている音源を再生する。yield関数により、その SFX の再生が終了するまで待機する。そのあとqueue_freeメソッドによりスタート画面のインスタンスノードが解放される流れだ。

これで、スタート画面でスタートボタンをクリック、またはスペースキーまたはエンターキーを押したら、SFX が再生されるようになったはずだ。

シーンを再生して動作確認をしておこう。下の動画では次の項目を確認している。

  • シーンが読み込まれて BGM が自動再生されるか
  • BGM がループ再生されるか
  • スペースキーを押して SFX が再生されるか




CharacterSelect シーン

次はプレイヤーキャラクター選択画面だ。

「CharacterSelect.tscn」シーンでも、画面が読み込まれた瞬間から BGM を流したい。いよいよゲームが始まるぞ、とワクワクする感じのドラムマーチ的な音楽を適用しよう。

一方、この画面上の操作に合わせて SFX も追加したい。まず必要なのは、左右矢印キー(または画面上のアイコンをクリック)で、キャラクターを切り替える時の SFX 。それに、好きなキャラクターを選んだあと、スペースキーかエンターキーを押して確定させた時の SFX。これはスタート画面と同じ SFX で良いだろう。

では「CharacterSelect.tscn」シーンファイルを開いてサウンドを追加していこう。


AudioStreamPlayer ノードを追加する

「CharacterSelect」ルートノードに「AudioStreamPlayer」クラスのノードを2つ追加して、それぞれ「BGMPlayer」「SFXPlayer」という名前に変更しよう。
AudioStreamPlayerを追加


BGMPlayer ノードを編集する

「BGMPlayer」ノードのプロパティを編集する。

  1. インスペクターの「Stream」プロパティへファイルシステムドックから「res://Assets/Audio/CharacterSelect.ogg」をドラッグ&ドロップして素材を追加する。
  2. 必要に応じて、音量を「Volume Db」プロパティの値を上げ下げして調節する。この音源の場合 -5 ~ -10 くらいがちょうど良いかもしれない。
  3. 「Autoplay」プロパティをオンにする。

SFXPlayer ノードを編集する

次に「SFXPlayer」ノードのプロパティ編集だが、インスペクターで編集することは特にない。今までリソースファイルを適用していた「Stream」プロパティの値も今回は[空]にしておいて良い。このあとスクリプトで特定のタイミングで適切なリソースファイルを適用するようにコーディングする。


スクリプトを編集して SFX の再生を制御する

では「CharacterSelect.gd」スクリプトを編集して SFX を制御していく。

まずはプロパティを 2 つ追加する。

# SFXPlayer ノードの参照
onready var sfx_player = $SFXPlayer # 追加
# SFXPlayer の Stream プロパティに適用する2種類の音源リソースを辞書型データにして定義
onready var sfx_res = {
	"select": preload("res://Assets/Audio/PressSelect.wav"),
	"enter": preload("res://Assets/Audio/PressEnter.wav")
} # 追加

まず「SFXPlayer」ノードを参照するプロパティsfx_playerを定義している。

もう一つのプロパティsfx_resは辞書型データで、2種類の音源リソースファイルのpreloadデータを格納する。sfx_resには左右の矢印キーを押した時のサウンドと確定キー(スペースキー、エンターキー)を押した時のサウンドの2種類が格納されている。それぞれselectおよびonreadyのキーで辞書内の値を呼び出すことができる。


そして、以下の3つのメソッド内に適切な音源リソースを「SFXPlayer」ノードの「Stream」プロパティに割り当てて、再生するためのコードを追加した。

func _on_LeftButton_pressed():
	anim_player.play("press_left")
	print(sfx_res["select"])
	# SFXPlayer ノードの Stream プロパティに res://Assets/Audio/PressSelect.wav を適用
	sfx_player.stream = sfx_res["select"] # 追加
	# SFXPlayer のサウンドを再生
	sfx_player.play() # 追加
	if frames_num > 0:
	# 以下省略


func _on_RightButton_pressed():
	anim_player.play("press_right")
	# SFXPlayer ノードの Stream プロパティに res://Assets/Audio/PressSelect.wav を適用
	sfx_player.stream = sfx_res["select"] # 追加
	# SFXPlayer のサウンドを再生
	sfx_player.play() # 追加
	if frames_num < global.spriteframes.size() -1:
	# 以下省略


unc _input(event):
	if event.is_action_pressed("ui_left"):
		_on_LeftButton_pressed()
	elif event.is_action_pressed("ui_right"):
		_on_RightButton_pressed()
	elif event.is_action_released("ui_accept"):
		is_choosing = false
		# SFXPlayer ノードの Stream プロパティに res://Assets/Audio/PressEnter.wav を適用
		sfx_player.stream = sfx_res["enter"] # 追加
		# SFXPlayer のサウンドを再生 
		sfx_player.play() # 追加
		# SFXPlayer ノードのサウンドの再生が終了するまで待機
		yield(sfx_player, "finished") # 追加
		emit_signal("character_selected", global.spriteframes[frames_num])
		#print("emitted signal: character_selected")
		queue_free()
		print("CharacterSelect scene is now freed.")

「SFXPlayer」ノードは一つだが、スクリプトでサウンドリソースを切り替えることで 2 つの SFX の再生をカバーしている。もちろん、AudioStreamPlayer2D ノードを複数用意してそれぞれに別々のリソースを割り当てても良い。特に今回のチュートリアルのように少ないサウンドリソースしか使わない場合は、ノードを必要数追加する方法の方がわかりやすいかもしれない。もっと大きなプロジェクトで大量の音を扱うことがあれば、シーンツリーをスッキリさせるために、スクリプトで切り替える方法を採用するのも一つだ。

さて、これで左右矢印ボタンアイコンをクリックまたはキーボードの左右矢印キーを押した時はプレイヤーキャラクターが切り替わると同時に「res://Assets/Audio/PressSelect.wav」を再生し、スペースキーまたはエンターキーを押した時は使用キャラクター確定と同時に「res://Assets/Audio/PressEnter.wav」を再生するようになった。

それではシーンを再生して動作確認をしておこう。下の動画では次の項目を確認している。

  • シーンが読み込まれて BGM が自動再生されるか
  • BGM がループ再生されるか
  • 左右矢印キーを押してキャラクター選択の SFX が再生されるか
  • スペースキーを押してキャラクター確定の SFX が再生されるか


GameOver シーン

続いてゲームオーバー画面のサウンドを追加する。ゲームオーバー画面も表示された瞬間から BGM を流したい。ちょっと暗い残念な感じの曲を長そう。

SFX としては、以下の 3 種類が必要だ。

  • Restart か Quit かを切り替える音(キャラクター選択時と同じで良い)
  • 選択を確定する音(これまでの確定の音と同じで良い)
  • 確定した選択をキャンセルする音(これは新しく追加)

では「GameOver.tscn」シーンファイルを開いてそれぞれのサウンドを追加しよう。


AudioStreamPlayer ノードを追加する

「GameOver」ルートノードに「AudioStreamPlayer」クラスのノードを2つ追加して、それぞれ「BGMPlayer」「SFXPlayer」という名前に変更しよう。
AudioStreamPlayerを追加


BGMPlayer ノードを編集する

これまでと同様の手順で「BGMPlayer」ノードのプロパティを編集する。

  1. インスペクターの「Stream」プロパティへファイルシステムドックから「res://Assets/Audio/GameOver.ogg」をドラッグ&ドロップして素材を追加する。
  2. 必要に応じて、音量を「Volume Db」プロパティの値を上げ下げして調節する。
  3. 「Autoplay」プロパティをオンにする。

SFXPlayer ノードを編集する

続けて「SFXPlayer」ノードのプロパティを編集しよう。

  1. 「Stream」プロパティの値は[空]のままにしておく。
  2. もし音量が大きすぎたり小さすぎる場合は「Volume Db」プロパティの値を調整する。
  3. 「Autoplay」プロパティは オフ にしておく。

スクリプトを編集して SFX の再生を制御する

では「GameOver.gd」スクリプトを開いて編集していく。

まずは、プロパティから定義しよう。

# SFXPlayer ノードの参照
onready var sfx_player = $SFXPlayer # 追加
# SFXPlayer の Stream プロパティに適用する3種類の音源リソースを辞書型データにして定義
onready var sfx_res = {
	"select": preload("res://Assets/Audio/PressSelect.wav"),
	"enter": preload("res://Assets/Audio/PressEnter.wav"),
	"cancel": preload("res://Assets/Audio/PressCancel.wav")
} # 追加

次に、4 つのメソッド内に SFX 再生のコードを追加しよう。「# 追加」と記載している行が今回追加した箇所だ。

# Restart ボタンまたは Quit ボタンを押した時に呼ばれるメソッド
func work_at_button_up(option):
	# SFXPlayer の Stream プロパティに res://Assets/Audio/PressEnter.wav を適用
	sfx_player.stream = sfx_res["enter"] # 追加
	# SFXPlayer のサウンドを再生
	sfx_player.play() # 追加
	selected_option = option
	color_rect.visible = true
	if selected_option == RESTART:
		confirmation.dialog_text = restart_text
	elif selected_option == QUIT:
		confirmation.dialog_text = quit_text
	confirmation.popup_centered()

# 確認ダイアログで OK ボタンを押して確定した時に呼ばれるメソッド
func _on_ConfirmationDialog_confirmed():
	# SFXPlayer の Stream プロパティに res://Assets/Audio/PressEnter.wav を適用
	sfx_player.stream = sfx_res["enter"] # 追加
	# SFXPlayer のサウンドを再生
	sfx_player.play() # 追加
	# SFXPlayer のサウンド再生が終了するまで待機
	yield(sfx_player, "finished") # 追加
	if selected_option == RESTART:
		get_tree().paused = false
		print("Scene tree paused: ", get_tree().paused)
		get_tree().change_scene("res://Game/Game.tscn")
		print("The game is restarted.")
	elif selected_option == QUIT:
		get_tree().quit()
		print("The game is quited.")

# 確認ダイアログが非表示になった時に呼ばれるメソッド
func _on_ConfirmationDialog_popup_hide():
	# SFXPlayer の Stream プロパティに res://Assets/Audio/PressCancel.wav を適用
	sfx_player.stream = sfx_res["cancel"] # 追加
	# SFXPlayer のサウンドを再生
	sfx_player.play() # 追加
	selected_option = null
	confirmation.dialog_text = ""
	color_rect.visible = false


# キーボードやマウスのインプット操作を行った時に呼ばれるメソッド
func _input(event):
	if visible:
		if event.is_action_released("ui_left"):
			# SFXPlayer の Stream プロパティに res://Assets/Audio/PressSelect.wav を適用
			sfx_player.stream = sfx_res["select"] # 追加
			# SFXPlayer のサウンドを再生
			sfx_player.play() # 追加
			line2d.position = square_pos[0]
		elif event.is_action_released("ui_right"):
			# SFXPlayer の Stream プロパティに res://Assets/Audio/PressSelect.wav を適用
			sfx_player.stream = sfx_res["select"] # 追加
			# SFXPlayer のサウンドを再生
			sfx_player.play() # 追加
			line2d.position = square_pos[1]
		elif event.is_action_released("ui_accept") and not color_rect.visible:
			if line2d.position == square_pos[0]:
				_on_RestartButton_button_up()
			elif line2d.position == square_pos[1]:
				_on_QuitButton_button_up()

「SFXPlayer」ノードは一つだが、スクリプトで音源リソースを切り替えることで 3 つの SFX の再生をカバーしている。

これで以下の場面で SFX が再生されるようになったはずだ。

  • 「Restart」と「Quit」を選択する際に「PressSelect.wav」が再生される
  • 「Restart」と「Quit」どちらかの選択を確定したら「PressEnter.wav」が再生される
  • 確認ダイアログで「OK」を押して確定した時も「「PressEnter.wav」が再生される
  • 確認ダイアログで「Cancel」を押した時に「PressCancel.wav」が再生される

上記内容に加えて、BGM の音源が正しく再生されるか、正しくループするかも併せて、実際にシーンを実行してみよう。




Level1 シーン

いよいよゲームプレイ中の BGM の追加だ。「Level1」シーンは外なので明るいややアップテンポの曲が最適だ。「Level1.tscn」シーンファイルを開いてサウンドを追加しよう。ちなみにこのシーンに SFX は不要なので、スクリプトの編集も不要だ。1分で作業を終えられるかもしれない。


AudioStreamPlayer ノードを追加する

「Level1」ルートノードに「AudioStreamPlayer」クラスのノードを1つ追加して「BGMPlayer」という名前に変更しよう。
AudioStreamPlayerを追加


BGMPlayer ノードを編集する

「BGMPlayer」ノードのプロパティを編集する。

  1. インスペクターの「Stream」プロパティへファイルシステムドックから「res://Assets/Audio/Land.ogg」をドラッグ&ドロップして素材を追加する。
  2. 必要に応じて、音量を「Volume Db」プロパティの値を上げ下げして調節する。
  3. 「Autoplay」プロパティをオンにする。

では BGM の自動再生とループを確認しておこう。なお、このリソースのみループ開始位置を調整しているので、開始位置に戻った時に違和感がないかはよく聴いて確認する必要がある。以下の動画では 0:31 のタイミングでループ開始位置に戻っている。




Level2 シーン

ようやく最後のシーンだ。「Level2」シーンはジメジメした(キノコが大量に生息する)ダンジョンの中なので、少しおどろおどろしい(ややホラーっぽい)曲が似合うはずだ。「Level2.tscn」シーンファイルを開いてサウンドを追加しよう。このシーンも SFX は不要なので、「Level1.tscn」の編集とほとんど同じ作業だ。


AudioStreamPlayer ノードを追加する

「Level2」ルートノードに「AudioStreamPlayer」クラスのノードを1つ追加して「BGMPlayer」という名前に変更しよう。
AudioStreamPlayerを追加


BGMPlayer ノードを編集する

「BGMPlayer」ノードのプロパティを編集する。

  1. インスペクターの「Stream」プロパティへファイルシステムドックから「res://Assets/Audio/Dungeon.ogg」をドラッグ&ドロップして素材を追加する。
  2. 必要に応じて、音量を「Volume Db」プロパティの値を上げ下げして調節する。
  3. 「Autoplay」プロパティをオンにする。

では BGM の自動再生とループを確認しておこう。以下の動画では 0:49 のタイミングでループ開始位置に戻っている。



これでサウンドの追加が必要なシーンの編集作業は完了した。最後にプロジェクト全体を通して音の確認をして今回の作業を終えよう。





Part 13 で編集したスクリプトのコード

最後に今回の Part 13 で編集したスクリプトのコードを共有しておくので、必要に応じて確認してほしい。

Player.gd の全コード
extends KinematicBody2D # Created @ Part 1

signal enemy_hit(damage) # Added @ Part 8
signal item_hit(point) # Added @ Part 8

export var acceleration = 256
export var max_speed = 64
export var max_dash_speed = 200
export var friction = 0.1
export var gravity = 512
export var jump_force = 224
export var air_resistance = 0.02
var velocity = Vector2()
onready var sprite = $AnimatedSprite
onready var anim_player = $AnimationPlayer # Added @ Part 7
onready var step_sfx = $StepSFX # Added @ Part 13
onready var jump_sfx = $JumpSFX # Added @ Part 13
onready var damage_sfx = $DamageSFX # Added @ Part 13
onready var die_sfx = $DieSFX # Added @ Part 13


func _ready(): # Added @ Part 7
	sprite.position = Vector2(0, 0)
	sprite.scale = Vector2(1, 1)
	sprite.modulate = Color(1, 1, 1, 1)


func _physics_process(delta):
	velocity.y += gravity * delta
	var x_input = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	
	if x_input != 0:
		velocity.x += x_input * acceleration
		if Input.is_action_pressed("dash"):
			velocity.x = clamp(velocity.x, -max_dash_speed, max_dash_speed)
		else:
			velocity.x = clamp(velocity.x, -max_speed, max_speed)
		sprite.flip_h = x_input < 0
	
	if is_on_floor():
		if x_input == 0:
			sprite.play("idle")
			velocity.x = lerp(velocity.x, 0, friction)
		else:
			sprite.play("run")
		
		if Input.is_action_just_pressed("jump"):
			sprite.play("jump")
			velocity.y = -jump_force
			jump_sfx.play() # Added @ Part 13

	else:
		if x_input == 0:
			velocity.x = lerp(velocity.x, 0, air_resistance)

		if Input.is_action_just_released("jump") and velocity.y < -jump_force / 2:
			velocity.y = -jump_force / 2

	velocity = move_and_slide(velocity, Vector2.UP)

	# Added @ Part 3
	if position.x < 16:
		position.x = 16


func _on_HitBox_body_entered(body): # Added @ Part 8
	if body.is_in_group("Enemies"):
		print("Enemy hit player. Damage is ", body.damage)
		emit_signal("enemy_hit", body.damage)
		sprite.play("hit")
		damage_sfx.play() # Added @ Part 13


func _on_AnimatedSprite_frame_changed(): # Added @ Part 13
	if sprite.animation == "run":
		if sprite.frame == 4  or sprite.frame == 10:
			step_sfx.play()

Game.gd の全コード
extends Node # Added @ Part 7

export var current_level = 1
export var final_level = 2

var health: float = 100.0 # Added @ Part 8
var score: int = 0 # Added @ Part 8
var level: Node2D
var player: KinematicBody2D
var char_frames: SpriteFrames # Added @ Part 10

onready var ui_layer = $UI # Added @ Part 10
onready var gamestart = $UI/GameStart # Added @ Part 10
onready var hud = $UI/HUD # Added @ Part 8
#onready var gameover = $UI/GameOver # Added @ Part 9 # Removed @ Part 10


func _ready():
	#gameover.visible = false # Added @ Part 9 # Removed @ Part 10
	gamestart.connect("tree_exited", self ,"_on_GameStart_tree_exited") # Added @ Part 10


func _on_GameStart_tree_exited(): # Added @ Part 10
	var char_select = load("res://UI/CharacterSelect/CharacterSelect.tscn").instance()
	char_select.connect("character_selected", self ,"_on_CharacterSelect_character_selected") # Added @ Part 10
	ui_layer.add_child(char_select)


func _on_CharacterSelect_character_selected(sprite_frames): # Added @ Part 10
	char_frames = sprite_frames
	add_level() # Moved @ Part 10 # Modified @ Part 10
	hud.update_health(health) # Added @ Part 8 # Moved @ Part 10
	hud.update_score(score) # Added @ Part 8 # Moved @ Part 10
	hud.update_level(current_level) # Added @ Part 8 # Moved @ Part 10


func add_level(): # Modified @ Part 10
	level = load("res://Levels/Level" + str(current_level) + ".tscn").instance()
	level.connect("tree_exited", self, "change_level")
	level.connect("player_dropped", self, "_on_Level_player_dropped") # Added @ Part 9
	call_deferred("add_child", level) # Fixed error
	player = level.get_node("Player") # Added @ Part 8
	player.connect("enemy_hit", self, "_on_Player_enemy_hit") # Added @ Part 8
	player.connect("item_hit", self, "_on_Player_item_hit") # Added @ Part 8
	player.get_node("AnimatedSprite").frames = char_frames # Added @ Part 10


func change_level():
	if get_tree(): # Added @ Part 9
		print("change_level() called.")
		if current_level < final_level:
			print("change to next level.")
			level.queue_free()
			current_level += 1
			hud.update_level(current_level) # Added @ Part 8
			add_level()
		else:
			print("Game Clear! Congrats!")
			get_tree().quit()


func _on_Player_enemy_hit(damage): # Added @ Part 8
	manage_health(damage) # Modified @ Part 9


func _on_Player_item_hit(point): # Added @ Part 8
	score += point
	hud.update_score(score)


func _on_Level_player_dropped(): # Added @ Part 9
	manage_health(100)


func manage_health(damage): # Added @ Part 9
	health -= damage # Moved from _on_Player_enemy_hit() @ Part 9
	print("Health updated: ", health) # Moved from _on_Player_enemy_hit() @ Part 9
	hud.update_health(health) # Moved from _on_Player_enemy_hit() @ Part 9
	# Added @ Part 9
	if health <= 0:
		player.anim_player.play("die")
		player.die_sfx.play() # Added @ Part 13
		yield(player.anim_player, "animation_finished")
		#gameover.visible = true # Removed @ Part 10
		var gameover = load("res://UI/GameOver/GameOver.tscn").instance() # Added @ Part 10
		ui_layer.add_child(gameover) # Added @ Part 10
		print("Game over screen is shown up.")
		get_tree().paused = true
		print("Scene tree paused: ", get_tree().paused)

.Enemygd の全コード
extends KinematicBody2D # Added @ Part 4


export var gravity: int
export var speed: int
export var damage: float # Added @ Part 8
var velocity = Vector2()
onready var sprite = $AnimatedSprite
onready var audio_player = $AudioStreamPlayer2D


func _ready():
	set_physics_process(false)


func _on_HitBox_body_entered(body):
	if body.is_in_group("Players"):
		print("Player entered in ", self.name)
		sprite.play("hit")
		audio_player.play() # Added @ Part 13
		yield(sprite, "animation_finished")
		yield(audio_player, "finished") # Added @ Part 13
		queue_free()
		print(self.name, " died")


func _on_VisibilityEnabler2D_screen_entered():
	set_physics_process(true)


func _on_VisibilityEnabler2D_screen_exited():
	set_physics_process(false)

Item.gd の全コード
extends Area2D # Added @ Part 6


export var point = 100
onready var sprite = $AnimatedSprite
onready var label = $Label
onready var anim_player = $AnimationPlayer
onready var audio_player = $AudioStreamPlayer2D # Added @ Part 13


func _ready():
	sprite.modulate = Color(1, 1, 1, 1)
	sprite.position = Vector2.ZERO
	sprite.scale = Vector2.ONE
	label.modulate = Color(1, 1, 1, 0)
	label.rect_position = Vector2(-32, -20)
	label.text = str(point)


func _on_Item_body_entered(body):
	if body.name == "Player":
		print("Player hit Item")
		hit(body) # Modified @ Part 8


func hit(body): # Modified @ Part 8
	print("Got ", point, " point.")
	body.emit_signal("item_hit", point) # Added @ Part 8
	anim_player.play("hit")
	audio_player.play() # Added @ Part 13
	yield(anim_player, "animation_finished")
	yield(audio_player, "finished") # Added @ Part 13
	queue_free()

BrokenBox.gd の全コード
extends Particles2D # Added @ Part 6


onready var audio_player = $AudioStreamPlayer2D


func _ready():
	emitting = true
	audio_player.play()


func _process(delta):
	if not emitting:
		queue_free()
		print("BrokenBox removed.")

StartPoint.gd の全コード
extends Area2D # Added @ Part 7


onready var sprite = $AnimatedSprite
onready var audio_player = $AudioStreamPlayer2D # Added @ Part 13

func _ready():
	sprite.play("idle")


func _on_StartPoint_body_entered(body):
	if body.name == "Player":
		sprite.play("moving")
		audio_player.play() # Added @ Part 13


func _on_StartPoint_body_exited(body):
	if body.name == "Player":
		sprite.play("idle")

Checkpoint.gd の全コード
extends Area2D # Added @ Part 7


var is_checked = false
onready var sprite = $AnimatedSprite
onready var audio_player = $AudioStreamPlayer2D # Added @ Part 13


func _ready():
	sprite.play("no_flag")


func _on_Checkpoint_body_entered(body):
	if body.name == "Player" and not is_checked:
		sprite.play("flag_out")
		audio_player.play() # Added @ Part 13
		yield(sprite, "animation_finished")
		sprite.play("flag_idle")
		is_checked = true

EndPoint.gd の全コード
extends Area2D # Added @ Part 7

onready var particle = $Particles2D
onready var sprite = $AnimatedSprite
onready var polygon = $StaticBody2D/CollisionPolygon2D
onready var anim_player = $AnimationPlayer
onready var audio_player = $AudioStreamPlayer2D # Added @ Part 13


func _ready():
	particle.emitting = false
	sprite.play("idle")
	polygon.position = Vector2(0, -4)


func _on_EndPoint_body_entered(body):
	if body.name == "Player":
		anim_player.play("clear")
		yield(anim_player, "animation_finished")
		particle.emitting = true
		body.anim_player.play("clear")
		audio_player.play() # Added @ Part 13
		yield(body.anim_player, "animation_finished")
		yield(audio_player, "finished") # Added @ Part 13
		print("Moving to the next level!")
		get_parent().queue_free()

GameStart.gd の全コード
extends Control # Added @ Part 10


onready var start_button = $VBoxContainer/StartVBox/StartButton
onready var anim_player = $AnimationPlayer
onready var sfx_player = $SFXPlayer # Added @ Part 13

func _ready():
	start_button.modulate = Color(1, 1, 1, 1)
	start_button.rect_scale = Vector2(1, 1)


func _on_StartButton_button_up():
	#print("_on_StartButton_button_up called.")
	anim_player.play("press_start")
	sfx_player.play() # Added @ Part 13
	yield(anim_player, "animation_finished")
	yield(sfx_player, "finished") # Added @ Part 13
	queue_free()
	print("GameStart scene is now freed.")


func _input(event):
	if event.is_action_released("ui_accept"):
		_on_StartButton_button_up()

CharacterSelect.gd の全コード
extends Control  # Added @ Part 10

signal character_selected(sprite_frames)

var is_choosing = true
var frames_num = 0
var characters = ["Mask Dude", "Ninja Frog", "Pink Man", "Virtual Guy"]

onready var global = get_node("/root/Global")
onready var char_name = $VBoxContainer/CharacterName
onready var l_button = $VBoxContainer/HBoxContainer/LeftButton
onready var r_button = $VBoxContainer/HBoxContainer/RightButton
onready var sprite = $AnimatedSprite
onready var anim_player = $AnimationPlayer
onready var sfx_player = $SFXPlayer # Added @ Part 13
onready var sfx_res = {
	"select": preload("res://Assets/Audio/PressSelect.wav"),
	"enter": preload("res://Assets/Audio/PressEnter.wav")
} # Added @ Part 13

func _ready():
	l_button.modulate = Color(1, 1, 1, 1)
	l_button.rect_scale = Vector2(1, 1)
	r_button.modulate = Color(1, 1, 1, 1)
	r_button.rect_scale = Vector2(1, 1)
	play_animations()


func play_animations():
	var animations: Array = sprite.frames.get_animation_names()
	var anim_index = 0
	var count = 0
	#print("animations array is ", animations)
	
	for anim in animations:
		if anim == "fall" or anim == "jump":
			animations.remove(animations.find(anim))
			#print("removed ", anim, " from animations array.")
	
	while is_choosing:
		sprite.play(animations[anim_index])
		yield(sprite, "animation_finished")
		sprite.stop()
		if count < 4:
			count += 1
			continue	
		count = 0
		if anim_index < animations.size() - 1:
			anim_index += 1
		else:
			anim_index = 0


func _on_LeftButton_pressed():
	anim_player.play("press_left")
	print(sfx_res["select"])
	sfx_player.stream = sfx_res["select"] # Added @ Part 13
	sfx_player.play() # Added @ Part 13
	if frames_num > 0:
		frames_num -= 1
		sprite.frames = global.spriteframes[frames_num]
		char_name.text = characters[frames_num]
	else:
		sprite.frames = global.spriteframes[global.spriteframes.size() - 1]
		char_name.text = characters[characters.size() - 1]
		frames_num = global.spriteframes.size() - 1


func _on_RightButton_pressed():
	anim_player.play("press_right")
	sfx_player.stream = sfx_res["select"] # Added @ Part 13
	sfx_player.play() # Added @ Part 13
	if frames_num < global.spriteframes.size() -1:
		frames_num += 1
		sprite.frames = global.spriteframes[frames_num]
		char_name.text = characters[frames_num]
	else:
		sprite.frames = global.spriteframes[0]
		char_name.text = characters[0]
		frames_num = 0


func _input(event):
	if event.is_action_pressed("ui_left"):
		_on_LeftButton_pressed()
	elif event.is_action_pressed("ui_right"):
		_on_RightButton_pressed()
	elif event.is_action_released("ui_accept"):
		is_choosing = false
		sfx_player.stream = sfx_res["enter"] # Added @ Part 13
		sfx_player.play() # Added @ Part 13
		yield(sfx_player, "finished") # Added @ Part 13
		emit_signal("character_selected", global.spriteframes[frames_num])
		#print("emitted signal: character_selected")
		queue_free()
		print("CharacterSelect scene is now freed.")

GameOver.gd の全コード
extends Control # Added @ Part 9


enum {
	RESTART,
	QUIT,
}
# Added @ Part 9 after
var square_pos = [
	Vector2(108, 128),
	Vector2(212, 128)
]

var selected_option = null
var restart_text = "Do you really want to restart the game?"
var quit_text = "Do you really want to quit the game?"

onready var restart_btn = $VBoxContainer/ButtonsHBox/RestartVBox/RestartButton
onready var quit_btn = $VBoxContainer/ButtonsHBox/QuitVBox/QuitButton
onready var color_rect = $ColorRect
onready var confirmation = $ConfirmationDialog
onready var line2d = $Line2D # Added @ Part 9 after
onready var anim_player = $Line2D/AnimationPlayer # Added @ Part 9 after
onready var sfx_player = $SFXPlayer # Added @ Part 13
onready var sfx_res = {
	"select": preload("res://Assets/Audio/PressSelect.wav"),
	"enter": preload("res://Assets/Audio/PressEnter.wav"),
	"cancel": preload("res://Assets/Audio/PressCancel.wav")
} # Added @ Part 13

func _ready():
	color_rect.visible = false
	anim_player.play("blink_square") # Added @ Part 9 after


func _on_RestartButton_mouse_entered():
	line2d.position = square_pos[0]


func _on_QuitButton_mouse_entered():
	line2d.position = square_pos[1]


func _on_RestartButton_button_up():
	work_at_button_up(RESTART)


func _on_QuitButton_button_up():
	work_at_button_up(QUIT)


func work_at_button_up(option):
	sfx_player.stream = sfx_res["enter"] # Added @ Part 13
	sfx_player.play() # Added @ Part 13
	selected_option = option
	color_rect.visible = true
	if selected_option == RESTART:
		confirmation.dialog_text = restart_text
	elif selected_option == QUIT:
		confirmation.dialog_text = quit_text
	confirmation.popup_centered()


func _on_ConfirmationDialog_confirmed():
	sfx_player.stream = sfx_res["enter"] # Added @ Part 13
	sfx_player.play() # Added @ Part 13
	yield(sfx_player, "finished") # Added @ Part 13
	if selected_option == RESTART:
		get_tree().paused = false
		print("Scene tree paused: ", get_tree().paused)
		get_tree().change_scene("res://Game/Game.tscn")
		print("The game is restarted.")
	elif selected_option == QUIT:
		get_tree().quit()
		print("The game is quited.")


func _on_ConfirmationDialog_popup_hide():
	sfx_player.stream = sfx_res["cancel"] # Added @ Part 13
	sfx_player.play() # Added @ Part 13
	selected_option = null
	confirmation.dialog_text = ""
	color_rect.visible = false


func _input(event): # Added @ Part 9 after
	if visible:
		if event.is_action_released("ui_left"):
			sfx_player.stream = sfx_res["select"] # Added @ Part 13
			sfx_player.play() # Added @ Part 13
			line2d.position = square_pos[0]
		elif event.is_action_released("ui_right"):
			sfx_player.stream = sfx_res["select"] # Added @ Part 13
			sfx_player.play() # Added @ Part 13
			line2d.position = square_pos[1]
		elif event.is_action_released("ui_accept") and not color_rect.visible:
			if line2d.position == square_pos[0]:
				_on_RestartButton_button_up()
			elif line2d.position == square_pos[1]:
				_on_QuitButton_button_up()


おわりに

以上で Part 13 は完了だ。

今回はゲームに BGM や SFX のサウンドを追加した。「AudioStreamPlayer」の基本的なプロパティ、例えば「Stream」、「Volume Db」、「Autoplay」などの編集や、スクリプトでのサウンドリソースの割り当て、サウンドの再生を経験した。

「AudioStreamPlayer」系クラスのノードは、一つのサウンドリソースしか割り当てられない。そのため、最初から複数のノードを追加しておくべきか、スクリプトでリソースを都度切り替えて割り当てるべきか、ちょっと悩ましい。スクリプトが苦手な方は無理をせず、最初はノードを複数追加し、それぞれに異なるサウンドリソースを割り当てる方法で問題ない。今後、ゲーム開発の腕が上がり、一つのシーンで大量のサウンドリソースを扱うようなプロジェクトの開発をすることがあれば、その時はスクリプトでの制御が有効かもしれない。

ところで今回、私自身このチュートリアルのために初めて BGM の作曲を試みた。気に入っていただけたら幸いだ。

さて、次回のチュートリアルでは、キャラクターのアクションをアップデートする予定だ。例えば、ジャンプして落下する時は別のアニメーションを再生したり、2段ジャンプや壁ジャンプをしたり、走っている時は砂埃がまったり、ゴーストエフェクト(残像)を追加したり、といった内容だ。

それでは次回もお楽しみに。

UPDATE:
2022/04/16 画面下落下時にゲームオーバー画面に遷移しない問題の修正のため Game.gd スクリプトのmanage_healthメソッドからyield(player.die_sfx, "finished") の行を削除