Unityを触り始めたころは、つい「とりあえず全部Updateに書いておけば動くしOK!」となりがちですよね。
プレイヤー操作、カメラ制御、UIの点滅演出、ダメージ処理……これらを1つの巨大スクリプトに押し込んでしまうと、数週間後には「どこを直せばいいのか分からない」「別シーンに持っていくのがつらい」という状態になりやすいです。

特に「被ダメージ演出(画面端を赤く光らせる)」のようなUI演出は、プレイヤー制御や敵AIとはまったく別の関心事なのに、つい同じクラスに書かれがちです。

そこでこの記事では、「ダメージを受けた瞬間に画面の端を赤く点滅させる」だけに責務を絞った
「DamageVignette」コンポーネントを作って、コンポーネント指向でスッキリ分離していく方法を解説します。

【Unity】ダメージ演出をUIに丸投げ!「DamageVignette」コンポーネント

このコンポーネントは、以下のようなシンプルな役割だけを持たせます。

  • ダメージを受けたときに「今だけ赤く光らせてほしい」と呼び出される
  • UIのTextureRect(Unity UI ToolkitのVisualElement)を赤くフェードイン/アウトさせる
  • プレイヤーのHP管理や敵の攻撃ロジックは一切知らない

こうしておくと、「ダメージ演出」を担当する小さな部品として、どのシーンにも簡単に再利用できるようになります。


フルコード:DamageVignette.cs

前提:

  • Unity6(UI Toolkit)を使用
  • UI Documentに、画面全体を覆う赤いTextureRect(VisualElement)を用意してある
  • そのVisualElementのnameをインスペクターから指定して使う

using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
/** 
 * ダメージを受けた瞬間に画面端を赤く点滅させるコンポーネント。
 * 
 * - UI Toolkit の UIDocument から TextureRect(VisualElement) を取得
 * - ダメージを受けたときに public メソッドを呼ぶだけで演出を再生
 * - プレイヤーのHPや敵の攻撃ロジックとは完全に分離
 */
/// </summary>
[RequireComponent(typeof(UIDocument))]
public class DamageVignette : MonoBehaviour
{
    // ====== インスペクタ設定項目 ======

    [Header("UI設定")]
    [Tooltip("ダメージ演出に使用するTextureRect(VisualElement)のname")]
    [SerializeField] private string vignetteElementName = "DamageVignette";

    [Header("フェード設定")]
    [Tooltip("最大まで赤くなるまでの時間(秒)")]
    [SerializeField] private float fadeInDuration = 0.1f;

    [Tooltip("赤から完全に消えるまでの時間(秒)")]
    [SerializeField] private float fadeOutDuration = 0.4f;

    [Tooltip("最大時のアルファ値(0〜1)。1で完全な不透明")]
    [SerializeField] private float maxAlpha = 0.6f;

    [Header("ダメージ連打時の挙動")]
    [Tooltip("ダメージを連続で受けたときにフェードを上書きするか")]
    [SerializeField] private bool restartOnDamage = true;

    // ====== 内部状態 ======

    private UIDocument uiDocument;
    private VisualElement vignetteElement;

    // 現在のアルファ値
    private float currentAlpha = 0f;

    // 演出中かどうか
    private bool isPlaying = false;

    // 経過時間
    private float elapsed = 0f;

    private void Awake()
    {
        // 同じGameObjectに付いているUIDocumentを取得
        uiDocument = GetComponent<UIDocument>();

        if (uiDocument == null)
        {
            Debug.LogError("[DamageVignette] UIDocument が見つかりません。GameObject に UIDocument を追加してください。", this);
            enabled = false;
            return;
        }
    }

    private void OnEnable()
    {
        // UI Document のルートから指定した name の要素を探す
        var root = uiDocument.rootVisualElement;
        vignetteElement = root.Q<VisualElement>(vignetteElementName);

        if (vignetteElement == null)
        {
            Debug.LogError($"[DamageVignette] name='{vignetteElementName}' の VisualElement が見つかりません。UXML 上の name を確認してください。", this);
            enabled = false;
            return;
        }

        // 初期状態では非表示(アルファ0)にしておく
        SetVignetteAlpha(0f);
        isPlaying = false;
        elapsed = 0f;
    }

