Unityを触り始めた頃って、つい「音の処理も全部1つのスクリプトのUpdateに書いてしまう」ことが多いですよね。プレイヤーの移動、UIの更新、カメラ制御、BGMの切り替え、SEの音量変更……全部1つの巨大クラスに押し込んでしまうと、
- どこで何が起きているのか追えない
- ちょっとした仕様変更で大量のコードを修正する羽目になる
- シーンごとに微妙に違う挙動を作りづらい
といった問題が出てきます。
音響まわりも同じで、「水中に入ったら音をこもらせたい」「特定エリアだけローパスをかけたい」といった処理をプレイヤー制御スクリプトに全部書いてしまうと、一気にGodクラス化してしまいます。
そこで今回は、「水中エリアに入ったら自動でローパスフィルタをかけるだけ」という、役割がハッキリした小さなコンポーネント 「LowPassFilter」 を作っていきます。
水中ゾーンのオブジェクトにポンと付けるだけで、AudioMixer(AudioBus)にローパスをかけて、音をこもらせるコンポーネントです。
【Unity】水中に入ったら音がこもる!「LowPassFilter」コンポーネント
ここでは、Unityの AudioMixer(ここでは「AudioBus」と呼ばれることも多い)に設定したローパスフィルタのカットオフ周波数を、トリガーの出入りに応じてスムーズに変化させるコンポーネントを実装します。
- 水中に入る:カットオフ周波数を下げて音をこもらせる
- 水中から出る:カットオフ周波数を上げて通常状態に戻す
- プレイヤー以外には反応しないようにレイヤー・タグでフィルタ可能
というシンプルな責務だけを持たせます。
フルコード
using UnityEngine;
using UnityEngine.Audio; // AudioMixer を使うため
using System.Collections;
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider))]
public class LowPassFilter : MonoBehaviour
{
// --- AudioMixer(AudioBus)関連 ---
[Header("AudioMixer 設定")]
[SerializeField]
private AudioMixer audioMixer;
// AudioMixer 側で exposed したパラメータ名
// 例: "MasterLowPassCutoff"
[SerializeField]
private string cutoffParameterName = "MasterLowPassCutoff";
[Header("ローパス設定 (Hz)")]
[Tooltip("通常時のカットオフ周波数 (例: 22000)")]
[SerializeField]
private float normalCutoff = 22000f;
[Tooltip("水中時のカットオフ周波数 (例: 800)")]
[SerializeField]
private float underwaterCutoff = 800f;
[Header("トランジション設定 (秒)")]
[Tooltip("水中に入った時にローパスをかけるまでの時間")]
[SerializeField]
private float enterDuration = 0.5f;
[Tooltip("水中から出た時に元に戻すまでの時間")]
[SerializeField]
private float exitDuration = 0.5f;
[Header("対象フィルタ")]
[Tooltip("true の場合、指定したタグのオブジェクトにのみ反応")]
[SerializeField]
private bool useTagFilter = true;
[SerializeField]
private string targetTag = "Player";
[Tooltip("true の場合、指定した Layer のオブジェクトにのみ反応")]
[SerializeField]
private bool useLayerFilter = false;
[SerializeField]
private LayerMask targetLayerMask = ~0; // デフォルトは全レイヤー
// 現在進行中のトランジション用コルーチン
private Coroutine transitionCoroutine;
// 現在のターゲット(トリガー内にいるオブジェクトがあるかどうか)
private int insideCount = 0;
private void Reset()
{
// Collider をトリガーにしておく
Collider col = GetComponent<Collider>();
if (col != null)
{
col.isTrigger = true;
}
}
private void Awake()
{
// AudioMixer が設定されていなければ警告
if (audioMixer == null)
{
Debug.LogWarning(
$"[{nameof(LowPassFilter)}] AudioMixer が設定されていません。ローパスは動作しません。",
this
);
}
// 起動時に通常状態のカットオフにしておく
ApplyCutoffInstant(normalCutoff);
}
private void OnTriggerEnter(Collider other)
{
if (!IsTarget(other.gameObject))
{
return;
}
insideCount++;
// 初めてターゲットが入ったタイミングでローパスをかけ始める
if (insideCount == 1)
{
StartCutoffTransition(underwaterCutoff, enterDuration);
}
}
private void OnTriggerExit(Collider other)
{
if (!IsTarget(other.gameObject))
{
return;
}
insideCount = Mathf.Max(insideCount - 1, 0);
// 全てのターゲットが出たタイミングで元に戻す
if (insideCount == 0)
{
StartCutoffTransition(normalCutoff, exitDuration);
}
}
/// <summary>
/// 対象オブジェクトかどうかを判定する
/// </summary>
private bool IsTarget(GameObject obj)
{
if (useTagFilter && !obj.CompareTag(targetTag))
{
return false;
}
if (useLayerFilter)
{
// LayerMask に含まれているかどうか
int layerMask = 1 << obj.layer;
if ((targetLayerMask.value & layerMask) == 0)
{
return false;
}
}
return true;
}
/// <summary>
/// 指定したカットオフ周波数に向けてトランジションを開始する
/// </summary>
private void StartCutoffTransition(float targetCutoff, float duration)
{
// AudioMixer が設定されていない場合は何もしない
if (audioMixer == null || string.IsNullOrEmpty(cutoffParameterName))
{
return;
}
// すでにトランジション中なら止める
if (transitionCoroutine != null)
{
StopCoroutine(transitionCoroutine);
}
transitionCoroutine = StartCoroutine(CutoffTransitionRoutine(targetCutoff, duration));
}
/// <summary>
/// カットオフ周波数を即座に適用する
/// </summary>
private void ApplyCutoffInstant(float cutoff)
{
if (audioMixer == null || string.IsNullOrEmpty(cutoffParameterName))
{
return;
}
// AudioMixer のパラメータは基本的に dB だが、
// ローパスのカットオフは Hz をそのまま渡すことが多い。
// ここでは「AudioMixer 側で Hz の exposed parameter を使っている」前提で直接セットする。
audioMixer.SetFloat(cutoffParameterName, cutoff);
}
/// <summary>
/// カットオフ周波数を時間をかけて補間するコルーチン
/// </summary>
private IEnumerator CutoffTransitionRoutine(float targetCutoff, float duration)
{
// 現在値を取得
float currentCutoff;
if (!audioMixer.GetFloat(cutoffParameterName, out currentCutoff))
{
// 取得できなかった場合は normalCutoff を基準にする
currentCutoff = normalCutoff;
}
float elapsed = 0f;
// duration が 0 の場合は即座に反映
if (duration <= 0f)
{
ApplyCutoffInstant(targetCutoff);
transitionCoroutine = null;
yield break;
}
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 線形補間でカットオフを変化
float newCutoff = Mathf.Lerp(currentCutoff, targetCutoff, t);
audioMixer.SetFloat(cutoffParameterName, newCutoff);
yield return null;
}
// 最終値を保証
audioMixer.SetFloat(cutoffParameterName, targetCutoff);
transitionCoroutine = null;
}
}
使い方の手順
ここでは、プレイヤーが水中エリアに入ると音がこもる という想定で進めます。
① AudioMixer(AudioBus)を用意してローパス用パラメータを作る
- Project ウィンドウで Create > Audio Mixer を選択し、
MasterMixerなど名前をつけます。 - ダブルクリックして AudioMixer ウィンドウを開き、Master グループ(または任意のグループ)を選択します。
- 右側の Inspector で、「Add Effect」から Lowpass Simple などのローパス系エフェクトを追加します。
- そのローパスの Cutoff などのパラメータ右クリック → Expose ‘Cutoff’ to script を選択します。
- AudioMixer ウィンドウ右側の Exposed Parameters パネルで、今 expose したパラメータ名を確認し、
MasterLowPassCutoffのような分かりやすい名前に変更します。
この Exposed Parameter の名前 を、先ほどのスクリプトの cutoffParameterName に設定します。
② シーンに水中エリア用の GameObject を作る
- Hierarchy で Create Empty を選び、名前を
WaterAreaにします。 - WaterArea に Box Collider を追加します(または水面の形状に合わせて Sphere / Capsule でもOK)。
- Box Collider の Is Trigger にチェックを入れます。
(スクリプトのReset()でも自動でトリガーにしますが、念のため確認しておきましょう)
③ 「LowPassFilter」コンポーネントをアタッチして設定する
- WaterArea を選択し、「Add Component」から LowPassFilter を追加します。
- Inspector で以下の項目を設定します:
- Audio Mixer:①で作った
MasterMixerをドラッグ&ドロップ - Cutoff Parameter Name:①で expose したパラメータ名(例:
MasterLowPassCutoff) - Normal Cutoff:通常時の周波数(例:
22000) - Underwater Cutoff:水中時の周波数(例:
800〜1500あたり) - Enter Duration:水中に入ったときのフェード時間(例:
0.5秒) - Exit Duration:水中から出たときのフェード時間(例:
0.5秒) - Use Tag Filter:プレイヤーだけに反応させたい場合は
true - Target Tag:
Player(プレイヤーのタグに合わせる)
- Audio Mixer:①で作った
プレイヤーの GameObject 側には、Collider(CharacterController 含む)+ Rigidbody などの物理コンポーネントが付いていることを確認してください。
水中エリアの Collider.isTrigger と、プレイヤー側の Collider が正しく設定されていれば、OnTriggerEnter/Exit が呼ばれます。
④ 実例:プレイヤー用、水中敵用、動く水中エリア
- プレイヤーが水中に潜るステージ
プレイヤーの移動スクリプトには一切手を入れず、水中エリアにだけ LowPassFilter を付けるだけでOKです。
ステージごとに水中の広さやローパスの強さを変えたい場合も、そのエリアのインスペクタ値を変えるだけで済みます。 - 水中にいる敵だけ音がこもる演出
敵キャラの AudioSource が別の AudioMixer グループ(例:Enemy)に送られているなら、敵専用の Mixer グループ+ローパスを作り、
敵専用のLowPassFilterを敵の周囲に置く、という構成もできます。
この場合はuseTagFilterをtrueにして、targetTagをEnemyにしておきましょう。 - 動く水中エリア(例:移動するバブルシールド)
WaterAreaをプレイヤーやオブジェクトの子にして動かすと、「その周囲だけ音がこもる」ような表現も作れます。
コンポーネント自体は「トリガーに入ったかどうか」しか見ていないので、動いていても問題なく動作します。
メリットと応用
この LowPassFilter コンポーネントを使うメリットは、
- 水中演出のロジックがプレイヤーや敵のスクリプトから完全に独立する
→ プレイヤー制御スクリプトを肥大化させずに済みます。 - プレハブ化して量産しやすい
→ 「水中エリア用プレハブ」を1つ作っておけば、シーン上にいくつでも配置してOK。
→ ステージごとにローパスの強さやフェード時間を変えたい場合も、プレハブを複製してパラメータを変えるだけです。 - レベルデザイン側で音響演出をいじりやすい
→ 「ここは音をちょっとだけこもらせたい」「ここはガッツリこもらせたい」といった要望に対して、
プログラマがコードを書き換えなくても、レベルデザイナーがインスペクタで調整できます。 - タグ・レイヤーで対象を切り替えられる
→ 将来的に「特定の仲間キャラだけ水中音にしたい」などの拡張もしやすい構造になっています。
コンポーネントの責務を「トリガー内外に応じて AudioMixer のパラメータを補間するだけ」に限定しているので、
他のコンポーネント(例えば、水中で移動速度を落とす UnderwaterMovementModifier など)とも組み合わせやすくなります。
改造案:フェードカーブをイージングで変える
今の実装は Mathf.Lerp による線形補間ですが、
「水中に入った瞬間だけグッとこもらせて、その後ゆっくり変化させたい」など、イージングカーブを使っても面白いです。
例えば、以下のようなイージング関数を追加して、CutoffTransitionRoutine 内の Mathf.Lerp の t に適用するだけで、
フェードの雰囲気をガラッと変えられます。
/// <summary>
/// ゆっくり始まって最後にスッと終わるイージング (EaseOutQuad)
/// </summary>
private float EaseOutQuad(float t)
{
// t: 0〜1 の値を想定
return 1f - (1f - t) * (1f - t);
}
この関数を使う場合、CutoffTransitionRoutine の中で
float t = Mathf.Clamp01(elapsed / duration);
t = EaseOutQuad(t); // ここでイージングを適用
のように一行追加するだけで、より自然なローパスのかかり方になります。
このように、小さなコンポーネントにしておくと、こうした改造も局所的に行いやすくなりますね。
