Unityを触り始めた頃は、つい何でもかんでも Update() に書いてしまいがちですよね。
プレイヤーの移動、敵のAI、UIの更新、サウンドの制御……すべて1つの巨大スクリプトに押し込むと、だんだん「どこを直せばいいか分からない」「音量だけ別のステージで挙動が変になる」といった問題が出てきます。
特にサウンド周りは、
- 距離による音量の減衰
- プレイヤーが近づいたら大きく、離れたら小さく
- 複数のSEや環境音をまとめて制御したい
といった要件が出てきがちですが、これを全部「プレイヤーのスクリプトのUpdate」に書き始めると一気にカオス化します。
そこでこの記事では、「距離による音量の自動調整」だけに責務を絞ったコンポーネント 「DistanceVolume」 を用意して、
どのオブジェクトにもペタッと貼るだけで距離減衰が効く仕組みを作っていきます。
【Unity】距離で音がフェードする!「DistanceVolume」コンポーネント
DistanceVolume は、指定した「リスナー」(通常はプレイヤー)との距離に応じて、
親オブジェクトにぶら下がった AudioSource たちの音量をまとめて調整するコンポーネントです。
- リスナーに近いときは音量100%
- 指定距離を超えるときは音量0%
- その間は0〜1の間でなめらかに補間
というシンプルな距離減衰を行います。
環境音のグループ(滝+鳥のさえずり+風の音など)を1つのオブジェクトにまとめておいて、
その親に DistanceVolume を付けるだけで、距離に応じて全部まとめてフェードしてくれます。
フルコード:DistanceVolume.cs
using UnityEngine;
/// <summary>
/// リスナー(例:プレイヤー)との距離に応じて、
/// このオブジェクト配下の AudioSource の音量を自動調整するコンポーネント。
///
/// ・責務は「距離に応じたボリューム計算と適用」だけに限定
/// ・実際の再生開始/停止は他のコンポーネントやTimeline側で行う想定
/// </summary>
public class DistanceVolume : MonoBehaviour
{
[Header("リスナー設定")]
[SerializeField]
private Transform listener;
// 通常はプレイヤーのTransformを指定する
// 未指定の場合は、Awakeで"MainCamera"タグのTransformを自動取得を試みる
[Header("距離減衰パラメータ")]
[SerializeField]
[Tooltip("この距離以下では最大音量になります。")]
private float minDistance = 2f;
[SerializeField]
[Tooltip("この距離以上では音量0になります。")]
private float maxDistance = 20f;
[SerializeField]
[Range(0f, 1f)]
[Tooltip("最大時の音量(0〜1)。複数の環境音をまとめて抑えたいときに使用します。")]
private float maxVolume = 1f;
[SerializeField]
[Tooltip("距離に応じたフェードのカーブ。X=0がminDistance、X=1がmaxDistanceに対応します。")]
private AnimationCurve attenuationCurve = AnimationCurve.Linear(0f, 1f, 1f, 0f);
[Header("更新設定")]
[SerializeField]
[Tooltip("毎フレームUpdateで更新するかどうか。falseにすると手動でUpdateVolumeを呼び出す運用も可能です。")]
private bool updateEveryFrame = true;
[SerializeField]
[Tooltip("距離変化がこの値未満なら音量を更新しない(微小な変化によるGCや演算を抑える)。")]
private float distanceChangeThreshold = 0.01f;
// キャッシュしておくAudioSource群
private AudioSource[] _audioSources;
// 前フレームで使用した距離
private float _lastDistance = -1f;
private void Awake()
{
// 子オブジェクトも含めてAudioSourceを一括取得
_audioSources = GetComponentsInChildren<AudioSource>(includeInactive: true);
// リスナーが未指定なら MainCamera を探してみる
if (listener == null)
{
Camera mainCam = Camera.main;
if (mainCam != null)
{
listener = mainCam.transform;
}
else
{
Debug.LogWarning(
$"[DistanceVolume] リスナーが指定されておらず、MainCameraも見つかりませんでした。" +
$" シーン内のプレイヤーやカメラのTransformをインスペクターから設定してください。",
this
);
}
}
// min > max になってしまった場合の保険
if (maxDistance < minDistance)
{
maxDistance = minDistance;
}
// 初回に一度ボリュームを反映しておく
UpdateVolume();
}
private void Update()
{
if (!updateEveryFrame)
{
return;
}
UpdateVolume();
}
/// <summary>
/// 距離に応じて音量を更新するメイン処理。
/// 毎フレームUpdateから呼ぶことも、他のスクリプトから手動で呼ぶこともできます。
/// </summary>
public void UpdateVolume()
{
if (listener == null)
{
// リスナー未設定なら何もしない
return;
}
// リスナーとの距離を計算
float distance = Vector3.Distance(listener.position, transform.position);
// 距離変化がごくわずかな場合はスキップ(パフォーマンスとGC対策)
if (_lastDistance >= 0f && Mathf.Abs(distance - _lastDistance) < distanceChangeThreshold)
{
return;
}
_lastDistance = distance;
// minDistance〜maxDistanceを0〜1に正規化
float t = 0f;
if (maxDistance - minDistance > 0.0001f)
{
t = Mathf.InverseLerp(minDistance, maxDistance, distance);
}
else
{
// minとmaxがほぼ同じなら、距離による変化はないとみなす
t = distance <= minDistance ? 0f : 1f;
}
// カーブを使って0〜1の減衰係数を取得
// t=0 => minDistance(近い) / t=1 => maxDistance(遠い)
float attenuation = attenuationCurve.Evaluate(Mathf.Clamp01(t));
// 実際に適用する音量
float targetVolume = maxVolume * Mathf.Clamp01(attenuation);
// すべてのAudioSourceに一括で適用
ApplyVolumeToAllSources(targetVolume);
}
/// <summary>
/// 取得済みのAudioSource全てに音量を適用する。
/// </summary>
/// <param name="volume">0〜1の音量</param>
private void ApplyVolumeToAllSources(float volume)
{
if (_audioSources == null || _audioSources.Length == 0)
{
return;
}
// ループを回してvolumeをセットするだけのシンプルな処理
for (int i = 0; i < _audioSources.Length; i++)
{
AudioSource source = _audioSources[i];
if (source == null)
{
continue;
}
// ここではAudioSource.volumeを直接いじる。
// ミキサー(AudioMixer)を使う場合は、そちらのパラメータ操作に切り替えてもOK。
source.volume = volume;
}
}
/// <summary>
/// ランタイム中にリスナーを差し替えたい場合に呼び出す。
/// 例:プレイヤー切り替え、カメラ切り替えなど。
/// </summary>
/// <param name="newListener">新しいリスナーのTransform</param>
public void SetListener(Transform newListener)
{
listener = newListener;
// リスナー変更直後に一度更新しておく
_lastDistance = -1f; // 強制的に更新させる
UpdateVolume();
}
/// <summary>
/// 子オブジェクトのAudioSourceを再スキャンしたい場合に呼ぶ。
/// 例:ランタイム中に環境音オブジェクトを動的生成した場合など。
/// </summary>
public void RefreshAudioSources()
{
_audioSources = GetComponentsInChildren<AudioSource>(includeInactive: true);
_lastDistance = -1f; // 強制的に更新させる
UpdateVolume();
}
}
使い方の手順
-
コンポーネントを作成する
- プロジェクトビューで
DistanceVolume.csを作成し、上記コードをコピペして保存します。 - Unityに戻ると自動的にコンパイルされ、コンポーネントとして使えるようになります。
- プロジェクトビューで
-
環境音(親オブジェクト)を用意する
例として、「洞窟の環境音」を作りたいとします。- ヒエラルキーで空のGameObjectを作成し、名前を
CaveAmbienceなどにします。 CaveAmbienceの子として、複数の環境音用オブジェクトを作成します。例:WaterDrip(水滴の音)WindLoop(風のループ音)EchoNoise(洞窟の反響ノイズ)
- それぞれの子オブジェクトに
AudioSourceを追加し、対応するAudioClipを設定してループ再生などの設定を行います。
- ヒエラルキーで空のGameObjectを作成し、名前を
-
DistanceVolume を親にアタッチする
CaveAmbience(親オブジェクト)を選択し、DistanceVolumeコンポーネントを追加します。- Listener には通常、プレイヤーやカメラの
Transformをドラッグ&ドロップで指定します。- もし指定しない場合は、
MainCameraタグの付いたカメラが自動で使われます。
- もし指定しない場合は、
- Min Distance に「近距離でフルボリュームになる距離」(例:2)を設定します。
- Max Distance に「それ以上離れたら聞こえなくなる距離」(例:20〜30)を設定します。
- Max Volume で全体の上限ボリューム(例:0.7)を調整します。
- Attenuation Curve を調整して、距離に対する減衰の仕方を好みに合わせてカスタマイズします。
- デフォルトは線形(Linear):距離に比例してまっすぐ減衰。
- ゆるやかに減衰させたいなら、最初は1.0付近を維持して最後にストンと落とすようなカーブに。
-
プレイして距離減衰を確認する
- プレイヤー(もしくはカメラ)を
CaveAmbienceに近づけたり離したりして、音量が変化することを確認します。 - 別の場所にも同じように親オブジェクト+子AudioSource群を用意して
DistanceVolumeを付ければ、複数のエリア環境音を簡単に管理できます。
- プレイヤー(もしくはカメラ)を
他の具体例として:
- 敵キャラの咆哮SE:敵の親オブジェクトに
DistanceVolumeを付けておけば、プレイヤーからの距離に応じて「近くの敵の咆哮は大きく、遠くは小さく」自然に聞こえます。 - 動く床やエレベーターの機械音:移動するオブジェクト自体に
DistanceVolumeを付けておけば、プレイヤーとの距離に応じてモーター音がフェードイン/アウトします。
メリットと応用
DistanceVolume を使うメリットは、何よりも「距離減衰の責務を1つの小さなコンポーネントに閉じ込められる」ことです。
- プレイヤー側のスクリプトは「移動」に集中でき、サウンドの距離計算は完全に切り離されます。
- シーンデザイナーは、環境音のプレハブをポンポン配置するだけで、自然な距離減衰付きの音場を作れます。
- プレハブとして
CaveAmbienceなどを保存しておけば、別シーンにドラッグするだけで同じ挙動を再利用できます。 - 「距離のパラメータだけ変えたバリエーション」を作るのも、インスペクターの調整だけで済みます。
さらに、DistanceVolume は 音量を決める「係数」を提供するコンポーネント としても扱えるので、応用範囲が広いです。例えば:
- 距離に応じて リバーブ量 を変えたい
- 距離に応じて BGMのフィルター を強くしたい
- 距離に応じて パーティクルの発生量 を変えたい
といった用途にも、「距離→0〜1の係数」という変換ロジックをそのまま流用できます。
改造案:AudioMixerのボリュームを距離で制御する
AudioSource.volume ではなく、AudioMixer の Exposed Parameter を使って距離減衰させたい場合は、
ApplyVolumeToAllSources の代わりに、例えばこんなメソッドを追加して使うのもアリですね。
using UnityEngine.Audio;
// AudioMixerを使って距離減衰させたい場合の一例
[SerializeField]
private AudioMixer audioMixer;
[SerializeField]
private string mixerVolumeParameter = "BGMVolume";
// ...
/// <summary>
/// AudioMixerのボリュームパラメータを距離に応じて更新する例。
/// volume(0〜1)をdB値(-80〜0など)に変換してセットします。
/// </summary>
private void ApplyVolumeToMixer(float volume)
{
if (audioMixer == null || string.IsNullOrEmpty(mixerVolumeParameter))
{
return;
}
// 0〜1のボリュームをdBに変換(かなり単純化した例)
// volume=1 => 0dB, volume=0 => -80dB くらいにマッピング
float dB;
if (volume <= 0.0001f)
{
dB = -80f;
}
else
{
dB = Mathf.Lerp(-80f, 0f, volume);
}
audioMixer.SetFloat(mixerVolumeParameter, dB);
}
このように、小さなコンポーネントとして距離減衰ロジックを切り出しておくと、
「どこで距離を計算しているのか」「どこで音量をいじっているのか」が明確になり、
プレハブ管理やレベルデザインもグッと楽になります。
大きなGodクラスではなく、小さな責務ごとのコンポーネントを積み上げて、扱いやすいプロジェクト構成を目指していきましょう。
