Unityを触り始めた頃は、とりあえず Update() に全部書いてしまいがちですよね。プレイヤー入力、移動、エフェクト、サウンド制御…すべてを1つの巨大スクリプトに押し込むと、だんだん「どこを触ればいいのか分からない」「バグを直すたびに別のところが壊れる」という状態になってしまいます。

音に関しても同じで、「プレイヤー制御スクリプトの中で、敵との距離を計算して音量を変える」「カメラ制御スクリプトの中で、左右のパンをいじる」といった形でロジックが散らばると、サウンド調整のたびにいろいろなスクリプトを開かないといけなくなります。

そこでこの記事では、「音の距離&方向による音量・左右パン調整」という責務だけを切り出したコンポーネント 「SpatialAudio」 を用意して、サウンドまわりをすっきり分離する方法を紹介します。任意のオブジェクトにこのコンポーネントを貼るだけで、音源とリスナー(カメラなど)の位置関係に応じて音量とパンを自動調整してくれるようにしてみましょう。

【Unity】3D音響を手軽にコンポーネント化!「SpatialAudio」コンポーネント

フルコード(SpatialAudio.cs)


using UnityEngine;

/// <summary>
/// 距離と方向に応じて音量と左右パンを自動調整する3D音響コンポーネント。
/// - AudioSource は 2D(spatialBlend = 0)でも 3D でも利用可能
/// - リスナー(通常はカメラ)を指定しておくと、
///   ・距離による減衰
///   ・左右方向によるパン(ステレオバランス)
///   を自動で計算してくれる
/// 
/// 責務は「位置関係からボリュームとパンを決めること」だけに限定し、
/// 入力や移動ロジックとは完全に分離することを目指したコンポーネントです。
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class SpatialAudio : MonoBehaviour
{
    // ==== 参照 ====

    [Header("参照設定")]
    [Tooltip("音を聴く側(通常はメインカメラ)の Transform。未設定の場合は MainCamera を自動検索します。")]
    [SerializeField] private Transform listenerTransform;


    // ==== 距離による音量設定 ====

    [Header("距離による音量減衰")]
    [Tooltip("この距離までは最大音量を維持します。")]
    [SerializeField] private float minDistance = 1.0f;

    [Tooltip("この距離を超えると完全に無音になります。")]
    [SerializeField] private float maxDistance = 20.0f;

    [Tooltip("最大音量(0〜1)。AudioSource.volume の基準値になります。")]
    [Range(0f, 1f)]
    [SerializeField] private float baseVolume = 1.0f;

    [Tooltip("距離による減衰カーブ。0=線形, 1=二乗, 0.5=中間など。")]
    [Range(0.1f, 3f)]
    [SerializeField] private float distanceFalloffPower = 1.0f;


    // ==== 左右方向によるパン設定 ====

    [Header("左右パン設定")]
    [Tooltip("左右パンの強さ。0=常に中央, 1=最大まで振る。")]
    [Range(0f, 1f)]
    [SerializeField] private float panIntensity = 1.0f;

    [Tooltip("左右に最大でどの程度パンさせるか(-1〜1)。通常は 1 で OK。")]
    [Range(0f, 1f)]
    [SerializeField] private float maxPan = 1.0f;


    // ==== 更新間隔 ====

    [Header("パフォーマンス設定")]
    [Tooltip("音量・パンを更新する間隔(秒)。0 なら毎フレーム更新。")]
    [SerializeField] private float updateInterval = 0.05f;


    // ==== 内部キャッシュ ====

    private AudioSource audioSource;
    private float updateTimer;


    private void Awake()
    {
        // 同じ GameObject についている AudioSource を取得
        audioSource = GetComponent<AudioSource>();

        // リスナーが未設定ならメインカメラを自動で探す
        if (listenerTransform == null && Camera.main != null)
        {
            listenerTransform = Camera.main.transform;
        }

        // AudioSource の 3D 設定に依存しないよう、
        // 距離減衰とパンはこのコンポーネント側で制御する前提とします。
        // (必要に応じて AudioSource.spatialBlend を 0 にして 2D として扱ってもOK)
    }

    private void OnEnable()
    {
        // 有効化されたタイミングで即座に1回更新しておく
        updateTimer = 0f;
        UpdateSpatialAudio();
    }

    private void Update()
    {
        if (updateInterval <= 0f)
        {
            // 毎フレーム更新
            UpdateSpatialAudio();
            return;
        }

        // 一定間隔でのみ更新して負荷を抑える
        updateTimer -= Time.deltaTime;
        if (updateTimer <= 0f)
        {
            UpdateSpatialAudio();
            updateTimer = updateInterval;
        }
    }

    /// <summary>
    /// 距離と方向に応じて AudioSource の volume と panStereo を更新するメイン処理。
    /// </summary>
    private void UpdateSpatialAudio()
    {
        if (listenerTransform == null || audioSource == null)
        {
            return;
        }

        // --- 距離計算 ---
        Vector3 toListener = listenerTransform.position - transform.position;
        float distance = toListener.magnitude;

        // minDistance 〜 maxDistance の範囲を 0〜1 に正規化
        float t = Mathf.InverseLerp(minDistance, maxDistance, distance);

        // 範囲外のときの扱い
        if (distance <= minDistance)
        {
            t = 0f; // 最大音量
        }
        else if (distance >= maxDistance)
        {
            t = 1f; // 無音
        }

        // カーブ(べき乗)を適用して減衰を調整
        float attenuation = Mathf.Pow(1f - t, distanceFalloffPower);

        // 最終的な音量
        float finalVolume = baseVolume * attenuation;


        // --- 左右パン計算 ---
        float finalPan = 0f;

        if (panIntensity > 0f)
        {
            // リスナーのローカル空間での位置を求める
            Vector3 localPos = listenerTransform.InverseTransformPoint(transform.position);

            // X の符号が左右方向を表す(右が +, 左が -)
            // Z が前後、Y が上下
            float horizontal = localPos.x;

            // -1 〜 1 の範囲に正規化
            // 大きな距離でも極端に振り切れないよう、tanh っぽいカーブを使用
            float normalized = Mathf.Clamp(horizontal / 5f, -1f, 1f);

            // 距離減衰もパンに少し影響させる(遠いほどパンを弱める)
            float distanceFactor = attenuation;

            finalPan = normalized * maxPan * panIntensity * distanceFactor;
        }

        // --- AudioSource に反映 ---
        audioSource.volume = finalVolume;
        audioSource.panStereo = finalPan;
    }

    /// <summary>
    /// 外部からリスナー(カメラなど)を差し替えたいとき用のヘルパー。
    /// 例: Split-screen などでリスナーを動的に切り替える場合。
    /// </summary>
    /// <param name="newListener">新しいリスナーの Transform</param>
    public void SetListener(Transform newListener)
    {
        listenerTransform = newListener;
        UpdateSpatialAudio();
    }
}

