Damage Popup
Floating damage numbers that animate upward and fade out. Supports critical hits, healing, and custom colors.
How to Use
Call DamagePopup.Create(position, damageAmount) from your combat script
For critical hits: DamagePopup.Create(pos, dmg, isCritical: true)
For healing: DamagePopup.Create(pos, amount, isHeal: true)
Numbers float up and fade automatically
Requires TextMeshPro package (included with Unity by default)
Features
- Static Create() factory method — no prefab setup required
- Separate colors for normal damage, critical hits, and healing
- Scaled-up text for critical hits with configurable crit scale
- Float-up animation with random horizontal offset for variety
- Smooth alpha fade-out starting at configurable percentage of lifetime
- Billboard rendering to always face the camera in 3D scenes
When to Use This
Perfect for RPGs, action games, hack-and-slash, and any combat-heavy game that wants visual damage feedback. Use this whenever enemies or players take damage and you want floating numbers to appear. Also great for healing indicators in co-op or support-class games. Pairs directly with the HealthSystem script.
Common Mistakes
TextMeshPro package must be installed (it is included by default in Unity 2022+, but older projects may need to import it via Package Manager). The popup uses TextMeshPro (world-space), not TextMeshProUGUI — don't confuse the two. Since Create() uses Instantiate/Destroy, consider integrating with ObjectPool for high-frequency combat to avoid GC spikes.
Source Code
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;
}
}