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を用意する
- シーンに Canvas を用意します(既にある場合はそれを使用)。
推奨設定:- Render Mode: Screen Space – Overlay
- Canvas Scaler: Scale With Screen Size(画面解像度に追従させるため)
- Canvas の子として UI > Image を作成し、名前を
VignetteDamageImageなどにします。 - Image の RectTransform を以下のように設定します:
- Anchor Min: (0, 0)
- Anchor Max: (1, 1)
- Pos X / Pos Y / Width / Height: 0(ストレッチ状態)
これで画面全体を覆うフルスクリーンのImageになります。
- Image の Source Image に、四隅が暗く中央が透明な「ビネット用テクスチャ」を設定すると、より自然な効果になります。
テクスチャがない場合は、一旦真っ黒の1×1テクスチャでも動きますが、四隅だけ暗くするには専用のテクスチャがあると便利です。
手順②:VignetteDamageコンポーネントをアタッチする
- 先ほど作成した
VignetteDamageImageオブジェクトを選択します。 - Add Component ボタンから VignetteDamage を追加します。
- インスペクター上で以下のパラメータを調整します:
- 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をまとめてプレハブ化しておけば、どのシーンでもドラッグ&ドロップだけで「瀕死視界」を導入できます。 - レベルデザインが楽になる
ステージごとにdangerColorやstartVignetteThresholdを変えるだけで、「このステージはちょっとダークめ」「このステージは優しめ」といった雰囲気調整がしやすくなります。 - 他のゲームでも再利用可能
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つの演出をコンポーネントとして切り出していく習慣をつけていきましょう。