使い方の手順

ここでは、代表的な使用例として「プレイヤー視点のカメラで、周囲のオブジェクトの音を3Dっぽく聞かせる」ケースを想定します。

  1. ① シーンに AudioListener を用意する
    通常はメインカメラに AudioListener コンポーネントが付いているので、そのままでOKです。
    このカメラが「耳(リスナー)」になります。

  2. ② 音を出したいオブジェクトに AudioSource を追加
    例として、以下のようなオブジェクトを作りましょう。

    • プレイヤーの近くを歩き回る「敵キャラ」
    • 一定位置で流れ続ける「滝の音」
    • 上を通り過ぎる「動く電車」

    それぞれの GameObject に AudioSource を追加し、AudioClip を設定しておきます。
    loop をオンにすると環境音やエンジン音などに便利です。

  3. ③ 同じオブジェクトに SpatialAudio コンポーネントを追加
    上で貼った SpatialAudio.cs をプロジェクトに追加し、
    音を出したいオブジェクト(敵、滝、電車など)に SpatialAudio コンポーネントを追加します。

    • Listener Transform:空欄でOK(自動で MainCamera を探します)。
      別のカメラを使いたい場合だけ、任意の Transform をドラッグ&ドロップで指定します。
    • Min Distance:この距離以内は最大音量。近距離の効果音なら 1〜2m くらいが目安。
    • Max Distance:この距離を超えると無音。環境音なら 30〜50m くらい、敵の声なら 15〜20m など。
    • Base Volume:その音源の基準音量。BGM より効果音を少し大きめにしたいなど、個別調整に使います。
    • Distance Falloff Power:距離による減衰カーブ。1 なら線形、2 なら近くで急に大きくなる感じ。
    • Pan Intensity:左右パンの強さ。0.5〜1.0 あたりを試すと分かりやすいです。
    • Max Pan:最大パン量。通常は 1 のままでOK。
    • Update Interval:0.05〜0.1 秒くらいにしておくと、負荷を抑えつつ違和感なく動きます。
  4. ④ 実行して動作確認
    ゲームを実行し、プレイヤー(カメラ)を動かしてみましょう。

    • 敵に近づくと声や足音が大きくなり、離れると小さくなる。
    • 滝の右側に回り込むと、音が右耳寄りに聞こえる。
    • 電車が左から右へ通過すると、音も左から右へスライドして聞こえる。

    これらが、プレイヤー制御やカメラ制御のスクリプトに一切手を入れず、「SpatialAudio」コンポーネントだけで完結しているのがポイントです。

