Unityを触り始めた頃は、つい Update() に「プレイヤーの入力処理」「移動」「UI更新」「サウンド制御」など、ありとあらゆる処理を書いてしまいがちですよね。
とくにサウンド周りは「とりあえずBGMと環境音を鳴らすだけだから」と軽く見られがちで、ステージごとの環境音やリバーブ調整を全部1つの巨大スクリプトで管理してしまうケースも多いです。

しかし、そのやり方だと…

  • 洞窟・水中・屋外など、環境ごとに条件分岐が増えてコードが肥大化
  • シーンやステージを追加するたびに巨大スクリプトを修正する必要がある
  • サウンド担当とレベルデザイナーが、1つのスクリプトに依存して作業しづらい

といった問題が一気に噴き出してきます。
そこでこの記事では、「環境ごとの音量・リバーブ設定だけ」を担当する小さなコンポーネント AmbientFader を作り、洞窟や水中に入ったときの環境音の変化を、コンポーネント指向でスッキリ管理する方法を紹介します。

【Unity】トリガーに入るだけで環境音が“空気”ごと変わる!「AmbientFader」コンポーネント

今回の AmbientFader は、

  • プレイヤーがトリガーに入ったら、環境用AudioSourceの音量・リバーブをターゲット値へフェード
  • トリガーから出たら、元の設定にゆっくり戻す
  • 複数のエリア(洞窟・水中・建物内など)ごとに別々の設定をプレハブ化して使い回し

といった用途を想定した、環境音専用の小さなコンポーネントです。
巨大な「サウンド管理クラス」に頼るのではなく、エリアごとに「環境音のキャラ付け」をコンポーネントとして貼っていくイメージですね。

フルコード:AmbientFader.cs


using UnityEngine;
using UnityEngine.Audio;

[RequireComponent(typeof(Collider))]
public class AmbientFader : MonoBehaviour
{
    // === 参照設定 ===

    [Header("対象の環境音 AudioSource")]
    [SerializeField]
    private AudioSource targetAmbientSource;
    // 例: 風の音、環境ループ音などを再生しているAudioSource

    [Header("リバーブ用 AudioMixerGroup (任意)")]
    [SerializeField]
    private AudioMixerGroup reverbMixerGroup;
    // AudioSource.outputAudioMixerGroup に設定しておくと、
    // AudioMixer側のReverbパラメータと連携しやすくなります。

    [Header("AudioMixer (任意)")]
    [SerializeField]
    private AudioMixer audioMixer;
    // Reverbなどのパラメータを操作するAudioMixer。
    // 未設定ならリバーブのフェードは無効になります。

    [Header("AudioMixer の Reverb パラメータ名")]
    [SerializeField]
    private string reverbParameterName = "ReverbLevel";
    // AudioMixer内でExposeしているパラメータ名を指定します。
    // 例: "ReverbLevel"

    // === フェード設定 ===

    [Header("音量設定")]
    [SerializeField, Range(0f, 1f)]
    private float targetVolume = 0.5f; // トリガー内での音量
    [SerializeField, Range(0f, 1f)]
    private float fadeDuration = 1.0f; // 音量フェードにかかる時間(秒)

    [Header("Reverb設定 (dB)")]
    [SerializeField]
    private float targetReverbDb = 0f; // トリガー内でのReverb量 (dB)
    [SerializeField]
    private float fadeReverbDuration = 1.0f; // Reverbフェードにかかる時間(秒)

    [Header("プレイヤー判定用タグ")]
    [SerializeField]
    private string playerTag = "Player";
    // ここに指定したタグのオブジェクトが入退場したときに反応します。

    // === 内部状態 ===

    // 初期値を保存しておくことで、トリガーから出たときに戻せる
    private float initialVolume;
    private float initialReverbDb;
    private bool hasInitialReverb; // AudioMixer & パラメータが有効かどうか

    // 現在のフェード用
    private float currentVolume;
    private float currentReverbDb;

    // プレイヤーがトリガー内にいるかどうか
    private int playerInsideCount = 0;

    // フェード進行用
    private float volumeFadeTime;
    private float reverbFadeTime;

    private float volumeStart;
    private float volumeEnd;

    private float reverbStart;
    private float reverbEnd;

    private void Reset()
    {
        // コンポーネントを追加したときに、よく使う設定を自動で補完
        Collider col = GetComponent<Collider>();
        col.isTrigger = true; // 物理衝突ではなく、トリガーとして扱う

        // 同じGameObject上にAudioSourceがあれば、自動で対象にする
        if (targetAmbientSource == null)
        {
            targetAmbientSource = GetComponent<AudioSource>();
        }
    }

