Unityを触り始めた頃、「プレイヤーのHP管理も、ダメージ処理も、瀕死演出も、全部ひとつのスクリプトのUpdateに書いてしまう」……という実装をしがちですよね。
たとえば、こんな感じです:
// 典型的な「なんでもUpdate」スタイル(例:やらない方がよい書き方)
void Update()
{
// HPを減らす
if (isDamaged)
{
hp -= damage;
}
// 瀕死かどうかを見る
if (hp < 20)
{
// オーディオのローパスをオンにする
}
else
{
// ローパスをオフにする
}
// その他、移動・攻撃・UI更新など……
}
こうなると、HPの仕様を変えたいだけなのにオーディオの処理まで巻き込まれる、瀕死演出を増やそうとしたらスクリプトがどんどん肥大化する、という「Godクラス」化まっしぐらです。
そこでこの記事では、HPの管理はHPコンポーネントに任せて、「瀕死時の音響演出」だけを担当する小さなコンポーネントとして、
LowHealthAudio(瀕死音効) を実装していきます。
HPコンポーネントを監視し、HPが一定以下になったら AudioLowPassFilter を自動でオン/オフするコンポーネントです。
【Unity】瀕死時の世界をこもらせる!「LowHealthAudio」コンポーネント
まずは、この記事だけで完結するように、シンプルな HP コンポーネントと LowHealthAudio コンポーネントをセットで用意します。
HPの実装はあくまで例ですが、そのままコピペして動作確認できるようにしてあります。
フルコード:HPコンポーネント(例)
using UnityEngine;
/// <summary>とてもシンプルなHPコンポーネントの例</summary>
public class SimpleHealth : MonoBehaviour
{
[Header("最大HP")]
[SerializeField] private float maxHealth = 100f;
[Header("現在HP(デバッグ用にInspector表示)")]
[SerializeField] private float currentHealth = 100f;
/// <summary>現在のHPを0〜maxHealthの範囲で返す</summary>
public float CurrentHealth => currentHealth;
/// <summary>最大HPを返す</summary>
public float MaxHealth => maxHealth;
/// <summary>0〜1の正規化HP(1=満タン, 0=死亡)を返す</summary>
public float NormalizedHealth => maxHealth > 0f ? currentHealth / maxHealth : 0f;
/// <summary>現在HPが0かどうか</summary>
public bool IsDead => currentHealth <= 0f;
private void Awake()
{
// 開始時は最大HPでスタート
currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);
}
/// <summary>ダメージを受ける(マイナス値を渡すと回復)</summary>
public void ApplyDamage(float amount)
{
// 減算してクランプ
currentHealth = Mathf.Clamp(currentHealth - amount, 0f, maxHealth);
}
/// <summary>HPを直接設定する(0〜maxHealthにクランプ)</summary>
public void SetHealth(float value)
{
currentHealth = Mathf.Clamp(value, 0f, maxHealth);
}
}
フルコード:LowHealthAudio(瀕死音効)
using UnityEngine;
/// <summary>
/// HPコンポーネントを監視し、瀕死時にローパスフィルターをかけるコンポーネント。
/// ・HPのしきい値を下回ったら AudioLowPassFilter を有効化
/// ・通常時はフィルターを無効化
/// ・フェード時間付きでカットオフ周波数を補間
/// HPの実装には依存せず、「SimpleHealth」などのHPコンポーネントを参照して使う想定です。
/// </summary>
[RequireComponent(typeof(AudioLowPassFilter))]
public class LowHealthAudio : MonoBehaviour
{
[Header("参照するHPコンポーネント")]
[SerializeField] private SimpleHealth health; // 例としてSimpleHealthを参照
[Header("瀕死判定")]
[Tooltip("この割合を下回ったら瀕死とみなす(0〜1)。例:0.3 = HP30%未満で瀕死")]
[Range(0f, 1f)]
[SerializeField] private float lowHealthThreshold = 0.3f;
[Header("ローパス設定")]
[Tooltip("通常時のカットオフ周波数(Hz)。大きいほど高音まで聞こえる")]
[SerializeField] private float normalCutoffFrequency = 22000f;
[Tooltip("瀕死時のカットオフ周波数(Hz)。小さいほどこもった音になる")]
[SerializeField] private float lowHealthCutoffFrequency = 800f;
[Header("フェード設定")]
[Tooltip("通常 <-> 瀕死 の切り替えにかける時間(秒)")]
[SerializeField] private float transitionDuration = 0.5f;
// 実際に操作するローパスフィルター
private AudioLowPassFilter lowPassFilter;
// 現在のターゲット周波数(瀕死 or 通常)
private float targetCutoffFrequency;
// フェード用のタイマー
private float transitionTimer = 0f;
// 現在が瀕死状態かどうか
private bool isLowHealth = false;
private void Reset()
{
// コンポーネントがアタッチされたタイミングで、同じGameObjectからSimpleHealthを探す
health = GetComponent<SimpleHealth>();
}
private void Awake()
{
lowPassFilter = GetComponent<AudioLowPassFilter>();
// 開始時は通常状態として初期化
isLowHealth = false;
targetCutoffFrequency = normalCutoffFrequency;
lowPassFilter.cutoffFrequency = normalCutoffFrequency;
lowPassFilter.enabled = true; // 常に有効にしておき、周波数だけ操作する
}
private void Update()
{
if (health == null)
{
// HPコンポーネントが設定されていなければ何もしない
return;
}
// 現在のHP割合(0〜1)
float normalizedHealth = health.NormalizedHealth;
// しきい値を下回ったら瀕死とみなす
bool shouldBeLowHealth = normalizedHealth < lowHealthThreshold;
// 状態が変わったタイミングでフェードを開始
if (shouldBeLowHealth != isLowHealth)
{
isLowHealth = shouldBeLowHealth;
transitionTimer = 0f;
// ターゲット周波数を更新
targetCutoffFrequency = isLowHealth ? lowHealthCutoffFrequency : normalCutoffFrequency;
}
// フェード処理(現在のcutoffFrequencyをtargetに向かって補間)
if (transitionDuration > 0f)
{
transitionTimer += Time.deltaTime;
float t = Mathf.Clamp01(transitionTimer / transitionDuration);
// 現在の周波数からターゲットへ線形補間
float current = lowPassFilter.cutoffFrequency;
float next = Mathf.Lerp(current, targetCutoffFrequency, t);
lowPassFilter.cutoffFrequency = next;
}
else
{
// フェード時間が0なら即座に切り替え
lowPassFilter.cutoffFrequency = targetCutoffFrequency;
}
}
/// <summary>
/// 外部から瀕死しきい値を変更したい場合のAPI。
/// 例:難易度によって瀕死ラインを変えたいときなど。
/// </summary>
public void SetLowHealthThreshold(float normalizedThreshold)
{
lowHealthThreshold = Mathf.Clamp01(normalizedThreshold);
}
/// <summary>
/// 現在、瀕死状態と判定されているかどうかを返す。
/// UIの点滅やポストエフェクトと連動させたいときなどに使えます。
/// </summary>
public bool IsLowHealthState => isLowHealth;
}
ポイント
- HPのロジック(SimpleHealth) と 音響演出(LowHealthAudio) を完全に分離
[RequireComponent(typeof(AudioLowPassFilter))]によって、アタッチ漏れを防止[SerializeField] privateで、カプセル化しつつ Inspector から調整可能- フェード時間を持たせて、瀕死・回復の切り替えを自然に演出
使い方の手順
-
プレイヤー(または敵)にHPコンポーネントを付ける
例として、プレイヤーの GameObject(Player)にSimpleHealthをアタッチします。Max Healthに 100 など適当な数値を設定Current Healthは基本的に最大値にしておきます
-
オーディオの出力先に AudioLowPassFilter を用意する
- シーン内の
AudioListenerが付いているカメラ、またはマスターミックス用の GameObject にAudioLowPassFilterをアタッチします。 - この GameObject に
LowHealthAudioをアタッチします(RequireComponentにより自動で AudioLowPassFilter も付きます)。
- シーン内の
-
LowHealthAudio と HP を接続する
LowHealthAudioのHealthフィールドに、プレイヤーのSimpleHealthをドラッグ&ドロップで割り当てます。Low Health Thresholdに 0.3(=30%)などを設定して、「どのくらい減ったら瀕死とみなすか」を決めます。Low Health Cutoff Frequencyを 800〜1500Hz くらいにすると、かなり「こもった」印象になります。Transition Durationを 0.3〜0.7 秒くらいにすると、自然にフェードしてくれます。
-
ダメージを与えて挙動を確認する
簡単なデバッグ用スクリプトを作って、キー入力でダメージを与えてみましょう。
例えばプレイヤーにこんなスクリプトを追加します:using UnityEngine; /// <summary>テスト用:スペースキーでダメージを与えるだけのスクリプト</summary> public class DebugDamageInput : MonoBehaviour { [SerializeField] private SimpleHealth health; [SerializeField] private float damagePerHit = 10f; private void Reset() { health = GetComponent<SimpleHealth>(); } private void Update() { if (health == null) return; // スペースキーでダメージ if (Input.GetKeyDown(KeyCode.Space)) { health.ApplyDamage(damagePerHit); Debug.Log($"Damage! Current HP: {health.CurrentHealth}"); } } }ゲームを再生して、スペースキーを連打してHPを減らしていくと、しきい値を下回った瞬間から音がこもり始めるのが分かるはずです。
回復処理を追加すれば、HPが回復したときにローパスが解除されていくのも確認できます。
具体的な使用例
-
プレイヤーの瀕死演出
プレイヤーの HP が 20% を切ったら、世界全体の音をこもらせることで「意識が遠のいている」感覚を演出できます。
さらに、画面のビネットや赤いフラッシュと組み合わせると、かなりリッチな瀕死表現になります。 -
ボス戦の緊張感アップ
ボスの HP が残りわずかになったとき、ボス側の攻撃音だけローパスするような応用も可能です。
その場合は、ボス専用の AudioSource / AudioLowPassFilter にこのコンポーネントを付けて、ボスの HP を参照するようにすればOKです。 -
動く床やギミックの「ダメージゾーン」演出
ダメージ床に乗っている間だけプレイヤーの HP が減るようなギミックがある場合、
HP が減少していき瀕死ラインに近づくにつれて徐々に音がこもっていくので、「そろそろ危ないぞ」という警告になります。
メリットと応用
この LowHealthAudio コンポーネントを使う最大のメリットは、「HPの管理」と「音の演出」を完全に分離できることです。
- HPの仕様を変更しても、LowHealthAudio は NormalizedHealth さえ提供されていればそのまま使える
- 瀕死ラインやフェード時間などを プレハブ単位で調整できる ので、シーンごと・キャラごとに演出を変えやすい
- 「瀕死音効」だけを別プレハブにしておけば、どのシーンにも簡単にコピペして再利用できる
レベルデザインの観点でも、プレイヤーのHPをいじることなく、音のしきい値だけを調整して難易度の体感を変えることができます。
例えば同じダメージ量でも、瀕死ラインを 0.5 に上げておけば、プレイヤーは「すぐにピンチになるゲームだ」と感じやすくなります。
改造案:HP割合に応じてカットオフを連続的に変える
今の実装は「しきい値を境に ON / OFF」ですが、
HPが減るほどどんどんこもっていくようにしたい場合は、次のような関数を追加してみましょう:
/// <summary>
/// HP割合に応じてローパスのカットオフ周波数を連続的に変化させる例。
/// 例えば、HP100%でnormalCutoff、HP0%でlowHealthCutoffになるように補間する。
/// (Update内などから呼び出して使う)
/// </summary>
private void UpdateCutoffByHealth()
{
if (health == null) return;
// 0(死亡)〜1(満タン)のHP割合
float h = Mathf.Clamp01(health.NormalizedHealth);
// HPが減るほどlowHealthCutoffに近づける
float target = Mathf.Lerp(lowHealthCutoffFrequency, normalCutoffFrequency, h);
// フェード時間を使ってなめらかに反映
if (transitionDuration > 0f)
{
transitionTimer += Time.deltaTime;
float t = Mathf.Clamp01(transitionTimer / transitionDuration);
lowPassFilter.cutoffFrequency = Mathf.Lerp(lowPassFilter.cutoffFrequency, target, t);
}
else
{
lowPassFilter.cutoffFrequency = target;
}
}
このように、「HPをどう解釈して音に反映するか」 のロジックを関数に切り出しておけば、
後から「HP50%以下だけ急にこもらせる」「HPが減るときだけフェードを速くする」などのアレンジもしやすくなります。
「全部Updateに書く」のを卒業して、「HPを見るだけのコンポーネント」「音をいじるだけのコンポーネント」といった小さな責務に分割していくと、
プロジェクトが大きくなっても破綻しにくい構成になります。
ぜひ LowHealthAudio をベースに、自分のゲームに合った瀕死演出コンポーネントを育てていきましょう。