    private void Update()
    {
        if (!isPlaying) return;

        elapsed += Time.deltaTime;

        // フェードインとフェードアウトを時間ベースで制御
        float totalDuration = fadeInDuration + fadeOutDuration;

        if (totalDuration <= 0f)
        {
            // 万が一0設定の場合は即座に終了
            SetVignetteAlpha(0f);
            isPlaying = false;
            return;
        }

        if (elapsed <= fadeInDuration)
        {
            // フェードイン区間
            float t = Mathf.Clamp01(elapsed / Mathf.Max(fadeInDuration, 0.0001f));
            currentAlpha = Mathf.Lerp(0f, maxAlpha, t);
        }
        else if (elapsed <= totalDuration)
        {
            // フェードアウト区間
            float t = Mathf.Clamp01((elapsed - fadeInDuration) / Mathf.Max(fadeOutDuration, 0.0001f));
            currentAlpha = Mathf.Lerp(maxAlpha, 0f, t);
        }
        else
        {
            // 終了
            currentAlpha = 0f;
            isPlaying = false;
        }

        SetVignetteAlpha(currentAlpha);
    }

    /// <summary>
    /// 外部から呼び出す「ダメージ演出開始」メソッド。
    /// プレイヤーがダメージを受けたタイミングでこれを呼ぶだけ。
    /// </summary>
    public void Play()
    {
        if (!enabled || vignetteElement == null)
        {
            return;
        }

        if (!isPlaying)
        {
            // 初回再生
            isPlaying = true;
            elapsed = 0f;
        }
        else
        {
            // すでに再生中のときの挙動
            if (restartOnDamage)
            {
                // 最初からやり直す
                elapsed = 0f;
            }
            else
            {
                // そのまま。ここでは「今の状態からさらに強くなる」などの処理もあり得る
                // 必要に応じて挙動を拡張しましょう
            }
        }
    }

    /// <summary>
    /// 現在のアルファ値を強制的に0にし、演出をリセットする。
    /// </summary>
    public void ResetVignette()
    {
        isPlaying = false;
        elapsed = 0f;
        currentAlpha = 0f;
        SetVignetteAlpha(0f);
    }

    /// <summary>
    /// 実際に VisualElement のスタイルにアルファ値を適用する共通関数。
    /// </summary>
    /// <param name="alpha">0〜1</param>
    private void SetVignetteAlpha(float alpha)
    {
        if (vignetteElement == null) return;

        // 現在の背景色を取得(未設定の場合は赤を仮定)
        Color baseColor = Color.red;

        // style.backgroundColor が設定されていればそれを基準にする
        // 注意: UI Toolkit の Color は UnityEngine.Color と互換
        if (vignetteElement.resolvedStyle.backgroundColor.a > 0f)
        {
            baseColor = vignetteElement.resolvedStyle.backgroundColor;
        }

        baseColor.a = Mathf.Clamp01(alpha);
        vignetteElement.style.backgroundColor = new StyleColor(baseColor);
    }
}

使い方の手順

