Unityを触り始めた頃って、Update() の中に「ダメージ計算」「HP管理」「エフェクト再生」「UI更新」など、全部まとめて書いてしまいがちですよね。
ダメージを受けたときの「頭上に数字を出す演出」も、ついその巨大なスクリプトの中に書き足してしまいがちです。

でもそれを続けていると、

  • ダメージ演出だけ仕様変更したいのに、プレイヤーのロジックまで巻き込んでしまう
  • 敵ごとに少しずつ挙動を変えたくても、巨大クラスにifが増え続ける
  • テストやデバッグで「どこで数字が出ているのか」追いづらい

という状態になりがちです。

そこでこの記事では、「ダメージ数字の表示だけ」を担当する小さなコンポーネントとして
DamagePopup(ダメージ数字)コンポーネントを用意して、
プレイヤーや敵のロジックからは「ダメージが出る箱」を呼ぶだけ、という構成にしてみましょう。

【Unity】ふわっと跳ねるダメージ数字!「DamagePopup」コンポーネント

ここでは、3D/2Dどちらでも使いやすいように、World Space の Canvas + TextMeshPro を前提にした実装を紹介します。
Unity6 で標準的な UI システム(UGUI + TextMeshPro)だけで動くようにしています。


DamagePopup.cs フルコード


using UnityEngine;
using TMPro;

/// <summary>
/** 
 * ダメージ数字をふわっと表示して、少し上に跳ねながらフェードアウトするコンポーネント。
 * 
 * 想定:
 * - World Space の Canvas 配下に配置された TextMeshProUGUI を持つプレハブにアタッチして使う。
 * - DamagePopupSpawner など別コンポーネントから Instantiate されて利用する。
 */
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public class DamagePopup : MonoBehaviour
{
    // ====== 基本設定 ======

    [Header("表示テキスト")]
    [SerializeField] private TextMeshProUGUI damageText;

    [Header("寿命(秒)")]
    [SerializeField] private float lifetime = 0.8f;

    [Header("初速度(ワールド座標系)")]
    [SerializeField] private Vector3 initialVelocity = new Vector3(0f, 2.0f, 0f);

    [Header("重力(ワールド座標系)")]
    [SerializeField] private Vector3 gravity = new Vector3(0f, -6.0f, 0f);

    [Header("開始時スケール")]
    [SerializeField] private float startScale = 1.0f;

    [Header("終了時スケール")]
    [SerializeField] private float endScale = 0.7f;

    [Header("フェードアウト開始割合(0~1)")]
    [SerializeField, Range(0f, 1f)] private float fadeStartRatio = 0.4f;

    [Header("ランダムな横方向の揺らぎ量")]
    [SerializeField] private float randomHorizontalOffset = 0.3f;

    [Header("カメラ方向を向かせるか")]
    [SerializeField] private bool faceToCamera = true;

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

    private CanvasGroup canvasGroup;
    private float elapsed;
    private Vector3 velocity;
    private Transform cachedTransform;
    private Camera mainCamera;

    private void Awake()
    {
        cachedTransform = transform;
        canvasGroup = GetComponent<CanvasGroup>();

        if (damageText == null)
        {
            // 自動取得(プレハブ設定ミス対策)
            damageText = GetComponentInChildren<TextMeshProUGUI>();
        }

        mainCamera = Camera.main;
    }

    private void OnEnable()
    {
        // 再利用を考慮してリセット
        elapsed = 0f;
        velocity = initialVelocity;

        // 少しだけランダムな横方向の揺らぎを与える
        if (randomHorizontalOffset > 0f)
        {
            float offsetX = Random.Range(-randomHorizontalOffset, randomHorizontalOffset);
            float offsetZ = Random.Range(-randomHorizontalOffset, randomHorizontalOffset);
            cachedTransform.position += new Vector3(offsetX, 0f, offsetZ);
        }

        cachedTransform.localScale = Vector3.one * startScale;
        canvasGroup.alpha = 1f;
    }

    private void Update()
    {
        elapsed += Time.deltaTime;

        // 寿命を超えたら破棄
        if (elapsed >= lifetime)
        {
            Destroy(gameObject);
            return;
        }

        float t = elapsed / lifetime;

        // ====== 位置の更新(簡易パーティクル的な挙動) ======
        // v = v0 + g * dt
        velocity += gravity * Time.deltaTime;
        // p = p + v * dt
        cachedTransform.position += velocity * Time.deltaTime;

        // ====== スケールの補間 ======
        float scale = Mathf.Lerp(startScale, endScale, t);
        cachedTransform.localScale = Vector3.one * scale;

        // ====== フェードアウト ======
        float fadeStartTime = lifetime * fadeStartRatio;
        if (elapsed >= fadeStartTime)
        {
            float fadeT = Mathf.InverseLerp(fadeStartTime, lifetime, elapsed);
            canvasGroup.alpha = 1f - fadeT;
        }

        // ====== カメラ方向を向かせる(ビルボード) ======
        if (faceToCamera && mainCamera != null)
        {
            // Canvas が World Space の場合、単純にカメラを向かせる
            Vector3 camForward = mainCamera.transform.rotation * Vector3.forward;
            Vector3 camUp = mainCamera.transform.rotation * Vector3.up;
            cachedTransform.LookAt(cachedTransform.position + camForward, camUp);
        }
    }

    /// <summary>
    /// 表示するダメージ値と色を設定する。
    /// 生成直後に呼び出す想定。
    /// </summary>
    /// <param name="value">ダメージ数値</param>
    /// <param name="color">テキストカラー(null の場合は変更しない)</param>
    public void Setup(int value, Color? color = null)
    {
        if (damageText == null) return;

        damageText.text = value.ToString();

        if (color.HasValue)
        {
            damageText.color = color.Value;
        }
    }

    /// <summary>
    /// クリティカルヒットなど、強調したい時にスケールを一時的に大きくする。
    /// </summary>
    public void PunchScale(float multiplier = 1.4f)
    {
        cachedTransform.localScale = Vector3.one * startScale * multiplier;
    }
}

