4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Godot 4.x】2D脱出ゲームを作ってみる⑪〜BGM・効果音を実装する〜

4
Last updated at Posted at 2026-07-20

はじめに

Claude Code + MCP + Godot 4.xで脱出ゲームを作る連載の第11回です。

前回までにゲームプレイの骨格(謎解き・アイテム管理・シーン遷移)が一通り完成しました。今回はBGMと効果音(SE)を実装し、ゲームとしての手触りを仕上げます。

この記事で作るもの

シーン(タイトル/メインなど)に応じてBGMが自動的に切り替わり、ループ再生される仕組みを作ります。ドアの鍵穴・ダイヤル・郵便受けなど、操作のたびに対応する効果音が鳴る仕組みも実装し、音量バランスを取れるようBGMとSFXを別バスに分離する設定も行います。

前提条件

BGMはGeminiで生成。
効果音は下記サイトから頂戴いたしました。

Claude Code + godot-mcpのセットアップ完了(第2回参照)、scripts/global.gd(オートロード)が作成済み(第8・9・10回参照)という前提で進めます。以下の音声素材も assets/sounds/ に配置済みです。

ファイル 用途
bgm_title.mp3 タイトル画面BGM
bgm_game.mp3 ゲーム中BGM(Main/Door/Post/PostOpen共通)
se_success.mp3 ダイヤル正解時
se_open_door.mp3 ドアが開く
se_use_key.mp3 鍵を使う
se_open_post.mp3 郵便受けを開ける
se_get_item.mp3 アイテム取得
se_reset.mp3 ダイヤルリセット
se_tap_to_start.mp3 スタートタップ
se_dial.mp3 ダイヤル操作

設計方針

BGMと効果音では音の「発生源」の性質が異なります。BGMはシーンをまたいで鳴り続ける状態、効果音はその場のイベントに紐づく単発の音です。この違いに沿って、BGMはAutoload(Global)で一元管理し、効果音は基本的に鳴らす主体(各シーン・コンポーネント)に閉じて配置する、という方針で実装します。ただしアイテム取得音のように複数シーンをまたぐ共通ロジックに紐づく効果音は例外的にAutoload側に置きます。


ファイル構成

assets/sounds/
  bgm_title.mp3        ← BGM(タイトル画面)
  bgm_game.mp3          ← BGM(ゲーム中共通)
  se_success.mp3        ← SE(ダイヤル正解)
  se_open_door.mp3       ← SE(ドアが開く)
  se_use_key.mp3          ← SE(鍵を使う)
  se_open_post.mp3        ← SE(郵便受けを開ける)
  se_get_item.mp3          ← SE(アイテム取得)
  se_reset.mp3              ← SE(ダイヤルリセット)
  se_tap_to_start.mp3        ← SE(スタートタップ)
  se_dial.mp3                 ← SE(ダイヤル操作)
default_bus_layout.tres  ← 今回作成(BGM / SFX バス定義)
scripts/global.gd         ← 変更(BGM自動切り替え・共通SEを追加)

1. BGM/SFXバスを分ける

プロンプト:
BGMとSEの音量を別々に調整できるようにaudio busを分けてください

default_bus_layout.tresBGMSFX の2バスを定義し、Master に送るようにします。

[gd_resource type="AudioBusLayout" format=3]

[resource]
bus/1/name = "BGM"
bus/1/volume_db = -3.098039
bus/1/send = "Master"
bus/2/name = "SFX"
bus/2/volume_db = 0.0
bus/2/send = "Master"

BGMはやや音量を絞り(-3.1dB)、効果音を聞き取りやすくしています。各 AudioStreamPlayerbus プロパティに "BGM""SFX" を指定するだけで、この階層に音声がルーティングされます。バスを分けておくことで、後から「BGMだけ音量を下げたい」といった調整がバス単位で一括反映できます。


2. シーン遷移に連動してBGMを自動再生する

プロンプト:
シーンが切り替わったら、そのシーンに対応するBGMを自動で再生・ループさせてください。既に同じBGMが鳴っていたら再生し直さないようにしてください
タイトルはbgm_title.mp3、メインシーン以降はbgm_game.mp3

BGMはシーンごとに切り替わるため、シーン単体のスクリプトではなくAutoload(Global)で一元管理します。

