Unityを触り始めた頃、「BGMの制御はとりあえず Update() に全部書いておけばいいや」と考えてしまいがちですよね。
戦闘に入ったらBGMを変える処理、ボス戦用の曲に差し替える処理、フェード処理、音量調整……すべてを1つの巨大なスクリプトに詰め込むと、次のような問題が出てきます。

  • どのシーンでどのBGMが鳴るのかがコードを読まないと分からない
  • 「戦闘に入ったらギターを足す」などの演出を追加するときに、既存コードを大量に書き換える必要がある
  • テストしたいのに、1つの変更が他のシーンや演出に思わぬ影響を与える

そこでこの記事では、「BGMのレイヤー(ベース・ドラム・ギターなど)を個別のトラックとして用意し、戦闘状態に応じて音量だけを切り替える」コンポーネント指向のやり方を紹介します。
この役割だけに責任を絞ったコンポーネントが、今回の 「DynamicMusic」コンポーネント です。

【Unity】戦闘でBGMが厚くなる!「DynamicMusic」コンポーネント

「DynamicMusic」は、ベースとなるBGMはずっと流しっぱなしにしておき、
戦闘に入ったときだけドラムやギターのレイヤーをフェードイン、戦闘終了時にフェードアウトするためのコンポーネントです。

ポイントは次の通りです。

  • 複数の AudioSource を同じタイミングで再生し、音量だけを変える
  • 戦闘フラグ(bool)を外部から切り替えるだけで演出が完結する
  • フェード時間やトラック構成はインスペクターから調整可能

フルコード:DynamicMusic.cs


using UnityEngine;

/// <summary>
/// 戦闘状態に応じてBGMレイヤー(ベース / ドラム / ギターなど)の音量を
/// 自動的にフェードさせるコンポーネント。
/// 
/// 想定する使い方:
/// - ベースBGMは常に鳴らし続ける
/// - 戦闘に入ったらドラム・ギターをフェードイン
/// - 戦闘が終わったらドラム・ギターをフェードアウト
/// 
/// 外部からは SetCombatState(bool) を呼ぶだけでOK。
/// </summary>
[DisallowMultipleComponent]
public class DynamicMusic : MonoBehaviour
{
    [Header("ベースBGM(常時鳴らすトラック)")]
    [SerializeField] private AudioSource baseBgmSource;

    [Header("戦闘時に追加されるレイヤー")]
    [Tooltip("戦闘に入ると音量を上げるトラック(ドラム、ギターなど)")]
    [SerializeField] private AudioSource[] combatLayerSources;

    [Header("音量設定")]
    [Tooltip("ベースBGMの目標音量(非戦闘時も戦闘時も基本はこの音量)")]
    [Range(0f, 1f)]
    [SerializeField] private float baseBgmVolume = 0.8f;

    [Tooltip("戦闘レイヤーの目標音量(戦闘中に到達させる音量)")]
    [Range(0f, 1f)]
    [SerializeField] private float combatLayerTargetVolume = 0.8f;

    [Header("フェード設定")]
    [Tooltip("戦闘状態の切り替えにかけるフェード時間(秒)")]
    [SerializeField] private float fadeDuration = 1.0f;

    [Tooltip("シーン開始時に自動で再生を開始するか")]
    [SerializeField] private bool playOnStart = true;

    // 現在の戦闘状態
    private bool isInCombat = false;

    // フェードの進行度を管理するためのタイマー
    private float fadeTimer = 0f;

    // フェード中かどうか
    private bool isFading = false;

    // フェード開始時の戦闘状態
    private bool fadeFromCombatState = false;

    // 戦闘レイヤーの現在音量(Lerp用キャッシュ)
    private float[] currentLayerVolumes;

    // 初期化
    private void Awake()
    {
        // nullチェックと警告
        if (baseBgmSource == null)
        {
            Debug.LogWarning("[DynamicMusic] baseBgmSource が設定されていません。ベースBGMなしで動作します。", this);
        }

        if (combatLayerSources == null || combatLayerSources.Length == 0)
        {
            Debug.LogWarning("[DynamicMusic] combatLayerSources が空です。戦闘レイヤーは追加されません。", this);
        }
        else
        {
            // 戦闘レイヤー用の音量配列を初期化
            currentLayerVolumes = new float[combatLayerSources.Length];
        }
    }

    private void Start()
    {
        // 各AudioSourceの初期状態を設定
        InitializeAudioSources();

        // 自動再生フラグがONなら再生開始
        if (playOnStart)
        {
            PlayAllTracks();
        }
    }

    /// <summary>
    /// 各AudioSourceの初期設定を行う。
    /// </summary>
    private void InitializeAudioSources()
    {
        // ベースBGMの初期設定
        if (baseBgmSource != null)
        {
            baseBgmSource.loop = true;
            baseBgmSource.playOnAwake = false;
            baseBgmSource.volume = baseBgmVolume;
        }

        // 戦闘レイヤーの初期設定
        if (combatLayerSources != null)
        {
            for (int i = 0; i < combatLayerSources.Length; i++)
            {
                var src = combatLayerSources[i];
                if (src == null) continue;

                src.loop = true;
                src.playOnAwake = false;

                // 非戦闘状態からスタートするので、最初は音量0
                src.volume = 0f;
                currentLayerVolumes[i] = 0f;
            }
        }
    }

