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);
}
}
使い方の手順
-
トラック用のAudioSourceを用意する
例として、プレイヤーが探索しているときは「静かなBGM」、戦闘に入るとドラムとギターが加わる構成を考えます。- 1つのGameObject(例:
MusicManager)に、AudioSourceを3つアタッチ - それぞれに以下のようにAudioClipを設定
- AudioSource A:
BaseBGM_Loop(常に鳴るベースBGM) - AudioSource B:
Battle_Drum_Loop(戦闘時に追加されるドラム) - AudioSource C:
Battle_Guitar_Loop(戦闘時に追加されるギター)
- AudioSource A:
- 各トラックの
LoopをON、Play On AwakeはOFFにしておきます(スクリプトから制御するため)。
- 1つのGameObject(例:
-
DynamicMusic コンポーネントを追加する
- 同じ
MusicManagerオブジェクトにDynamicMusicスクリプトをアタッチ - インスペクターで
Base Bgm Sourceに AudioSource A をドラッグ&ドロップCombat Layer Sourcesに AudioSource B, C を配列としてセットBase Bgm VolumeやCombat Layer Target Volumeを好みの音量に調整Fade Durationでフェード時間(例: 0.8秒)を設定Play On StartをONにしておくと、シーン開始時に自動再生されます
- 同じ
-
ゲーム側の「戦闘開始/終了」から呼び出す
例えば、プレイヤーの近くに敵が来たら戦闘状態にする簡単な例です。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); } }これを使えば、敵との距離に応じて自然にドラム&ギターが乗ってくる演出が作れます。
-
プレイヤーや敵のプレハブに組み込む
例えば:- プレイヤーの「戦闘管理コンポーネント」が
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ロジックを詰め込む前に、ぜひこうしたコンポーネント指向の設計を試してみてください。
