Godotで3D/2Dゲームを作っていると、「洞窟に入ったら声が反響してほしい」「ホールに入ったら残響を強めたい」といった、空間ごとのサウンド演出をしたくなりますよね。
でも素直にやろうとすると…

  • シーンごとに AudioBusLayout を分ける? → 管理が地獄
  • プレイヤーに直接「洞窟用」「ホール用」の処理を書き足す? → プレイヤースクリプトが太りがち
  • エリアごとに専用プレイヤー処理を継承して作る? → 継承ツリーが増えて破綻しがち

このあたり、Godot標準の「ノード継承+深いツリー」で頑張ろうとすると、後から仕様変更が入ったときにかなりつらくなります。
そこで今回は「エリアに入ったら指定のAudioBusにリバーブを自動でオン/オフするだけのコンポーネント」として、ReverbZone を用意しておきましょう。

プレイヤーや敵、動く乗り物など、Area3D / Area2D にこのコンポーネントをペタッと貼るだけで、「この範囲にいる間だけリバーブを強める」といった演出ができるようになります。
継承ではなく「合成(Composition)」でサウンド演出を組み立てていくスタイルですね。

【Godot 4】入るだけで残響が変わる!「ReverbZone」コンポーネント

以下は 3D/2D どちらでも使えるようにした、AudioBus のエフェクトスロットを自動でオン/オフする ReverbZone のフルコードです。

フルコード(GDScript)


extends Area3D
class_name ReverbZone
"""
ReverbZone (残響エリア) コンポーネント
-----------------------------------
プレイヤーなどがこの Area3D に入っている間だけ、
指定した AudioBus のエフェクトスロットを ON にするコンポーネントです。

・洞窟エリア
・ホール / 大聖堂
・屋内 / 屋外の切り替え

など、「この範囲にいる時だけリバーブを強めたい」ケースで使えます。

注意:
- 実際のリバーブの種類やパラメータは AudioBusLayout 側で設定しておきます。
- このコンポーネントは「ON/OFF」と「フェード時間」を制御する役割に特化しています。
"""

@export_category("Target Bus / Effect")
@export var bus_name: StringName = &"Master"
## 対象にする AudioBus の名前。
## Project Settings > Audio > Bus Layout で定義したバス名を指定します。
## 例: "Master", "SFX", "Environment" など。

@export_range(0, 15, 1)
@export var effect_index: int = 0
## 上記バスの「どのエフェクトスロット」を制御するか。
## 0 が一番上のスロット。
## ここに Reverb エフェクトを挿しておきましょう。

@export_category("Fade Settings")
@export_range(0.0, 5.0, 0.05)
@export var fade_in_time: float = 0.5
## エリアに入ったとき、エフェクトが完全に有効になるまでの時間(秒)。
## 0 にすると即時 ON になります。

@export_range(0.0, 5.0, 0.05)
@export var fade_out_time: float = 0.5
## エリアから出たとき、エフェクトが完全に無効になるまでの時間(秒)。
## 0 にすると即時 OFF になります。

@export_category("Filter")
@export var only_for_player: bool = true
## true の場合、「プレイヤー」だけに反応させたいときに使います。
## プレイヤー側に "player" グループをつけておき、このフラグを ON にすると、
## "player" グループのノードだけがトリガー対象になります。

@export var player_group_name: StringName = &"player"
## プレイヤーとして扱うグループ名。
## only_for_player = true のときのみ使用されます。

@export_category("Debug")
@export var debug_print: bool = false
## デバッグ用ログ出力の ON/OFF。

# 内部状態管理用
var _bus_index: int = -1
var _current_weight: float = 0.0
var _target_weight: float = 0.0
var _fade_time: float = 0.0
var _fade_elapsed: float = 0.0
var _inside_bodies: int = 0  # エリア内にいる対象数

func _ready() -> void:
    # Area3D のボディ検知シグナルを接続
    body_entered.connect(_on_body_entered)
    body_exited.connect(_on_body_exited)

    # バスインデックスを取得
    _bus_index = AudioServer.get_bus_index(bus_name)
    if _bus_index == -1:
        push_warning("ReverbZone: Audio bus '%s' not found. Check your Bus Layout." % bus_name)
        return

    # 初期状態ではエフェクトを OFF にしておく(weight = 0)
    _set_effect_weight(0.0, immediate=true)

    if debug_print:
        print("ReverbZone ready on bus '%s' (index=%d, effect_index=%d)" % [bus_name, _bus_index, effect_index])


func _process(delta: float) -> void:
    # フェード処理
    if _fade_time > 0.0 and _fade_elapsed < _fade_time:
        _fade_elapsed += delta
        var t := clamp(_fade_elapsed / _fade_time, 0.0, 1.0)
        var new_weight := lerp(_current_weight, _target_weight, t)
        _set_effect_weight(new_weight, immediate=true)

        if _fade_elapsed >= _fade_time:
            # フェード完了時に最終値を保証
            _set_effect_weight(_target_weight, immediate=true)


