Unityを触り始めると、つい何でもかんでも Update() に書きがちですよね。HPの管理、移動処理、入力処理、BGM、SE……すべてを1つのスクリプトの Update() に詰め込むと、最初は動いていても、あとから仕様変更が入ったときに「どこを触ればいいのか分からない」「ちょっと直したら別のバグが出た」という状態になりがちです。

そこでおすすめなのが、機能ごとに小さなコンポーネントに分割する設計です。今回作るのは、

  • HPが少なくなったときだけ「ドクン…ドクン…」という心拍音を鳴らす
  • HPが回復したら自動で心拍音を止める

という、「低HP時の心拍音だけ」を担当する専用コンポーネント HeartbeatSound です。
HP管理やダメージ処理のロジックからは「心拍音」のことを忘れられるようにしておくと、スクリプト同士の結合度が下がってメンテしやすくなります。

【Unity】瀕死状態を“音”で演出!「HeartbeatSound」コンポーネント

フルコード


using UnityEngine;

/// <summary> 
/// シンプルなHPコンポーネントの例。
/// 実運用では別のHPスクリプトを使ってもOKですが、
/// HeartbeatSound から参照するためのインターフェースとして用意しています。
/// </summary>
public class SimpleHealth : MonoBehaviour
{
    [SerializeField] private float maxHp = 100f;
    [SerializeField] private float currentHp = 100f;

    /// <summary> 現在HP(読み取り専用プロパティ)</summary>
    public float CurrentHp => currentHp;

    /// <summary> 最大HP(読み取り専用プロパティ)</summary>
    public float MaxHp => maxHp;

    /// <summary> ダメージを受ける(マイナス値で回復も可能)</summary>
    public void ApplyDamage(float amount)
    {
        currentHp = Mathf.Clamp(currentHp - amount, 0f, maxHp);
    }

    /// <summary> HPを全回復させる</summary>
    public void HealFull()
    {
        currentHp = maxHp;
    }
}

