Part of these game systems:
beginner UI

Damage Popup

Floating damage numbers that animate upward and fade out. Supports critical hits, healing, and custom colors.

Unity 2022.3+ · 2.6 KB · DamagePopup.cs

How to Use

1

Call DamagePopup.Create(position, damageAmount) from your combat script

2

For critical hits: DamagePopup.Create(pos, dmg, isCritical: true)

3

For healing: DamagePopup.Create(pos, amount, isHeal: true)

4

Numbers float up and fade automatically

5

Requires TextMeshPro package (included with Unity by default)

Source Code

DamagePopup.cs
C#
using UnityEngine;
using TMPro;

/// <summary>
/// Floating damage number popup. Spawns at a world position,
/// floats upward, and fades out over its lifetime.
/// </summary>
public class DamagePopup : MonoBehaviour
{
    [Header("Animation")]
    [SerializeField] private float floatSpeed = 1.5f;
    [SerializeField] private float lifetime = 0.8f;
    [SerializeField] private float fadeStartPercent = 0.5f;

    [Header("Scaling")]
    [SerializeField] private float normalScale = 1f;
    [SerializeField] private float critScale = 1.4f;

    [Header("Colors")]
    [SerializeField] private Color damageColor = Color.white;
    [SerializeField] private Color critColor = new Color(1f, 0.85f, 0f);
    [SerializeField] private Color healColor = new Color(0.3f, 1f, 0.5f);

    private TextMeshPro textMesh;
    private float timer;
    private Color startColor;
    private Vector3 randomOffset;
    private bool isCrit;

    private void Awake()
    {
        textMesh = GetComponent<TextMeshPro>();
        if (textMesh == null)
            textMesh = gameObject.AddComponent<TextMeshPro>();

        textMesh.alignment = TextAlignmentOptions.Center;
        textMesh.fontSize = 6f;
        textMesh.sortingOrder = 100;
    }

    /// <summary>
    /// Create a damage popup at the given world position.
    /// </summary>
    public static DamagePopup Create(Vector3 position, float amount, bool isCritical = false, bool isHeal = false)
    {
        GameObject go = new GameObject("DamagePopup");
        go.transform.position = position + new Vector3(
            Random.Range(-0.3f, 0.3f),
            Random.Range(0f, 0.2f),
            0f
        );

        DamagePopup popup = go.AddComponent<DamagePopup>();
        popup.Setup(amount, isCritical, isHeal);
        return popup;
    }

    private void Setup(float amount, bool isCritical, bool isHeal)
    {
        if (textMesh == null)
        {
            textMesh = GetComponent<TextMeshPro>();
            if (textMesh == null)
                textMesh = gameObject.AddComponent<TextMeshPro>();
        }

        string prefix = isHeal ? "+" : "";
        textMesh.text = prefix + Mathf.RoundToInt(Mathf.Abs(amount)).ToString();

        if (isHeal)
            startColor = healColor;
        else if (isCritical)
            startColor = critColor;
        else
            startColor = damageColor;

        textMesh.color = startColor;

        isCrit = isCritical;
        float scale = isCritical ? critScale : normalScale;
        transform.localScale = Vector3.one * scale;

        randomOffset = new Vector3(Random.Range(-0.3f, 0.3f), 0f, 0f);
    }

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

        // Float upward
        transform.position += (Vector3.up * floatSpeed + randomOffset * 0.5f) * Time.deltaTime;

        // Fade out
        float percent = timer / lifetime;
        if (percent > fadeStartPercent)
        {
            float fadePercent = (percent - fadeStartPercent) / (1f - fadeStartPercent);
            Color c = startColor;
            c.a = Mathf.Lerp(1f, 0f, fadePercent);
            textMesh.color = c;
        }

        // Scale down slightly
        float scalePercent = 1f - (percent * 0.2f);
        transform.localScale = Vector3.one * scalePercent * (isCrit ? critScale : normalScale);

        if (timer >= lifetime)
            Destroy(gameObject);
    }

    // Face camera (billboard)
    private void LateUpdate()
    {
        if (Camera.main != null)
            transform.forward = Camera.main.transform.forward;
    }
}