func _on_body_entered(body: Node) -> void:
    if not _is_valid_trigger(body):
        return

    _inside_bodies += 1
    if debug_print:
        print("ReverbZone: body entered (%s), inside count = %d" % [body.name, _inside_bodies])

    # 最初の1体が入ったときだけ ON フェードを開始
    if _inside_bodies == 1:
        _start_fade(to_weight=1.0, duration=fade_in_time)


func _on_body_exited(body: Node) -> void:
    if not _is_valid_trigger(body):
        return

    _inside_bodies = max(0, _inside_bodies - 1)
    if debug_print:
        print("ReverbZone: body exited (%s), inside count = %d" % [body.name, _inside_bodies])

    # すべていなくなったら OFF フェードを開始
    if _inside_bodies == 0:
        _start_fade(to_weight=0.0, duration=fade_out_time)


func _is_valid_trigger(body: Node) -> bool:
    """
    このノードが ReverbZone をトリガーしてよい対象かどうかを判定します。
    - only_for_player = false の場合: すべての PhysicsBody3D を対象
    - only_for_player = true の場合: 指定グループ (player_group_name) に属するノードのみ対象
    """
    if only_for_player:
        return body.is_in_group(player_group_name)
    # ざっくりと PhysicsBody3D だけに限定しておく
    return body is PhysicsBody3D


func _start_fade(to_weight: float, duration: float) -> void:
    """
    エフェクトのウェイトを指定値に向けてフェードさせます。
    duration が 0 の場合は即時変更。
    """
    # バスが見つからなかった場合は何もしない
    if _bus_index == -1:
        return

    _current_weight = _get_effect_weight()
    _target_weight = clamp(to_weight, 0.0, 1.0)

    if duration <= 0.0:
        _set_effect_weight(_target_weight, immediate=true)
        _fade_time = 0.0
        _fade_elapsed = 0.0
    else:
        _fade_time = duration
        _fade_elapsed = 0.0


func _set_effect_weight(weight: float, immediate: bool = false) -> void:
    """
    AudioBus の指定エフェクトスロットの「ウェイト」を設定します。
    Godot の標準 Reverb エフェクトは「Wet」と「Dry」のバランスで
    実質的なオン/オフ感が決まるので、ここでは weight を 0〜1 で扱います。

    実装としては:
    - 有効/無効フラグ: off にすると完全にバイパスされる
    - Wet レベル: weight に応じて 0〜1 の範囲でスケール
    など、プロジェクトに合わせて調整してもOKです。
    """
    if _bus_index == -1:
        return

    # エフェクトが存在するかチェック
    if effect_index < 0 or effect_index >= AudioServer.get_bus_effect_count(_bus_index):
        push_warning("ReverbZone: effect_index %d is out of range on bus '%s'." % [effect_index, bus_name])
        return

    # 有効/無効の切り替え (0 なら OFF, それ以外は ON)
    AudioServer.set_bus_effect_enabled(_bus_index, effect_index, weight > 0.01)

    # ReverbEffect の場合、"wet" プロパティを weight に合わせて変更する例
    var effect := AudioServer.get_bus_effect(_bus_index, effect_index)
    if effect == null:
        return

    # ReverbEffect のときだけ wet を変更する(他のエフェクトでも似たように応用可能)
    if effect is AudioEffectReverb:
        var reverb := effect as AudioEffectReverb
        # wet を 0〜1 にクランプして設定
        reverb.wet = clamp(weight, 0.0, 1.0)

    if immediate:
        _current_weight = weight


func _get_effect_weight() -> float:
    """
    現在の Reverb の「wet」値を取得します。
    Reverb 以外のエフェクトが挿さっている場合は 0 を返します。
    """
    if _bus_index == -1:
        return 0.0
    if effect_index < 0 or effect_index >= AudioServer.get_bus_effect_count(_bus_index):
        return 0.0

    var effect := AudioServer.get_bus_effect(_bus_index, effect_index)
    if effect is AudioEffectReverb:
        return (effect as AudioEffectReverb).wet
    return 0.0

使い方の手順

ここからは、実際に「洞窟に入ったらリバーブを強くする」例で使い方を見ていきましょう。

手順①: AudioBus に Reverb エフェクトを挿す

  1. メニューから Project > Project Settings… を開く
  2. Audio > Bus Layout を開く
  3. 例として Environment というバスを追加
  4. Environment バスを選択し、Add Effect から Reverb を追加
  5. ゲームに合わせて Reverb のパラメータ(Room Size, Decay Time など)を調整

ここまでで、「Environment バスに Reverb が挿さった状態」になります。

手順②: プレイヤーをグループに登録