/// <summary>
/// HPが一定割合を下回ったときに、心拍音を周期的に再生するコンポーネント。
/// ・HPの参照元(例: SimpleHealth)
/// ・心拍音のAudioClip
/// ・再生間隔、音量
/// などをインスペクタから調整できます。
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class HeartbeatSound : MonoBehaviour
{
    [Header("HP 参照設定")]
    [SerializeField]
    private SimpleHealth health; 
    // 実プロジェクトでは IHealth インターフェースなどに差し替えてもOK

    [Header("心拍音設定")]
    [SerializeField]
    private AudioClip heartbeatClip; // ドクン…の音声クリップ

    [Tooltip("HP がこの割合(0〜1)を下回ったら心拍音を鳴らす")]
    [SerializeField, Range(0.01f, 1f)]
    private float lowHpThresholdRate = 0.3f;

    [Tooltip("心拍音を鳴らす間隔(秒)。数値を小さくするとドクドクが早くなる")]
    [SerializeField, Min(0.05f)]
    private float heartbeatInterval = 1.0f;

    [Tooltip("心拍音の音量")]
    [SerializeField, Range(0f, 1f)]
    private float heartbeatVolume = 0.8f;

    [Header("オプション")]
    [Tooltip("瀕死になるほど心拍間隔を短くするかどうか")]
    [SerializeField]
    private bool useDynamicInterval = true;

    [Tooltip("HP が 0% のときの最短間隔(useDynamicInterval が true のときのみ有効)")]
    [SerializeField, Min(0.05f)]
    private float minHeartbeatInterval = 0.35f;

    private AudioSource audioSource;
    private float heartbeatTimer;
    private bool isLowHp; // 現在「低HP状態」かどうか

    private void Awake()
    {
        // 同一GameObject上のAudioSourceを取得
        audioSource = GetComponent<AudioSource>();

        // 心拍音はワンショット再生にするので、ループは切っておく
        audioSource.loop = false;

        // BGMなどと混ざらないように、出力設定やSpatial Blendは
        // プロジェクトの方針に合わせてインスペクタ側で調整しましょう。
    }

    private void Update()
    {
        if (health == null || heartbeatClip == null)
        {
            // 参照が設定されていない場合は何もしない
            return;
        }

        // 現在のHP割合(0〜1)
        float hpRate = health.MaxHp > 0f ? health.CurrentHp / health.MaxHp : 0f;

        // 低HP状態かどうか判定
        bool nowLowHp = hpRate <= lowHpThresholdRate;

        // 状態が切り替わった瞬間の処理
        if (nowLowHp && !isLowHp)
        {
            // 通常HP → 低HP に切り替わった瞬間
            OnEnterLowHp();
        }
        else if (!nowLowHp && isLowHp)
        {
            // 低HP → 通常HP に戻った瞬間
            OnExitLowHp();
        }

        isLowHp = nowLowHp;

        // 低HP状態でなければ心拍音は鳴らさない
        if (!isLowHp)
        {
            return;
        }

        // タイマーを進める
        heartbeatTimer -= Time.deltaTime;

        if (heartbeatTimer <= 0f)
        {
            // 心拍音を1回だけ再生
            PlayHeartbeat();

            // 次回までのインターバルを再計算
            heartbeatTimer = GetCurrentInterval(hpRate);
        }
    }

    /// <summary>
    /// 低HP状態に入った瞬間に呼ばれる処理
    /// </summary>
    private void OnEnterLowHp()
    {
        // 最初の心拍音をすぐ鳴らしたいので、タイマーを0にリセット
        heartbeatTimer = 0f;
    }

    /// <summary>
    /// 低HP状態から抜けた瞬間に呼ばれる処理
    /// </summary>
    private void OnExitLowHp()
    {
        // タイマーをリセット
        heartbeatTimer = 0f;
    }

    /// <summary>
    /// 現在のHP割合に応じて、心拍の間隔を返す。
    /// HPが低いほど心拍を早くする演出に使います。
    /// </summary>
    private float GetCurrentInterval(float hpRate)
    {
        if (!useDynamicInterval)
        {
            return heartbeatInterval;
        }

        // hpRate: 0〜lowHpThresholdRate の範囲で線形補間する
        // HPが lowHpThresholdRate のとき → heartbeatInterval
        // HPが 0 のとき → minHeartbeatInterval
        float t = 0f;
        if (lowHpThresholdRate > 0f)
        {
            // 0〜1 に正規化
            t = Mathf.Clamp01(1f - (hpRate / lowHpThresholdRate));
        }

        // t=0 → heartbeatInterval, t=1 → minHeartbeatInterval になるように補間
        return Mathf.Lerp(heartbeatInterval, minHeartbeatInterval, t);
    }

    /// <summary>
    /// 心拍音を1回だけ再生する
    /// </summary>
    private void PlayHeartbeat()
    {
        // すでに同じクリップが再生中でも、ワンショットで上乗せしてOKな演出ならこれで十分
        audioSource.PlayOneShot(heartbeatClip, heartbeatVolume);
    }

    /// <summary>
    /// 外部から強制的に心拍音を止めたい場合に呼ぶメソッド。
    /// 例えば、ゲームオーバー画面に遷移した瞬間などで使用できます。
    /// </summary>
    public void StopHeartbeatImmediately()
    {
        isLowHp = false;
        heartbeatTimer = 0f;
        audioSource.Stop();
    }
}