使い方の手順

ここからは、実際に「プレイヤーが敵を攻撃したときにダメージ数字を出す」例で手順を説明します。

手順①:DamagePopup プレハブを作る

  1. Hierarchy で Canvas を作成し、Render Mode を「World Space」 に変更します。
  2. Canvas の子として TextMeshPro - Text (UI) を作成します。
  3. Canvas のスケール・サイズを調整して、シーン内で適切な大きさに見えるようにします。
    (例:Canvas の RectTransform の Width/Height を 200×100、スケールを 0.01 など)
  4. Canvas の GameObject に DamagePopup コンポーネントをアタッチします。
  5. DamagePopupDamage Text フィールドに、子の TextMeshProUGUI をドラッグ&ドロップします。
  6. この Canvas を Project ビューへドラッグして Prefab 化します(例:DamagePopup.prefab)。

手順②:ダメージ数字を生成するスクリプトを用意する

プレイヤーや敵のロジックに直接 Instantiate を書くと肥大化しやすいので、
「ダメージポップアップを出すだけのコンポーネント」を用意しておくと扱いやすくなります。


using UnityEngine;

/// <summary>
/// 任意のワールド座標に DamagePopup プレハブを生成してくれる小さなヘルパー。
/// プレイヤーや敵のオブジェクトにアタッチして使う想定。
/// </summary>
public class DamagePopupSpawner : MonoBehaviour
{
    [Header("ダメージポップアップのプレハブ")]
    [SerializeField] private DamagePopup damagePopupPrefab;

    [Header("生成位置のオフセット(ローカル座標)")]
    [SerializeField] private Vector3 spawnOffset = new Vector3(0f, 2.0f, 0f);

    /// <summary>
    /// ダメージポップアップを生成してセットアップする。
    /// </summary>
    public void SpawnDamagePopup(int damage, bool isCritical = false)
    {
        if (damagePopupPrefab == null)
        {
            Debug.LogWarning("DamagePopupSpawner: damagePopupPrefab が設定されていません。");
            return;
        }

        // ワールド座標での生成位置
        Vector3 spawnPosition = transform.position + spawnOffset;

        // プレハブを生成(親なし / ワールドのルートに出す)
        DamagePopup popup = Instantiate(damagePopupPrefab, spawnPosition, Quaternion.identity);

        // 通常ダメージとクリティカルで色を変える例
        Color color = isCritical ? Color.yellow : Color.white;

        popup.Setup(damage, color);

        if (isCritical)
        {
            popup.PunchScale(1.6f);
        }
    }
}