ここでは、プレイヤーが敵からダメージを受けたときに、画面端が赤く点滅する例で説明します。

  1. ① UI Toolkit で赤いTextureRectを用意する

    • UXMLファイルを作成し、画面全体を覆うような VisualElement もしくは TextureRect を配置します。
    • style(USS)で以下のような感じに設定しておくと分かりやすいです:
      • position: absolute
      • left/top/right/bottom: 0
      • background-color: red(アルファは1.0でOK。スクリプト側で上書き)
    • この要素の nameDamageVignette などにしておきます。
  2. ② シーンに UIDocument を配置する

    • 空の GameObject を作成し、UIDocument コンポーネントを追加します。
    • Source Asset に①で作成した UXML を指定します。
  3. ③ 同じGameObjectに「DamageVignette」コンポーネントを追加する

    • ②の UIDocument を持っている GameObject に DamageVignette スクリプトを追加します。
    • インスペクターで
      • Vignette Element Name に、UXML側で付けた name(例: DamageVignette)を入力
      • フェード時間や最大アルファ値を好みに合わせて調整
  4. ④ プレイヤーのダメージ処理から「Play()」を呼ぶ

    プレイヤーのHP管理コンポーネントなどから、ダメージを受けたタイミングで DamageVignette.Play() を呼び出します。

    
    using UnityEngine;
    
    /// <summary>
    /// 例:とても簡略化したプレイヤーHP管理。
    /// ダメージを受けたときに DamageVignette に演出を依頼する。
    /// </summary>
    public class PlayerHealth : MonoBehaviour
    {
        [SerializeField] private int maxHp = 100;
        [SerializeField] private int currentHp = 100;
    
        [Header("ダメージ演出")]
        [SerializeField] private DamageVignette damageVignette;
    
        public void TakeDamage(int damage)
        {
            currentHp = Mathf.Max(currentHp - damage, 0);
    
            // ダメージ演出を再生
            if (damageVignette != null)
            {
                damageVignette.Play();
            }
    
            if (currentHp <= 0)
            {
                // 死亡処理など
                Debug.Log("Player Dead");
            }
        }
    }
    

    このように、PlayerHealth は「UIの詳細」を一切知らず
    「ダメージ演出をしてくれるコンポーネント」にただ依頼しているだけ、という形になります。

同じように、敵のHPスクリプトから呼べば「敵がダメージを受けたときにも画面を赤くする」ことも簡単ですし、
動く床のダメージゾーンなどから呼べば「トラップに触れたときも同じ演出」を使い回せます。


メリットと応用

1. プレハブ管理が楽になる

  • 「ダメージ演出」のロジックが DamageVignette に閉じ込められているので、
    UI用のプレハブ(Canvas / UIDocumentを乗せたGameObject)を1つ作っておけば、
    どのシーンにもそのプレハブをポンと置くだけで同じダメージ演出を再利用できます。
  • プレイヤーや敵のプレハブは、HPや移動、攻撃など「ゲームロジック」だけに集中させられます。

2. レベルデザイン時の調整がしやすい

  • ステージによって「ダメージが重く感じられるように、赤の演出を強めたい」などの要望が出たとき、
    シーンごとに DamageVignette コンポーネントの maxAlphafadeOutDuration を変えるだけで演出を調整できます。
  • ゲームデザイナーがインスペクターから値をいじるだけで、
    「このステージはシビアだから、赤の残像を長く残そう」といった調整が可能になります。

3. 責務が分かれているのでバグ調査がしやすい

  • 「ダメージ演出が出ない」問題が起きたとき、
    DamageVignette だけを見ればよいので、原因切り分けがとても楽です。
  • 逆に「HPが減らない」問題は HP 管理側のコンポーネントだけを見ればよく、
    UI演出が絡んできて混乱することがありません。

改造案:ダメージ量に応じて赤さを変える

例えば「大ダメージのときはより赤く、長く残したい」という場合、
Play() をダメージ量付きの関数に拡張するのもアリですね。


/// <summary>
/// ダメージ量に応じて強さを変えるバージョンの再生関数例。
/// </summary>
/// <param name="damageRatio">0〜1想定。1で最大ダメージ。</param>
public void PlayByDamage(float damageRatio)
{
    // 0〜1にクランプ
    damageRatio = Mathf.Clamp01(damageRatio);

    // ダメージが大きいほどアルファとフェードアウト時間を強くする
    float baseMaxAlpha = maxAlpha;
    float baseFadeOut = fadeOutDuration;

    maxAlpha = Mathf.Lerp(baseMaxAlpha * 0.3f, baseMaxAlpha, damageRatio);
    fadeOutDuration = Mathf.Lerp(baseFadeOut * 0.5f, baseFadeOut * 1.5f, damageRatio);

    Play();

    // 必要であれば、あとで元の値に戻す処理を入れてもよい
    // (ここでは簡略化のため省略)
}

このように、1つのコンポーネントに1つの責務を意識しておくと、
後からいくらでも安全に改造・差し替えができるようになります。
ぜひ、自分のプロジェクトでも「ダメージ演出はDamageVignetteに任せる」スタイルを試してみてください。