    /// <summary>
    /// すべてのトラックを同時に再生開始する。
    /// ベースBGMとレイヤーのタイミングを揃えたいので、一括でPlay()する。
    /// </summary>
    public void PlayAllTracks()
    {
        if (baseBgmSource != null && !baseBgmSource.isPlaying)
        {
            baseBgmSource.Play();
        }

        if (combatLayerSources != null)
        {
            foreach (var src in combatLayerSources)
            {
                if (src == null) continue;
                if (!src.isPlaying)
                {
                    src.Play();
                }
            }
        }
    }

    /// <summary>
    /// すべてのトラックを停止する。
    /// </summary>
    public void StopAllTracks()
    {
        if (baseBgmSource != null && baseBgmSource.isPlaying)
        {
            baseBgmSource.Stop();
        }

        if (combatLayerSources != null)
        {
            foreach (var src in combatLayerSources)
            {
                if (src == null) continue;
                if (src.isPlaying)
                {
                    src.Stop();
                }
            }
        }

        // 状態リセット
        isFading = false;
        fadeTimer = 0f;
    }

    /// <summary>
    /// 外部から戦闘状態を切り替えるための公開メソッド。
    /// 例: 敵を検知したら true、敵がいなくなったら false。
    /// </summary>
    /// <param name="inCombat">戦闘中なら true</param>
    public void SetCombatState(bool inCombat)
    {
        if (isInCombat == inCombat)
        {
            // 既に同じ状態なら何もしない
            return;
        }

        isInCombat = inCombat;

        // フェード開始
        fadeFromCombatState = !inCombat; // 今まで戦闘中だったかどうか
        fadeTimer = 0f;
        isFading = true;
    }

    private void Update()
    {
        // フェード処理を毎フレーム更新
        if (isFading)
        {
            UpdateFade(Time.deltaTime);
        }
    }

    /// <summary>
    /// フェードの進行を更新し、戦闘レイヤーの音量を補間する。
    /// </summary>
    private void UpdateFade(float deltaTime)
    {
        if (combatLayerSources == null || combatLayerSources.Length == 0)
        {
            isFading = false;
            return;
        }

        // フェード時間が0以下なら即時切り替え
        if (fadeDuration <= 0f)
        {
            ApplyInstantFade();
            isFading = false;
            return;
        }

        fadeTimer += deltaTime;
        float t = Mathf.Clamp01(fadeTimer / fadeDuration);

        // 非戦闘 -> 戦闘 に入るとき
        if (isInCombat)
        {
            for (int i = 0; i < combatLayerSources.Length; i++)
            {
                var src = combatLayerSources[i];
                if (src == null) continue;

                // 0 から combatLayerTargetVolume に向かってLerp
                float newVolume = Mathf.Lerp(0f, combatLayerTargetVolume, t);
                src.volume = newVolume;
                currentLayerVolumes[i] = newVolume;
            }
        }
        // 戦闘 -> 非戦闘 に戻るとき
        else
        {
            for (int i = 0; i < combatLayerSources.Length; i++)
            {
                var src = combatLayerSources[i];
                if (src == null) continue;

                // combatLayerTargetVolume から 0 に向かってLerp
                float newVolume = Mathf.Lerp(combatLayerTargetVolume, 0f, t);
                src.volume = newVolume;
                currentLayerVolumes[i] = newVolume;
            }
        }

        // フェード完了判定
        if (t >= 1f)
        {
            isFading = false;
        }
    }

    /// <summary>
    /// フェード時間が0のときに即座に音量を切り替える。
    /// </summary>
    private void ApplyInstantFade()
    {
        if (combatLayerSources == null) return;

        float target = isInCombat ? combatLayerTargetVolume : 0f;

        for (int i = 0; i < combatLayerSources.Length; i++)
        {
            var src = combatLayerSources[i];
            if (src == null) continue;

            src.volume = target;
            currentLayerVolumes[i] = target;
        }
    }

    /// <summary>
    /// デバッグ用に、現在の戦闘状態をトグルする。
    /// インスペクターから呼べるようにしておくと便利。
    /// (本番では使わなくてもOK)
    /// </summary>
    [ContextMenu("Toggle Combat State (Debug)")]
    private void ToggleCombatStateDebug()
    {
        SetCombatState(!isInCombat);
    }
}