# scripts/global.gd(抜粋)
const _SCENE_BGM := {
    "Title": "res://assets/sounds/bgm_title.mp3",
    "Main": "res://assets/sounds/bgm_game.mp3",
    "Door": "res://assets/sounds/bgm_game.mp3",
    "Post": "res://assets/sounds/bgm_game.mp3",
    "PostOpen": "res://assets/sounds/bgm_game.mp3",
}

var _bgm_player: AudioStreamPlayer
var _current_bgm_path: String = ""

func _ready() -> void:
    _setup_bgm_player()

func _setup_bgm_player() -> void:
    _bgm_player = AudioStreamPlayer.new()
    _bgm_player.bus = "BGM"
    add_child(_bgm_player)
    _update_bgm(get_tree().current_scene)

func _on_root_child_entered(node: Node) -> void:
    if node.get_parent() != get_tree().root:
        return
    _update_bgm(node)

func _update_bgm(scene: Node) -> void:
    if scene == null:
        return
    var path: String = _SCENE_BGM.get(scene.name, "")
    if path == _current_bgm_path:
        return
    _bgm_player.stop()
    _bgm_player.stream = null
    _current_bgm_path = path
    if path.is_empty():
        return
    var stream := load(path) as AudioStream
    if stream is AudioStreamMP3:
        stream.loop = true
    _bgm_player.stream = stream
    _bgm_player.play()

get_tree().root.child_entered_tree を購読し、ルート直下にシーンが追加されるたびに _update_bgm() を呼んでいます。これは第9回でインベントリバーの表示切り替えに使ったのと同じ仕組みで、change_scene_to_file() によるシーン遷移をこの購読で横取りできます。

_current_bgm_path で直前のBGMパスを覚えておき、同じ曲なら stop()play() し直しません。MainDoor のように同じ bgm_game.mp3 を使うシーン間を移動しても曲が途切れないようにするための工夫です。AudioStreamMP3.loop = true はコードから設定しているため、.import 側でループ設定をしなくても常にループ再生になります。Main/Door/Post/PostOpen 以外(Splash シーンなど)は _SCENE_BGM に登録がなく path が空文字になり、この場合は _bgm_player.stop() のみが実行されて無音状態になります。


3. ワンショットの効果音はシーンに直接ぶら下げる

プロンプト:
ダイヤルを回したらse_dial.mp3を鳴らしてください

BGMと違い、効果音は鳴らす対象のシーン・コンポーネントに閉じているため、Autoloadではなく各 .tscnAudioStreamPlayer を直接置いて管理します。

# scenes/components/dial.tscn(抜粋)
[ext_resource type="AudioStream" path="res://assets/sounds/se_dial.mp3" id="2_se_dial"]

[node name="SeDial" type="AudioStreamPlayer" parent="."]
stream = ExtResource("2_se_dial")
bus = &"SFX"
# scripts/components/dial.gd(抜粋)
func _on_pressed() -> void:
    _digit = (_digit + 1) % 10
    texture_normal = _textures[_digit]
    $SeDial.play()
    digit_changed.emit(_digit)

同じパターンで、他のギミックにも効果音を仕込んでいます。
状況に対して鳴らしたい効果音ファイルを指定する形でプロンプト経由で対応してもらいました。

シーン/コンポーネント ノード名 再生タイミング
dial.tscn SeDial ダイヤルをタップした瞬間
dial_lock.tscn SeReset リセットボタンで数字を0に戻した時
popup.tscn SeSuccess ダイヤルの正解判定が通った時
attraction_popup.tscn SeUseKey 鍵の使用演出を開始した時
attraction_popup.tscn SeOpenDoor 演出終了とともにドアが開いた時
post.tscn SeOpenPost 郵便受けから post_open シーンへ遷移する直前
title.tscn SeTapToStart スタートタップ時

attraction_popup.gd(第10回で実装した演出ポップアップ)のように、1つのコンポーネントで開始と終了それぞれに別の効果音を鳴らすケースもあります。

# scripts/components/attraction_popup.gd(抜粋)
func show_for(duration: float) -> void:
    show()
    $SeUseKey.play()
    modulate.a = 0.0
    var tween := create_tween()
    tween.tween_property(self, "modulate:a", 1.0, 0.2)
    tween.tween_interval(duration)
    tween.tween_property(self, "modulate:a", 0.0, 0.2)
    tween.tween_callback(func(): hide(); $SeOpenDoor.play(); closed.emit())