手順③:プレイヤーや敵から呼び出す

例として、敵がダメージを受けるコンポーネントを作り、その中からダメージ数字を出してみます。


using UnityEngine;

/// <summary>
/// とてもシンプルな敵の HP 管理クラスの例。
/// DamagePopupSpawner を使ってダメージ数字を出す。
/// </summary>
[RequireComponent(typeof(DamagePopupSpawner))]
public class SimpleEnemyHealth : MonoBehaviour
{
    [SerializeField] private int maxHp = 100;

    private int currentHp;
    private DamagePopupSpawner popupSpawner;

    private void Awake()
    {
        currentHp = maxHp;
        popupSpawner = GetComponent<DamagePopupSpawner>();
    }

    /// <summary>
    /// 外部から呼ばれるダメージ処理。
    /// </summary>
    public void TakeDamage(int damage, bool isCritical = false)
    {
        currentHp -= damage;
        currentHp = Mathf.Max(currentHp, 0);

        // ダメージ数字を表示
        popupSpawner.SpawnDamagePopup(damage, isCritical);

        if (currentHp <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        // 実際のゲームではアニメーションやドロップ処理などをここに書く
        Destroy(gameObject);
    }
}

あとは、攻撃判定側(プレイヤーの武器やスキルなど)から TakeDamage() を呼ぶだけで、
敵の頭上にダメージ数字がふわっと出るようになります。

手順④:動く床やボスなど、別オブジェクトでも使い回す

  • プレイヤー:敵に攻撃を当てたときに、敵の SimpleEnemyHealth を参照して TakeDamage() を呼ぶ。
  • トゲ床:プレイヤーが乗ったら、プレイヤー側の HP 管理コンポーネントに TakeDamage() を呼び、同時にプレイヤーにアタッチした DamagePopupSpawner から数字を出す。
  • ボス:通常攻撃とクリティカルの色を変えたい場合は、isCritical を切り替えて SpawnDamagePopup() を呼ぶ。

どのケースでも「ダメージ数字をどう動かすか」は DamagePopup に閉じ込められているので、
攻撃ロジックや HP 管理のコードはシンプルなまま保てます。


メリットと応用

このように DamagePopupDamagePopupSpawner を小さなコンポーネントとして分離しておくと、

  • プレハブ単位で演出を差し替えやすい
    通常ダメージ用、クリティカル専用、毒ダメージ用など、色やフォント、アニメーションを変えたプレハブを複数用意しておけば、
    スクリプトはほとんど触らずに演出を切り替えられます。
  • レベルデザインが楽になる
    敵のプレハブに SimpleEnemyHealthDamagePopupSpawner をセットしておけば、
    ステージに敵をポンポン配置するだけで、どこでも同じクオリティのダメージ表示が出ます。
  • 巨大な God クラスを避けられる
    「ダメージ計算」「HP 管理」「表示演出」がそれぞれ別のコンポーネントに分かれているので、
    どこか 1 箇所を変更しても他の責務に影響しづらくなります。

また、DamagePopup は「ふわっと動いてフェードアウトする UI 要素」という汎用的な動きなので、

  • 回復量の表示
  • 取得したコイン数の表示
  • 経験値の増加表示

などにもそのまま流用できます。

改造案:ダメージ値に応じて色をグラデーションさせる

例えば「ダメージが大きいほど赤く、小さいほど白く」したい場合は、
DamagePopup にこんなメソッドを追加してみるのもアリです。


    /// <summary>
    /// ダメージ量に応じて色を補間して設定する例。
    /// minDamage ~ maxDamage の範囲で、lowColor から highColor にグラデーション。
    /// </summary>
    public void SetupWithGradient(int value, int minDamage, int maxDamage, Color lowColor, Color highColor)
    {
        if (damageText == null) return;

        damageText.text = value.ToString();

        // 0~1 に正規化
        float t = 0f;
        if (maxDamage > minDamage)
        {
            t = Mathf.InverseLerp(minDamage, maxDamage, value);
        }

        damageText.color = Color.Lerp(lowColor, highColor, t);
    }

このように、小さなコンポーネント単位で責務を分けておけば、
「色を変える」「動き方を変える」といった演出の試行錯誤もやりやすくなります。
ぜひ自分のプロジェクトに合わせて、少しずつカスタマイズしてみてください。