使い方の手順

  1. トラック用のAudioSourceを用意する
    例として、プレイヤーが探索しているときは「静かなBGM」、戦闘に入るとドラムとギターが加わる構成を考えます。
    • 1つのGameObject(例: MusicManager)に、AudioSource を3つアタッチ
    • それぞれに以下のようにAudioClipを設定
      • AudioSource A: BaseBGM_Loop(常に鳴るベースBGM)
      • AudioSource B: Battle_Drum_Loop(戦闘時に追加されるドラム)
      • AudioSource C: Battle_Guitar_Loop(戦闘時に追加されるギター)
    • 各トラックの Loop をON、Play On Awake はOFFにしておきます(スクリプトから制御するため)。
  2. DynamicMusic コンポーネントを追加する
    • 同じ MusicManager オブジェクトに DynamicMusic スクリプトをアタッチ
    • インスペクターで
      • Base Bgm Source に AudioSource A をドラッグ&ドロップ
      • Combat Layer Sources に AudioSource B, C を配列としてセット
      • Base Bgm VolumeCombat Layer Target Volume を好みの音量に調整
      • Fade Duration でフェード時間(例: 0.8秒)を設定
      • Play On Start をONにしておくと、シーン開始時に自動再生されます
  3. ゲーム側の「戦闘開始/終了」から呼び出す
    例えば、プレイヤーの近くに敵が来たら戦闘状態にする簡単な例です。
    
    using UnityEngine;
    
    /// <summary>
    /// かなり簡略化した「戦闘状態のトリガー」例。
    /// 敵が範囲内に入ったら戦闘開始、いなくなったら戦闘終了。
    /// </summary>
    public class SimpleCombatTrigger : MonoBehaviour
    {
        [SerializeField] private DynamicMusic dynamicMusic;
        [SerializeField] private Transform player;
        [SerializeField] private Transform enemy;
        [SerializeField] private float combatDistance = 5f;
    
        private void Update()
        {
            if (dynamicMusic == null || player == null || enemy == null) return;
    
            float distance = Vector3.Distance(player.position, enemy.position);
            bool inCombat = distance <= combatDistance;
    
            // 状態をDynamicMusicに通知
            dynamicMusic.SetCombatState(inCombat);
        }
    }
    

    これを使えば、敵との距離に応じて自然にドラム&ギターが乗ってくる演出が作れます。

  4. プレイヤーや敵のプレハブに組み込む
    例えば:
    • プレイヤーの「戦闘管理コンポーネント」が DynamicMusic を参照していて、攻撃を受けたときに SetCombatState(true) を呼ぶ
    • 敵を全滅させたときに、敵マネージャーから SetCombatState(false) を呼ぶ
    • ボス戦用のシーンでは、別の MusicManager プレハブに別の曲をセットしておく

    こうしておくと、レベルデザイナーは「どのシーンでどのMusicManagerプレハブを置くか」だけを考えればよくなり、ロジック側は「戦闘かどうか」を通知するだけで済みます。

メリットと応用

この「DynamicMusic」コンポーネントを使うメリットは、責務がはっきり分離されることです。

  • BGMのレイヤー管理はDynamicMusicだけが担当
    戦闘システムや敵AIは、「今戦闘中かどうか」だけを判断して SetCombatState を呼べばOKです。
    音楽のフェードやトラックの数を変えたくなっても、戦闘システム側には一切手を入れる必要がありません。
  • プレハブベースでレベルデザインしやすい
    シーンごとに違うBGM構成を持たせたい場合は、MusicManager プレハブを複製して、AudioClipだけ差し替えればOKです。
    スクリプトは同じものを使い回せるので、管理がとても楽になります。
  • テストとデバッグがしやすい
    インスペクターのコンテキストメニュー「Toggle Combat State (Debug)」から、エディタ上で戦闘状態のON/OFFを切り替えられるので、
    実際の敵AIが完成していなくても、BGM演出だけ先に作っておくことができます。

さらに、応用として「プレイヤーのHPが減るほど緊張感のあるレイヤーが増える」「時間経過で徐々に楽器が増える」なども考えられます。

例えば、プレイヤーのHP割合に応じて戦闘レイヤーの音量を変化させる改造案はこんな感じです。


/// <summary>
/// プレイヤーHPの割合(0〜1)に応じて戦闘レイヤーの音量を調整する例。
/// DynamicMusic にこのメソッドを追加して、HP更新時に呼ぶイメージです。
/// </summary>
public void SetIntensityByHealth(float healthRatio)
{
    // 0〜1にクランプ
    float t = Mathf.Clamp01(1f - healthRatio); // HPが減るほど音量アップ

    if (combatLayerSources == null) return;

    for (int i = 0; i < combatLayerSources.Length; i++)
    {
        var src = combatLayerSources[i];
        if (src == null) continue;

        float volume = Mathf.Lerp(0f, combatLayerTargetVolume, t);
        src.volume = volume;
        if (currentLayerVolumes != null && i < currentLayerVolumes.Length)
        {
            currentLayerVolumes[i] = volume;
        }
    }
}

このように、小さなコンポーネントに責務を分けておくと、「戦闘フラグでON/OFF」「HPで強度調整」「ボス戦だけ別のレイヤー」など、演出のバリエーションを増やすのがとても楽になります。
巨大なGodクラスにBGMロジックを詰め込む前に、ぜひこうしたコンポーネント指向の設計を試してみてください。