Unityを触り始めた頃は、つい「とりあえず全部PlayerのスクリプトのUpdateに書いてしまう」ことが多いですよね。移動、攻撃、HP管理、UI更新、画面エフェクト…すべてが1つのクラスに詰め込まれていくと、だんだん手が付けられなくなります。

とくに「HPが減ったら画面を赤くする」「ダメージを受けたらエフェクトを出す」といった演出系は、ゲームロジックとは別物なのに、同じスクリプトに書かれがちです。

そこでこの記事では、「HPが減ると画面の四隅を赤黒く暗くする」というビネット演出だけを担当する、小さなコンポーネント VignetteDamage(瀕死視界) を作っていきます。HPロジックと画面演出を分離しておくと、あとから演出だけを差し替えたり、別のゲームでも使い回したりしやすくなります。

【Unity】HP減少で自動ビネット演出!「VignetteDamage」コンポーネント

ここでは Unity UI(Canvas + Image)を使って、HPに応じて画面の四隅が赤黒く暗くなるビネット効果を実装します。ポストプロセスを使わずに、プレーンなUnity UIだけで完結する構成です。

前提:シンプルなHP値を受け取るだけのコンポーネント

このコンポーネントは「HPの現在値と最大値」を外部から受け取るだけに絞っています。プレイヤーのHP管理クラスや敵のHPクラスなどから、

  • 現在HPが変わったタイミングで SetHealth を呼ぶ
  • または毎フレーム SetHealth を呼ぶ

という使い方を想定しています。どのオブジェクトのHPかは気にせず、「0〜1に正規化されたHP割合」だけをもらって画面演出を更新する、という分離された責務になっています。

フルコード:VignetteDamage.cs


using UnityEngine;
using UnityEngine.UI;

namespace Samples.UI
{
    /// <summary>
    /// HPが減ると画面の四隅を赤黒く暗くするビネット演出コンポーネント。
    /// 
    /// 責務:
    /// - HP割合(0〜1)を受け取り、UI Imageの色・透明度を補間して更新する
    /// - フェードのなめらかさ(追従速度)を制御する
    /// - エディタ上でプレビューしやすいように、インスペクターからテスト値を指定可能にする
    /// 
    /// 注意:
    /// - HPの計算やダメージ処理は別コンポーネントに任せること(Single Responsibility)
    /// - このコンポーネントはCanvas配下のImageにアタッチして使う想定
    /// </summary>
    [DisallowMultipleComponent]
    [RequireComponent(typeof(Image))]
    public class VignetteDamage : MonoBehaviour
    {
        [Header("HP設定")]

        [Tooltip("最大HP。外部から割合を直接渡す場合は使わなくてもOKです。")]
        [SerializeField] private float maxHealth = 100f;

        [Tooltip("現在HP。外部のHP管理コンポーネントから更新してもらう値です。")]
        [SerializeField] private float currentHealth = 100f;

        [Tooltip("外部からSetHealthRatioを使って割合(0〜1)だけを渡すかどうか。trueならmaxHealth/currentHealthは無視されます。")]
        [SerializeField] private bool useExternalHealthRatio = false;

        [Header("ビネット演出")]

        [Tooltip("HPが最大のときのビネット色(通常は完全に透明に近い色)")]
        [SerializeField] private Color safeColor = new Color(0f, 0f, 0f, 0f);

        [Tooltip("HPが0のときのビネット色(瀕死時の赤黒い色)")]
        [SerializeField] private Color dangerColor = new Color(0.4f, 0f, 0f, 0.8f);

        [Tooltip("HPがこの割合を下回ると、ビネットが目に見えて濃くなり始める閾値(0〜1)")]
        [Range(0f, 1f)]
        [SerializeField] private float startVignetteThreshold = 0.5f;

        [Tooltip("ビネット変化のカーブ。X:HP割合(0〜1), Y:ビネット強度(0〜1)")]
        [SerializeField] private AnimationCurve intensityCurve =
            AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);

        [Header("フェード設定")]

        [Tooltip("現在のHP状態にビネットが追従する速度。大きいほど即時、小さいほどなめらかに遅れて変化します。")]
        [SerializeField, Min(0f)] private float followSpeed = 8f;

        [Tooltip("Time.timeScaleの影響を受けずにフェードさせたい場合はtrueにします。")]
        [SerializeField] private bool useUnscaledTime = true;

        [Header("デバッグ / エディタ用")]

        [Tooltip("エディタ上でプレビューするためのHP割合(0〜1)。Play中は実際の値で上書きされます。")]
        [Range(0f, 1f)]
        [SerializeField] private float debugHealthRatio = 1f;