使い方の手順

  1. プレイヤー(または敵)に HP コンポーネントを付ける
    例として、プレイヤーの GameObject(Player)に SimpleHealth をアタッチします。
    Max Hp を 100 などに設定
    ・シーン開始時の HP を変えたい場合は Current Hp を調整
  2. 心拍音用の AudioSource を用意する
    同じく PlayerAudioSource コンポーネントを追加します。
    Play On Awake はオフ(自動再生しないように)
    Loop もオフ(ワンショット再生のみ)
    ・2Dゲームであれば Spatial Blend を 0(2D)に、3Dゲームなら 1(3D)寄りにするなど、プロジェクトに合わせて調整しましょう。
  3. HeartbeatSound をアタッチして設定する
    PlayerHeartbeatSound コンポーネントを追加します。
    インスペクタで以下を設定します。
    • Health:先ほどアタッチした SimpleHealth をドラッグ&ドロップ
    • Heartbeat Clip:心拍音の AudioClip(「ドクン…」のSE)を設定
    • Low Hp Threshold Rate:例として 0.3 にすると、HP 30% 以下で心拍開始
    • Heartbeat Interval:通常の心拍間隔(例: 1.0 秒)
    • Use Dynamic Interval:チェックを入れると、HP が減るほど間隔が短くなる
    • Min Heartbeat Interval:HP 0% 付近の最短間隔(例: 0.35 秒)
  4. ダメージ処理と組み合わせてテストする
    例えばテスト用に、スペースキーでダメージを受ける簡単なスクリプトを Player に追加してみましょう。
    
    using UnityEngine;
    
    public class DamageOnSpace : MonoBehaviour
    {
        [SerializeField] private SimpleHealth health;
        [SerializeField] private float damageAmount = 10f;
    
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                health.ApplyDamage(damageAmount);
                Debug.Log($"Damage! Current HP = {health.CurrentHp}");
            }
        }
    }
    

    この状態でゲームを再生し、スペースキーを何度か押して HP を減らしていくと、設定した閾値を下回ったタイミングで「ドクン…ドクン…」と心拍音が鳴り始めます。HP を回復する処理(HealFull() など)を呼べば、心拍音は自動で止まります。

応用例として、

  • プレイヤーだけでなく、ボス敵にも HeartbeatSound を付けて、瀕死状態の演出に使う
  • 動く足場の上に乗ったときだけ、プレイヤーの HP が徐々に減っていき、一定以下で心拍音が鳴るトラップ演出

といった使い方も簡単にできます。

メリットと応用

HeartbeatSound を「心拍音だけ」に責務を絞ったコンポーネントにしておくと、次のようなメリットがあります。

  • プレハブの再利用性が上がる
    プレイヤーのプレハブに SimpleHealthHeartbeatSound をセットで仕込んでおけば、別シーンにドラッグ&ドロップするだけで「瀕死時の心拍演出付きプレイヤー」がすぐ使えます。
  • レベルデザインが楽になる
    HPの値や心拍音の間隔はインスペクタから調整できるので、レベルデザイナーやサウンド担当がコードを書かずに「どのくらいの瀕死感にするか」を調整できます。
  • 責務が分離されているので、仕様変更に強い
    「瀕死のときは画面も赤くフェードさせたい」「コントローラを振動させたい」といった要望が出たときも、HeartbeatSound には手を入れず、別の「VignetteOnLowHp」「GamepadRumbleOnLowHp」などのコンポーネントを追加するだけで対応できます。

最後に、簡単な「改造案」として、心拍音が鳴るたびに画面を一瞬だけ暗くする演出を追加するメソッド例を載せておきます。
(実際にはポストプロセスやUI Imageのアルファをいじるなど、プロジェクトに合わせて中身を差し替えてください。)


/// <summary>
/// 心拍のタイミングで画面を一瞬暗くする演出(疑似コード)
/// HeartbeatSound.PlayHeartbeat() の最後から呼び出すイメージです。
/// </summary>
private void PulseScreenDarkness()
{
    // ここでは Debug.Log だけにしておきますが、
    // 実際には Post Processing の Vignette 強度を上げたり、
    // 画面全体のフェード用 Image のアルファを一瞬だけ上げるなどの処理を書くと良いです。
    Debug.Log("Pulse screen darkness with heartbeat!");
}

このように、心拍音コンポーネントを起点にして「瀕死演出」をどんどん小さなコンポーネントへ分割していくと、Godクラスを避けつつ、拡張しやすい設計になっていきます。ぜひ自分のプロジェクト用にカスタマイズしてみてください。