メリットと応用

SpatialAudio を使う最大のメリットは、「音の空間的な制御」を1つの小さなコンポーネントに閉じ込められることです。

  • プレハブ管理が楽になる
    敵キャラやギミックのプレハブにあらかじめ AudioSource + SpatialAudio を仕込んでおけば、シーンに配置するだけで3D音響が効いた状態になります。
    レベルデザイナーは「どこに置くか」だけを考えればよく、音量やパンのロジックを意識する必要がありません。
  • スクリプトの責務が明確になる
    プレイヤーや敵の挙動スクリプトから「距離を測って音量を変える」処理を追い出せるので、移動やAIのコードがすっきりします。
    音の調整は SpatialAudio だけを触ればよく、「音が大きすぎる/小さすぎる」といったフィードバックに素早く対応できます。
  • レベルデザインの試行錯誤がしやすい
    シーンビュー上でオブジェクトを移動させるだけで、距離減衰とパンの結果も変わります。
    「この滝の音はもう少し遠くから聞こえてほしい」「このスイッチの音は近くまで行かないと聞こえないようにしたい」といった調整を、プレハブの Inspector の値をいじるだけで試せます。

さらに、応用として以下のような改造も考えられます。

  • 敵がプレイヤーを見つけたときだけ baseVolume を上げて、威圧感のある声にする。
  • プレイヤーがダメージを受けたとき、一時的に maxDistance を短くして「耳が遠くなる」演出をする。
  • 水中に入ったら高音をカットしたようなフィルタを別コンポーネントで併用する。

最後に、「プレイヤーが特定の状態のときだけ音量をブーストする」ような簡単な改造案を示します。
例えば、プレイヤーが「集中モード」に入ったとき、近くの敵の音だけ強調したい場合などに使えます。


    /// <summary>
    /// 一時的に音量をブーストする例。
    /// 例えば「集中モード中は環境音を強調する」といった演出に使える。
    /// boostFactor は 1 で通常、2 で2倍など。
    /// </summary>
    /// <param name="boostFactor">音量ブースト倍率</param>
    public void ApplyVolumeBoost(float boostFactor)
    {
        // baseVolume を一時的に変更し、即座に反映する
        baseVolume = Mathf.Clamp01(baseVolume * boostFactor);
        UpdateSpatialAudio();
    }

このように、小さな責務に切り分けたコンポーネントを積み重ねていくと、プロジェクト全体の見通しがぐっと良くなります。
サウンド周りがごちゃついてきたら、ぜひ「SpatialAudio」のような専用コンポーネントに分離して整理してみてください。