        [Tooltip("エディタ上で常にdebugHealthRatioをプレビューするかどうか。")]
        [SerializeField] private bool previewInEditor = true;

        // 実際に色を変える対象のImage
        private Image _vignetteImage;

        // 現在のビネット強度(0〜1)
        private float _currentIntensity = 0f;

        // 目標のビネット強度(0〜1)
        private float _targetIntensity = 0f;

        // 外部から渡されたHP割合(0〜1)。useExternalHealthRatioがtrueのときのみ使用。
        private float _externalHealthRatio = 1f;

        private void Awake()
        {
            _vignetteImage = GetComponent<Image>();

            // 安全側の初期化。nullのままだとエラーになるので念のため
            if (_vignetteImage == null)
            {
                Debug.LogError("[VignetteDamage] Imageコンポーネントが見つかりません。Canvas配下のUI Imageにアタッチしてください。", this);
                enabled = false;
                return;
            }

            // 初期状態の色を反映
            UpdateVignetteImmediate(1f);
        }

        private void Update()
        {
            float deltaTime = useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
            if (deltaTime <= 0f) return;

            // 現在のHP割合(0〜1)を取得
            float healthRatio = GetCurrentHealthRatio();

            // HP割合から目標ビネット強度を計算
            _targetIntensity = EvaluateIntensity(healthRatio);

            // なめらかに補間
            _currentIntensity = Mathf.Lerp(
                _currentIntensity,
                _targetIntensity,
                1f - Mathf.Exp(-followSpeed * deltaTime) // 指数補間でフレームレート依存を軽減
            );

            // 実際の色を更新
            ApplyVignetteColor(_currentIntensity);
        }

#if UNITY_EDITOR
        /// <summary>
        /// エディタ上でインスペクターの値を変更したときに呼ばれます。
        /// プレビュー用にビネットを更新します。
        /// </summary>
        private void OnValidate()
        {
            if (!previewInEditor) return;

            // 再生中はUpdateに任せる
            if (Application.isPlaying) return;

            // エディタ上でのプレビュー用に即時反映
            if (_vignetteImage == null)
            {
                _vignetteImage = GetComponent<Image>();
            }

            if (_vignetteImage == null) return;

            UpdateVignetteImmediate(debugHealthRatio);
        }
#endif

        /// <summary>
        /// 外部からHPの現在値と最大値を渡してもらうためのAPI。
        /// ここで割合(0〜1)に正規化して内部で保持します。
        /// </summary>
        /// <param name="current">現在HP</param>
        /// <param name="max">最大HP(0より大きい値)</param>
        public void SetHealth(float current, float max)
        {
            if (max <= 0f)
            {
                Debug.LogWarning("[VignetteDamage] maxは0より大きい必要があります。", this);
                return;
            }

            float ratio = Mathf.Clamp01(current / max);
            SetHealthRatio(ratio);
        }

        /// <summary>
        /// 外部から0〜1のHP割合だけを直接渡すためのAPI。
        /// HP管理側で割合を計算している場合はこちらを使うとシンプルです。
        /// </summary>
        /// <param name="ratio">HP割合(0〜1)</param>
        public void SetHealthRatio(float ratio)
        {
            _externalHealthRatio = Mathf.Clamp01(ratio);
        }

        /// <summary>
        /// 内部状態から現在のHP割合(0〜1)を取得します。
        /// useExternalHealthRatioがtrueなら、外部から渡された割合をそのまま使います。
        /// falseなら、currentHealth/maxHealthから計算します。
        /// </summary>
        private float GetCurrentHealthRatio()
        {
            if (useExternalHealthRatio)
            {
                return _externalHealthRatio;
            }

            if (maxHealth <= 0f) return 1f; // 不正値の場合は安全側に倒す

            float ratio = Mathf.Clamp01(currentHealth / maxHealth);
            return ratio;
        }

        /// <summary>
        /// HP割合からビネット強度(0〜1)を計算します。
        /// startVignetteThresholdよりHPが高い間はほぼ0、そこから下がるほど強くなります。
        /// intensityCurveで細かいカーブ調整も可能です。
        /// </summary>
        /// <param name="healthRatio">HP割合(0〜1)</param>
        private float EvaluateIntensity(float healthRatio)
        {
            // HP割合が高い間は強度0(もしくはかなり小さく)に保つ
            if (healthRatio >= startVignetteThreshold)
            {
                return 0f;
            }

            // 0〜startVignetteThresholdの範囲を0〜1に正規化
            float t = Mathf.InverseLerp(startVignetteThreshold, 0f, healthRatio);

            // カーブを適用して最終的な強度を決定
            float curveValue = intensityCurve.Evaluate(t);
            float intensity = Mathf.Clamp01(curveValue);

            return intensity;
        }

