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 エフェクトを挿す
- メニューから Project > Project Settings… を開く
- Audio > Bus Layout を開く
- 例として
Environmentというバスを追加 Environmentバスを選択し、Add Effect から Reverb を追加- ゲームに合わせて Reverb のパラメータ(Room Size, Decay Time など)を調整
ここまでで、「Environment バスに Reverb が挿さった状態」になります。
手順②: プレイヤーをグループに登録
ReverbZone は「only_for_player = true」のとき、指定グループのノードだけに反応します。
プレイヤーを player グループに登録しておきましょう。
Player (CharacterBody3D) ├── Camera3D ├── CollisionShape3D └── ...(その他コンポーネント)
- Player ノードを選択
- インスペクタ横の「Node」タブ → 「Groups」
playerと入力して「Add」ボタンを押す
これで、Player は player グループに属するようになります。
手順③: 洞窟シーンに ReverbZone を置く
洞窟の入口や内部に、ReverbZone を配置します。
Cave (Node3D)
├── MeshInstance3D
├── CollisionShape3D
└── ReverbZone (Area3D)
└── CollisionShape3D # エリアの範囲
Caveシーンの子として Area3D を追加し、ReverbZone.gd をアタッチする- CollisionShape3D を子に追加し、洞窟内部の形に合わせてスケールする
ReverbZone のインスペクタ設定例:
bus_name:Environmenteffect_index:0(Environment バスの一番上のスロットに Reverb を挿している場合)fade_in_time: 0.5(洞窟に入って0.5秒かけてリバーブを強くする)fade_out_time: 0.5(洞窟から出たら0.5秒でリバーブを弱くする)only_for_player: ONplayer_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 を育てていきましょう。