ReverbZone は「only_for_player = true」のとき、指定グループのノードだけに反応します。
プレイヤーを player グループに登録しておきましょう。

Player (CharacterBody3D)
 ├── Camera3D
 ├── CollisionShape3D
 └── ...(その他コンポーネント)
  1. Player ノードを選択
  2. インスペクタ横の「Node」タブ → 「Groups」
  3. player と入力して「Add」ボタンを押す

これで、Playerplayer グループに属するようになります。

手順③: 洞窟シーンに ReverbZone を置く

洞窟の入口や内部に、ReverbZone を配置します。

Cave (Node3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 └── ReverbZone (Area3D)
      └── CollisionShape3D   # エリアの範囲
  • Cave シーンの子として Area3D を追加し、ReverbZone.gd をアタッチする
  • CollisionShape3D を子に追加し、洞窟内部の形に合わせてスケールする

ReverbZone のインスペクタ設定例:

  • bus_name : Environment
  • effect_index : 0(Environment バスの一番上のスロットに Reverb を挿している場合)
  • fade_in_time : 0.5(洞窟に入って0.5秒かけてリバーブを強くする)
  • fade_out_time : 0.5(洞窟から出たら0.5秒でリバーブを弱くする)
  • only_for_player : ON
  • player_group_name : player

これで、プレイヤーが洞窟エリアに入ると、Environment バスの Reverb がフェードインし、
出るとフェードアウトするようになります。

手順④: 敵や動く床にも簡単に応用

コンポーネント指向の良さは、「同じ振る舞いをそのまま別のシーンにペタッと貼れる」ことです。
例えば、ホールを移動する「動く足場」に ReverbZone を付けると、
足場の上に乗っている間だけ残響が強くなる、といった演出もできます。

MovingPlatform (Node3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 ├── AnimationPlayer
 └── ReverbZone (Area3D)
      └── CollisionShape3D   # 足場の上だけをカバーするように設定

敵キャラが「自分の周囲だけ空間が歪んでいる」ような演出をしたければ、敵に ReverbZone を付けてもOKです。

EnemyMage (CharacterBody3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 ├── AnimationTree
 ├── HealthComponent (Node)
 └── ReverbZone (Area3D)
      └── CollisionShape3D

このように、「残響演出」はプレイヤーや敵の継承ツリーに縛られず、
ReverbZone という独立コンポーネントとして、どのシーンにも再利用できます。


メリットと応用

  • シーン構造がスッキリ
    「洞窟プレイヤー」「ホールプレイヤー」といった継承の枝分かれを作らず、
    どこにでも貼れる ReverbZone コンポーネントとして切り出すことで、
    プレイヤーや敵のスクリプトをシンプルに保てます。
  • レベルデザイナーが触りやすい
    音の演出をいじりたい人は、シーン上で ReverbZone の CollisionShape3D を動かすだけ。
    スクリプトを触らなくても「ここからここまで洞窟感を出す」といった調整ができます。
  • Bus と Effect の設計をきれいに分離
    「どんな Reverb を使うか」は BusLayout 側の仕事。
    「いつ ON/OFF するか」は ReverbZone 側の仕事。
    役割がはっきり分かれるので、後から「リバーブを別の種類に差し替える」などの変更も楽です。
  • 複数ゾーンの組み合わせも簡単
    洞窟の入口に弱めの ReverbZone、奥に強めの ReverbZone を重ねることで、
    「入るほど残響が強くなる」ような表現も作れます(その場合はロジックを少し拡張するとより自然になります)。

改造案: 距離に応じて自動フェードする ReverbZone

「エリア内にいる間は常に 1.0」ではなく、
プレイヤーとエリア中心の距離に応じて Reverb の強さを変えたい場合の簡単な改造例です。

以下のような関数を追加し、_process から呼び出すことで、
エリア中心から離れるほど Reverb が弱まるような表現ができます。


func _update_weight_by_distance(player: Node3D, max_radius: float) -> void:
    """
    プレイヤーとの距離に応じて Reverb のウェイトを変化させる例。
    - max_radius: この距離以上では weight = 0
    - 中心(距離0)では weight = 1
    """
    if _bus_index == -1:
        return

    var distance := global_position.distance_to(player.global_position)
    var t := clamp(1.0 - (distance / max_radius), 0.0, 1.0)
    _set_effect_weight(t, immediate=true)

例えば、_process 内でプレイヤーを取得して呼び出せば、
「洞窟の中心に近づくほど残響が強くなる」といった、よりリッチな演出も簡単に実現できます。

このように、ReverbZone を「コンポーネント」として切り出しておけば、
振る舞いの拡張や差し替えも、継承ツリーをいじらずにサクッとできます。
ぜひ、自分のプロジェクト用にカスタマイズした ReverbZone を育てていきましょう。