        /// <summary>
        /// 指定した強度(0〜1)でビネット色を反映します。
        /// safeColorとdangerColorの間を補間します。
        /// </summary>
        /// <param name="intensity">ビネット強度(0〜1)</param>
        private void ApplyVignetteColor(float intensity)
        {
            if (_vignetteImage == null) return;

            Color targetColor = Color.Lerp(safeColor, dangerColor, intensity);
            _vignetteImage.color = targetColor;
        }

        /// <summary>
        /// 即時にHP割合を反映してビネットを更新したいときに使う内部メソッド。
        /// エディタプレビューやAwake時の初期化で利用します。
        /// </summary>
        /// <param name="healthRatio">HP割合(0〜1)</param>
        private void UpdateVignetteImmediate(float healthRatio)
        {
            float intensity = EvaluateIntensity(Mathf.Clamp01(healthRatio));
            _currentIntensity = intensity;
            _targetIntensity = intensity;
            ApplyVignetteColor(intensity);
        }

        #region サンプル用:インスペクターからHPをテストできるプロパティ

        // これらのプロパティはインスペクター上での操作を少し便利にするためのものです。
        // 実運用では外部のHP管理コンポーネントからSetHealth/SetHealthRatioを呼ぶのが基本です。

        /// <summary>
        /// インスペクターから現在HPを直接変更したいときのためのプロパティ。
        /// </summary>
        public void EditorSetCurrentHealth(float value)
        {
            currentHealth = Mathf.Clamp(value, 0f, maxHealth);
        }

        #endregion
    }
}

使い方の手順

ここからは、実際にシーンに組み込む手順を具体的に見ていきましょう。例として「プレイヤーのHPが減ると画面が赤黒くなる」ケースを扱いますが、敵やボス専用の演出としても同じ手順で使えます。

手順①:ビネット用のUIを用意する

  1. シーンに Canvas を用意します(既にある場合はそれを使用)。
    推奨設定:
    • Render Mode: Screen Space – Overlay
    • Canvas Scaler: Scale With Screen Size(画面解像度に追従させるため)
  2. Canvas の子として UI > Image を作成し、名前を VignetteDamageImage などにします。
  3. Image の RectTransform を以下のように設定します:
    • Anchor Min: (0, 0)
    • Anchor Max: (1, 1)
    • Pos X / Pos Y / Width / Height: 0(ストレッチ状態)

    これで画面全体を覆うフルスクリーンのImageになります。

  4. Image の Source Image に、四隅が暗く中央が透明な「ビネット用テクスチャ」を設定すると、より自然な効果になります。
    テクスチャがない場合は、一旦真っ黒の1×1テクスチャでも動きますが、四隅だけ暗くするには専用のテクスチャがあると便利です。

手順②:VignetteDamageコンポーネントをアタッチする

  1. 先ほど作成した VignetteDamageImage オブジェクトを選択します。
  2. Add Component ボタンから VignetteDamage を追加します。
  3. インスペクター上で以下のパラメータを調整します:
    • safeColor: 通常時の色(完全に透明に近い色)。例:RGBA(0,0,0,0)
    • dangerColor: 瀕死時の色。例:RGBA(0.4, 0, 0, 0.85)
    • startVignetteThreshold: 0.5 にすると HP50%以下から徐々に暗くなります。
    • followSpeed: 8〜12 くらいにすると、なめらかに追従しつつレスポンスも良い感じになります。
    • previewInEditor: チェックを入れると、debugHealthRatio のスライダーでエディタ上から演出を確認できます。

手順③:プレイヤーのHP管理と連携する

ここでは、プレイヤーにシンプルなHP管理コンポーネントを用意し、ダメージを受けたときに VignetteDamage にHP割合を渡す例を示します。


using UnityEngine;
using Samples.UI; // VignetteDamageのnamespaceを参照

/// <summary>
/// シンプルなプレイヤーHP管理コンポーネントの例。
/// 実際のゲームでは、ダメージ処理や死亡処理などを
/// 別コンポーネントに分けていくとさらに綺麗になります。
/// </summary>
public class PlayerHealth : MonoBehaviour
{
    [SerializeField] private float maxHealth = 100f;
    [SerializeField] private float currentHealth = 100f;

