Godot 4で環境音をちゃんと作り込もうとすると、AudioStreamPlayerをシーンごとに置いて、エリアに入ったらスクリプトでボリュームをいじって…と、だんだんスクリプトが肥大化してきますよね。
さらに、洞窟・水中・屋外など、場所によってリバーブやローパスを切り替えたいとなると、Area3DやArea2Dのシグナルに直接ゴリゴリ書いてしまいがちです。
その結果:
- プレイヤーのスクリプトが「環境音制御」まで抱え込んで巨大化
- 敵やギミックにも同じような処理をコピペしてしまう
- 「このエリアに入ったらリバーブを変える」ロジックがあちこちに散らばる
こういう「継承+肥大化スクリプト」スタイルから卒業するために、環境音の制御だけを担当するコンポーネントとして切り出したのが、今回の AmbientFader コンポーネントです。
ノード階層をムダに深くせず、「エリアにポン付けするだけで環境音をフェード制御」できるようにしていきましょう。
【Godot 4】入るだけで環境が“しっとり”変わる!「AmbientFader」コンポーネント
AmbientFaderは、エリアに入った/出たタイミングで:
- 指定したバスのボリューム(dB)をフェード
- 指定したバスのリバーブ(Send量)をフェード
するためのコンポーネントです。
「洞窟エリア」「水中エリア」「街中エリア」などにアタッチしておき、プレイヤーがそのエリアに入ると自動で環境音が切り替わる、という使い方を想定しています。
フルコード:AmbientFader.gd
extends Area3D
class_name AmbientFader
## 環境音用のバスやリバーブを、エリアに入ったときに
## なめらかにフェードさせるためのコンポーネント。
##
## - このノード自体は Area3D として振る舞い、
## 体(たとえばプレイヤー)が入退場すると AudioServer を操作します。
## - 継承ではなく「エリアにポン付け」して使う前提です。
@export_category("ターゲット設定")
## プレイヤーなど「トリガーになるボディ」のグループ名。
## 例: "player" にしておき、Player を "player" グループに入れておく。
@export var trigger_group: StringName = &"player"
## フェード対象とするオーディオバス名。
## 例: "Environment" や "SFX" など、プロジェクト設定のバス名。
@export var target_bus_name: StringName = &"Environment"
## バスの Send 先(リバーブ用)のバス名。
## 例: "Reverb" など。空文字のままなら Send は変更しない。
@export var reverb_bus_name: StringName = &"Reverb"
@export_category("ボリューム設定 (dB)")
## エリアに入っていないときの基準ボリューム(dB)。
## 例: 0.0 が通常、-6.0 で少し小さめ。
@export var default_volume_db: float = 0.0
## エリアに入ったときに目指すボリューム(dB)。
## 例: 洞窟でこもらせたいなら -4 ~ -8 あたり。
@export var inside_volume_db: float = -3.0
@export_category("リバーブ Send 設定")
## エリア外でのリバーブ Send レベル [0.0 - 1.0]。
## 0.0 ならリバーブ無し、1.0 ならフル Send として扱う想定。
@export_range(0.0, 1.0, 0.01)
@export var default_reverb_send: float = 0.0
## エリア内でのリバーブ Send レベル [0.0 - 1.0]。
## 洞窟などは 0.5~1.0、水中なら 0.7~1.0 など。
@export_range(0.0, 1.0, 0.01)
@export var inside_reverb_send: float = 0.7
@export_category("フェード設定")
## フェードにかける時間(秒)。
## 0.0 にすると即時切り替えになります。
@export_range(0.0, 10.0, 0.05)
@export var fade_time: float = 1.5
## フェードの補間カーブ。0.0=線形, 1.0=イーズイン, -1.0=イーズアウト。
@export_range(-1.0, 1.0, 0.05)
@export var ease: float = 0.0
## エリアから出たときに、元の値に戻すかどうか。
@export var revert_on_exit: bool = true
## デバッグ用: エディタ上で現在値をインスペクタに表示するためのダミー。
@export_category("デバッグ")
@export var debug_log: bool = false
# 内部状態
var _bus_index: int = -1
var _reverb_bus_index: int = -1
var _current_tween: Tween
var _is_inside: bool = false
func _ready() -> void:
# バス名からインデックスを解決
_bus_index = AudioServer.get_bus_index(target_bus_name)
if _bus_index == -1:
push_warning("AmbientFader: バス '%s' が見つかりません。プロジェクト設定の Audio バス名を確認してください。" % target_bus_name)
if reverb_bus_name != StringName():
_reverb_bus_index = AudioServer.get_bus_index(reverb_bus_name)
if _reverb_bus_index == -1:
push_warning("AmbientFader: リバーブバス '%s' が見つかりません。" % reverb_bus_name)
# 初期値をセット(シーン開始時の環境状態)
_set_bus_volume_db(default_volume_db)
_set_bus_send(default_reverb_send)
# シグナル接続
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node3D) -> void:
if not _is_trigger_body(body):
return
_is_inside = true
if debug_log:
print("AmbientFader: body entered: ", body.name)
_fade_to(inside_volume_db, inside_reverb_send)
func _on_body_exited(body: Node3D) -> void:
if not _is_trigger_body(body):
return
_is_inside = false
if not revert_on_exit:
return
if debug_log:
print("AmbientFader: body exited: ", body.name)
_fade_to(default_volume_db, default_reverb_send)
func _is_trigger_body(body: Node) -> bool:
# 指定グループに属しているかどうかで判定
if trigger_group == StringName():
# グループ指定なしなら、何でもトリガーにしてしまう
return true
return body.is_in_group(trigger_group)
func _fade_to(target_volume_db: float, target_reverb_send: float) -> void:
# すでにフェード中ならキャンセル
if _current_tween and _current_tween.is_valid():
_current_tween.kill()
if fade_time <= 0.0:
# 即時反映
_set_bus_volume_db(target_volume_db)
_set_bus_send(target_reverb_send)
return
# 現在値を取得
var start_volume_db := _get_bus_volume_db()
var start_send := _get_bus_send()
_current_tween = create_tween()
_current_tween.set_parallel(true)
# ボリュームの Tween
_current_tween.tween_method(
func(v):
_set_bus_volume_db(v),
start_volume_db,
target_volume_db,
fade_time
).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
# Send の Tween(リバーブなど)
if _reverb_bus_index != -1:
_current_tween.tween_method(
func(v):
_set_bus_send(v),
start_send,
target_reverb_send,
fade_time
).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_SINE)
func _set_bus_volume_db(db: float) -> void:
if _bus_index == -1:
return
AudioServer.set_bus_volume_db(_bus_index, db)
func _get_bus_volume_db() -> float:
if _bus_index == -1:
return 0.0
return AudioServer.get_bus_volume_db(_bus_index)
func _set_bus_send(amount: float) -> void:
# Reverb バスが設定されていない場合は何もしない
if _bus_index == -1 or _reverb_bus_index == -1:
return
# AudioServer には「Send レベル」を直接セットする API はないので、
# バスの Send を「オン/オフ+量」に見立てて扱う方針にする。
#
# ここでは簡易的に、「amount > 0 なら Send 先を reverb_bus_name に設定」
# という運用を想定し、Send レベルは別途 Reverb バス側のエフェクトで調整します。
#
# より細かく制御したい場合は、個別の AudioStreamPlayer の bus_send を
# 直接いじる方式に拡張してください(応用編で後述)。
if amount > 0.0:
AudioServer.set_bus_send(_bus_index, reverb_bus_name)
else:
# 空文字にすると Send 無しになる
AudioServer.set_bus_send(_bus_index, StringName())
func _get_bus_send() -> float:
# 簡易的に「Send が設定されているかどうか」で 0 or 1 を返す
if _bus_index == -1:
return 0.0
var current_send := AudioServer.get_bus_send(_bus_index)
if current_send == reverb_bus_name:
return 1.0
return 0.0
※Godot 4 の AudioServer API は「バスの Send レベル」を直接数値で変える仕組みがないため、ここでは「Send 先の有無」を 0/1 とみなす簡易実装にしています。
「水中に入ったら Reverb バスに Send する/出たら切る」といった使い方にはこれで十分です。
使い方の手順
① Audio バスを用意する
- メニューから
Project > Project Settings... > Audio > Busesを開く。 - Environment というバスを追加し、BGM や環境音プレイヤーの
busをこのバスに設定。 - Reverb というバスを追加し、
EffectにReverbEffectを追加。 EnvironmentバスのSendを空にしておく(スクリプトから切り替えます)。
② AmbientFader コンポーネントをシーンに追加
洞窟エリアの例(3Dの場合):
CaveArea (Node3D)
├── MeshInstance3D
└── AmbientFader (Area3D) ← このコンポーネント
└── CollisionShape3D ← エリア範囲
プレイヤー側のシーン例:
Player (CharacterBody3D) ├── MeshInstance3D ├── CollisionShape3D └── AudioListener3D
AmbientFader のインスペクタ設定例:
trigger_group="player"(Player ノードをplayerグループに入れておく)target_bus_name="Environment"reverb_bus_name="Reverb"default_volume_db=0.0inside_volume_db=-4.0(洞窟で少し抑える)default_reverb_send=0.0inside_reverb_send=1.0(洞窟でリバーブON)fade_time=1.5(1.5秒かけてフェード)
③ プレイヤーにグループを設定
プレイヤーシーンを開いて、Root ノード(例: Player)を選択し、
インスペクタの「Node」タブ →「Groups」から player グループを追加します。
シーン構成図(プレイヤー+環境音プレイヤー):
World (Node3D)
├── Player (CharacterBody3D)
│ ├── MeshInstance3D
│ ├── CollisionShape3D
│ └── AudioListener3D
├── BGM_Ambient (AudioStreamPlayer)
│ └── (bus = "Environment")
└── CaveArea (Node3D)
├── CaveMesh (MeshInstance3D)
└── AmbientFader (Area3D)
└── CollisionShape3D
この状態でゲームを再生すると:
- プレイヤーが洞窟エリアに入る →
Environmentバスのボリュームが-4dBにフェードし、Reverbバスへ Send される - 出ると → 元のボリュームと Send 状態に戻る
④ 具体的な使用例
例1: 水中エリア
水中に入ったときに、音をこもらせてリバーブを強める例です。
WaterArea (Node3D)
├── WaterMesh (MeshInstance3D)
└── AmbientFader (Area3D)
└── CollisionShape3D
WaterArea/AmbientFader の設定例:
target_bus_name="Environment"reverb_bus_name="Reverb"default_volume_db=0.0inside_volume_db=-6.0(水の中でかなり減衰)default_reverb_send=0.0inside_reverb_send=1.0fade_time=0.8(やや素早く切り替え)
例2: 街中エリア(逆にリバーブを切る)
城下町に入ったら、屋外らしくリバーブを少なめにする例。
TownArea (Node3D)
├── TownMesh (MeshInstance3D)
└── AmbientFader (Area3D)
└── CollisionShape3D
TownArea/AmbientFader の設定例:
default_volume_db=-2.0(屋外BGM 少し小さめ)inside_volume_db=0.0(街中で音が近くなるイメージ)default_reverb_send=0.5(屋外の残響)inside_reverb_send=0.0(街中は残響少なめ)
メリットと応用
AmbientFader を使うメリットは、何よりも 責務がきれいに分離される ことです。
- プレイヤーのスクリプトは「移動・入力・アニメーション」に集中できる
- 「このエリアに入ったらこういう音響にする」というルールを、エリア側のシーンに閉じ込められる
- 洞窟、水中、教会、ダンジョンなど、エリアをコピペするだけで同じ音響ルールを再利用できる
- ノード構成は「Area + AmbientFader」という薄いレイヤーだけで済み、深い継承ツリーや巨大スクリプトを避けられる
コンポーネント指向で考えると、「環境音制御」はプレイヤーやBGMプレイヤーの属性ではなく、『空間(エリア)が持つ性質』ですよね。
その性質を AmbientFader という独立コンポーネントに切り出しておくことで、レベルデザイン時に「ここは残響強め」「ここは水中っぽく」といった調整を、シーンエディタ上のパラメータいじりだけで完結させられます。
改造案:特定の AudioStreamPlayer だけをフェードする
より細かく制御したい場合、「バス全体」ではなく「特定の AudioStreamPlayer だけ」のボリュームを変えたくなることがあります。
その場合は、AmbientFader を少し改造して、ターゲットノードを直接参照する方式に変えると柔軟になります。
例:子ノードの AudioStreamPlayer3D だけをフェードする関数
@export_category("プレイヤー直接制御モード")
@export var target_player_path: NodePath
func _fade_player_volume_to(target_db: float) -> void:
var player := get_node_or_null(target_player_path) as AudioStreamPlayer3D
if not player:
push_warning("AmbientFader: target_player_path が不正です。")
return
var start_db := linear_to_db(player.volume_db) if player.volume_db != 0 else 0.0
var tween := create_tween()
tween.tween_property(player, "volume_db", target_db, fade_time) \
.set_ease(Tween.EASE_IN_OUT) \
.set_trans(Tween.TRANS_SINE)
このように、「バス全体をいじる版」「個別プレイヤーをいじる版」などを用途に応じて作り分けておくと、プロジェクト全体の音響設計がかなり整理されます。
大事なのは、どちらのパターンでも「環境音制御は AmbientFader コンポーネントに閉じ込める」という構造を崩さないことですね。
継承より合成で、音響まわりもすっきり保守しやすくしていきましょう。