    private void Awake()
    {
        if (targetAmbientSource == null)
        {
            Debug.LogWarning($"[AmbientFader] AudioSource が未設定です: {name}");
        }

        // 初期音量を保存
        if (targetAmbientSource != null)
        {
            initialVolume = targetAmbientSource.volume;
            currentVolume = initialVolume;
        }

        // Reverb初期値を取得
        if (audioMixer != null && !string.IsNullOrEmpty(reverbParameterName))
        {
            if (audioMixer.GetFloat(reverbParameterName, out float value))
            {
                initialReverbDb = value;
                currentReverbDb = value;
                hasInitialReverb = true;
            }
            else
            {
                Debug.LogWarning($"[AmbientFader] AudioMixer にパラメータ '{reverbParameterName}' が見つかりません: {name}");
                hasInitialReverb = false;
            }
        }
        else
        {
            hasInitialReverb = false;
        }
    }

    private void Update()
    {
        // 毎フレーム、フェードを進めるだけの小さな責務に分離
        UpdateVolumeFade(Time.deltaTime);
        UpdateReverbFade(Time.deltaTime);
    }

    private void UpdateVolumeFade(float deltaTime)
    {
        if (targetAmbientSource == null) return;
        if (Mathf.Approximately(fadeDuration, 0f)) return;

        // フェードが進行中なら時間を進める
        if (!Mathf.Approximately(currentVolume, volumeEnd))
        {
            volumeFadeTime += deltaTime;
            float t = Mathf.Clamp01(volumeFadeTime / fadeDuration);
            currentVolume = Mathf.Lerp(volumeStart, volumeEnd, t);
            targetAmbientSource.volume = currentVolume;
        }
    }