    [Header("瀕死視界演出")]
    [Tooltip("画面のビネット演出を担当するコンポーネント")]
    [SerializeField] private VignetteDamage vignetteDamage;

    private void Awake()
    {
        // 念のため初期HPを最大にしておく
        currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);

        // VignetteDamageが指定されていなければ自動検索(任意)
        if (vignetteDamage == null)
        {
            vignetteDamage = FindObjectOfType<VignetteDamage>();
        }

        // 初期状態のHPを反映
        UpdateVignette();
    }

    /// <summary>
    /// ダメージを受ける処理の例。
    /// 実際には敵の攻撃やトラップなどから呼ばれる想定です。
    /// </summary>
    /// <param name="amount">受けるダメージ量(正の値)</param>
    public void TakeDamage(float amount)
    {
        if (amount <= 0f) return;

        currentHealth -= amount;
        currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);

        // HP変化をビネット演出に反映
        UpdateVignette();

        // ここで死亡処理などを行う
        if (currentHealth <= 0f)
        {
            Debug.Log("Player Dead");
        }
    }

    private void UpdateVignette()
    {
        if (vignetteDamage == null) return;

        float ratio = maxHealth > 0f ? currentHealth / maxHealth : 1f;
        vignetteDamage.SetHealthRatio(ratio);
    }
}

このように、PlayerHealth は「HPの増減」だけVignetteDamage は「画面演出」だけを担当させることで、後から「別の演出に差し替える」「UIだけデザイナーに渡す」といった変更がしやすくなります。

手順④:他の用途への応用例

  • 敵ボス専用の瀕死演出
    ボスのHPが30%を切ったら画面全体が赤黒くなり、緊張感を演出する。
    ボスのHP管理クラスから SetHealthRatio を呼ぶだけでOKです。
  • 毒状態・出血状態の視界演出
    HPではなく「状態異常の強度」を0〜1で渡すことで、毒が強くなるほど緑っぽいビネットになる、などの応用も可能です(色を緑系に変更)。
  • 暗闇エリアでの視界制限
    プレイヤーの「松明の明るさ」や「スタミナ」を0〜1で渡し、スタミナが切れると画面四隅が暗くなっていく…といった演出にも使えます。

メリットと応用

このコンポーネントを使うメリット

  • 責務が明確
    HPロジックと画面演出を完全に分離しているので、どこを修正すれば何が変わるのかが分かりやすいです。
  • プレハブ化しやすい
    Canvas配下に VignetteDamageImage + VignetteDamage をまとめてプレハブ化しておけば、どのシーンでもドラッグ&ドロップだけで「瀕死視界」を導入できます。
  • レベルデザインが楽になる
    ステージごとに dangerColorstartVignetteThreshold を変えるだけで、「このステージはちょっとダークめ」「このステージは優しめ」といった雰囲気調整がしやすくなります。
  • 他のゲームでも再利用可能
    HPの値をどこから持ってくるかは完全に外部委譲しているため、新しいゲームプロジェクトでも SetHealthRatio さえ呼べばそのまま使い回せます。

簡単な改造案:ダメージを受けた瞬間だけ一瞬強く光らせる

「じわじわ暗くなる」だけでなく、「ダメージを受けた瞬間にだけ、ビネットを一瞬強くする」演出を追加したい場合は、以下のようなメソッドを VignetteDamage に追加して、TakeDamage の中から呼ぶのがおすすめです。


        /// <summary>
        /// ダメージを受けた瞬間に、ビネットを一時的に強くする簡易フラッシュ演出。
        /// 例:PlayerHealth.TakeDamage内から呼び出す。
        /// </summary>
        /// <param name="extraIntensity">
        /// 追加で強くする度合い(0〜1)。0.2〜0.4程度がおすすめ。
        /// </param>
        public void PunchVignette(float extraIntensity = 0.3f)
        {
            // 現在の目標強度に対して、少しだけ上乗せする
            float punched = Mathf.Clamp01(_targetIntensity + extraIntensity);

            // 即座に現在強度を引き上げることで「ビクッ」とした感じを出す
            _currentIntensity = Mathf.Max(_currentIntensity, punched);

            // その後は通常のUpdate処理で徐々に_targetIntensityに戻っていきます
            ApplyVignetteColor(_currentIntensity);
        }

このように、「瀕死視界」専用の小さなコンポーネントとして作っておくと、あとから「ダメージフラッシュ」「毒演出」「暗闇演出」などを小さく足し引きしながら、演出を育てていけます。巨大なGodクラスに演出を詰め込むのではなく、1つ1つの演出をコンポーネントとして切り出していく習慣をつけていきましょう。