フェードアウト完了と同時に「ドアが開く音」を鳴らすため、tween_callback の中で $SeOpenDoor.play() を呼んでいます。第3回・第8回でも使ってきた「Tweenチェーンの最後にコールバックを繋ぐ」パターンが、効果音の再生タイミング制御にもそのまま活用できています。


4. アイテム取得音だけはGlobal経由で鳴らす

プロンプト:
アイテムを取得したらどのシーンでもse_get_item.mp3を鳴らしてください

アイテム取得はどのシーンからでも起こり得ます(add_item() は第9回で実装したインベントリを扱う共通ロジックです)。そのためこちらは例外的に Global 側に AudioStreamPlayer を持たせます。

# scripts/global.gd(抜粋)
var _se_get_item_player: AudioStreamPlayer

func _setup_se_player() -> void:
    _se_get_item_player = AudioStreamPlayer.new()
    _se_get_item_player.bus = "SFX"
    _se_get_item_player.stream = preload("res://assets/sounds/se_get_item.mp3")
    add_child(_se_get_item_player)

func add_item(item: ItemData) -> void:
    if has_item(item.id):
        return
    _items.append(item)
    _se_get_item_player.play()
    item_added.emit(item)
    _save()

効果音の発生源が単一シーンに閉じているか、複数シーンをまたぐ共通ロジックかが、Autoloadに置くかシーン側に置くかの判断基準になります。add_item()door.gd からも post_open.gd からも呼ばれる共通処理なので、呼び出し元それぞれに効果音再生を書くのではなく、ロジックの中心である Global 側に1箇所だけ実装するのが自然です。


動作確認

ゲームを実行し、タイトル画面で bgm_title.mp3 がループ再生され、「Tap to Start」でメイン画面に遷移すると bgm_game.mp3 に切り替わることを確認します。main.tscndoor.tscnmain.tscn と行き来しても曲が途切れず鳴り続け、ダイヤルをタップすると se_dial.mp3 が鳴り、正解判定が通ると se_success.mp3 が鳴れば順調です。鍵穴に鍵を使うと se_use_key.mp3 が鳴り、演出終了と同時に se_open_door.mp3 が鳴ります。どのシーンでアイテムを取得しても se_get_item.mp3 が鳴れば、一通りの動作確認は完了です。

BGMが鳴らない場合は default_bus_layout.tresBGM バスが Master に送られているか、_bgm_player.bus"BGM" になっているかを確認してください。シーン間移動のたびに曲が途切れる場合は _current_bgm_path の比較が正しく機能しているか、同じ bgm_game.mp3 を指すシーン名がすべて _SCENE_BGM に登録されているかを見ます。BGMがループしない場合は stream is AudioStreamMP3 の判定を通っているか、stream.loop = true が再生前に設定されているかを確認してください。効果音が鳴らない場合は対象ノード($SeDial など)がシーンに配置されているか、bus"SFX" になっているかを見ます。アイテム取得音だけ鳴らない場合は Global._setup_se_player()_ready() から呼ばれているかを確認してください。


成果物

BGM再生、各所で効果音が再生されます!

まとめ

今回はClaude Code + godot-mcpを使って以下を実装しました。

やりたいこと 実装場所 方法
BGMの音量だけ下げたい default_bus_layout.tres バスを分けて volume_db を個別設定
シーン遷移でBGMを自動切り替え global.gd(Autoload) child_entered_tree を監視し辞書から再生パスを解決
同じBGMのシーン間移動で途切れさせない global.gd(Autoload) _current_bgm_path で直前の曲と比較
ギミック固有の効果音 .tscn + 対応スクリプト AudioStreamPlayer をシーンに直接配置し $Node.play()
シーン横断で鳴る効果音(アイテム取得等) global.gd(Autoload) 共通ロジック内に AudioStreamPlayer を保持

BGMは「シーンに紐づく状態」としてグローバルに一元管理し、効果音は「鳴らす主体に閉じたイベント」としてローカルに配置する、という使い分けが、Godotでオーディオを破綻なく管理するコツです。この判断基準は、今後新しいギミックに音を足していく際にも一貫して使えます。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?