    private void UpdateReverbFade(float deltaTime)
    {
        if (!hasInitialReverb) return;
        if (Mathf.Approximately(fadeReverbDuration, 0f)) return;

        if (!Mathf.Approximately(currentReverbDb, reverbEnd))
        {
            reverbFadeTime += deltaTime;
            float t = Mathf.Clamp01(reverbFadeTime / fadeReverbDuration);
            currentReverbDb = Mathf.Lerp(reverbStart, reverbEnd, t);
            audioMixer.SetFloat(reverbParameterName, currentReverbDb);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        // 指定したタグのオブジェクトだけを対象にする
        if (!other.CompareTag(playerTag)) return;

        playerInsideCount++;

        // 最初の1人が入ったタイミングでフェード開始
        if (playerInsideCount == 1)
        {
            StartFadeIn();
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (!other.CompareTag(playerTag)) return;

        playerInsideCount = Mathf.Max(0, playerInsideCount - 1);

        // 誰もいなくなったらフェードアウト
        if (playerInsideCount == 0)
        {
            StartFadeOut();
        }
    }

    private void StartFadeIn()
    {
        // 音量フェード設定
        if (targetAmbientSource != null)
        {
            volumeStart = currentVolume;
            volumeEnd = targetVolume;
            volumeFadeTime = 0f;

            // フェード時間0なら即座に反映
            if (Mathf.Approximately(fadeDuration, 0f))
            {
                currentVolume = targetVolume;
                targetAmbientSource.volume = currentVolume;
            }
        }

        // Reverbフェード設定
        if (hasInitialReverb)
        {
            reverbStart = currentReverbDb;
            reverbEnd = targetReverbDb;
            reverbFadeTime = 0f;

            if (Mathf.Approximately(fadeReverbDuration, 0f))
            {
                currentReverbDb = targetReverbDb;
                audioMixer.SetFloat(reverbParameterName, currentReverbDb);
            }
        }
    }

    private void StartFadeOut()
    {
        // 音量を初期値に戻すフェード
        if (targetAmbientSource != null)
        {
            volumeStart = currentVolume;
            volumeEnd = initialVolume;
            volumeFadeTime = 0f;

            if (Mathf.Approximately(fadeDuration, 0f))
            {
                currentVolume = initialVolume;
                targetAmbientSource.volume = currentVolume;
            }
        }

        // Reverbを初期値に戻すフェード
        if (hasInitialReverb)
        {
            reverbStart = currentReverbDb;
            reverbEnd = initialReverbDb;
            reverbFadeTime = 0f;

            if (Mathf.Approximately(fadeReverbDuration, 0f))
            {
                currentReverbDb = initialReverbDb;
                audioMixer.SetFloat(reverbParameterName, currentReverbDb);
            }
        }
    }
}

使い方の手順

ここでは「洞窟に入ったら環境音がこもって、リバーブが強くなる」ケースを例にします。プレイヤーはFPS/TPS問わず、Player タグがついている想定です。

  1. シーンに環境音用のAudioSourceを用意する
    • 空のGameObjectを作成し、名前を AmbientSound などにします。
    • AudioSource コンポーネントを追加し、風の音や環境ループ音のAudioClipを設定します。
    • ループ再生にチェックを入れ、Play On Awake をオンにしておくと便利です。
    • このAudioSourceは「外の世界の標準的な環境音」として鳴らしておきます。
  2. AudioMixerでReverbパラメータをExposeしておく(任意)
    • AudioMixerを作成し、Reverbを含むエフェクトチェーンを用意します。
    • 環境音用の AudioMixerGroup を作り、先ほどのAudioSourceの Output に割り当てます。
    • ReverbのWetレベル、あるいはSendレベルなど、調整したいパラメータをExposeし、名前を例えば ReverbLevel にします。
    • このExpose名を AmbientFaderreverbParameterName に入力します。
  3. 洞窟エリア用のトリガーを作る
    • 洞窟の入口付近に空のGameObjectを作成し、名前を CaveArea にします。
    • Box Collider(または Sphere / Capsule)を追加し、Is Trigger にチェックを入れます。
    • このColliderのサイズを調整して「ここに入ったら洞窟」という範囲を決めます。
    • AmbientFader コンポーネントを追加します(RequireComponent によりColliderは必須)。
  4. AmbientFaderのパラメータを設定する
    • Target Ambient Source に、手順1で作成した AmbientSound のAudioSourceをドラッグ&ドロップします。
    • AudioMixerを使う場合は AudioMixer に該当のMixerを、Reverb Mixer Group に環境用グループを設定します。
    • Target Volume を 0.3 などにして、洞窟内で少し音量が下がるようにします。
    • Fade Duration を 1.0〜2.0 秒程度にして、自然なフェードにします。
    • Target Reverb Db を 0〜+5dB などにして、洞窟内でリバーブが増えるようにします。
    • Fade Reverb Duration も 1.0〜2.0 秒程度に設定します。
    • Player TagPlayer を指定(プレイヤーオブジェクトのタグも Player にしておきます)。

    これで、プレイヤーが CaveArea のトリガーに入ると、環境音の音量とリバーブが洞窟用の設定にフェードし、出ると元の設定に戻るようになります。

同じ要領で、

  • 水中エリア
    TargetVolumeを下げ、Reverbを強め、別のAudioSourceで「こもった水中ノイズ」を鳴らす。
  • 建物の中
    外の環境音を少し下げて、室内特有の短いリバーブを足す。
  • ボス部屋の前
    環境音を少し絞って、BGMを際立たせるための「静けさエリア」を作る。

といったように、トリガー+AmbientFaderの組み合わせで、レベルデザインに合わせた「空気感の変化」を簡単に演出できます。

メリットと応用

AmbientFader を使う大きなメリットは、環境音の制御ロジックを「1つの巨大なサウンド管理クラス」に押し込めないことです。代わりに、

  • 洞窟のプレハブには「洞窟用AmbientFader」をセット
  • 水中ボリュームのプレハブには「水中用AmbientFader」をセット
  • 建物の入口プレハブには「室内用AmbientFader」をセット

といった形で、エリアごとの性格をそのままコンポーネントとして持たせられます。

これにより、

  • レベルデザイナーがシーンを組みながら「ここはもう少しこもらせたい」と思ったら、該当プレハブのパラメータをいじるだけでOK
  • サウンド担当はAudioMixer側のReverb設定に集中し、ゲームロジックにあまり手を出さなくて済む
  • 新しいステージを作るときも、既存のAmbientFader付きプレハブを置いて微調整するだけ

といった形で、プレハブ管理やレベルデザインがかなり楽になります。
責務も「環境音のフェード制御」だけに絞られているので、バグの切り分けもしやすいですね。

さらに応用として、「時間帯によって環境音を切り替える」「特定のイベント中だけ環境音をミュートする」などの拡張も考えられます。
例えば、以下のような簡単な改造を加えると、「スクリプト側から強制的に環境音をミュート・復帰」できるようになります。


    // 例: 外部スクリプトから呼び出して、一時的に環境音をミュートする
    public void ForceMute(float fadeTime)
    {
        if (targetAmbientSource == null) return;

        // 現在の状態から0までフェード
        volumeStart = currentVolume;
        volumeEnd = 0f;
        volumeFadeTime = 0f;
        fadeDuration = fadeTime;
    }

このように、小さなコンポーネントとして設計しておくと、必要に応じてメソッドを少し足すだけで、柔軟なサウンド制御ができるようになります。
「巨大なサウンド管理クラス」を作る前に、こうした小さな責務のコンポーネントを積み上げていく設計を意識してみると、プロジェクト全体の見通しがグッと良くなりますよ。