Skip to content

HealthComponent and Damage Response Pattern

A reusable health component keeps "how health changes" in one place without making every weapon know every enemy, barrel, shield, UI widget, and death animation in the project.

The one-minute version

  • Unreal's damage nodes deliver a damage message. They do not create health, clamp values, play death, update UI, or replicate health for you.
  • Put shared health state in a HealthComponent when players, enemies, props, weak points, or destructibles all need the same health rules.
  • The component should own current health, max health, clamping, invulnerability gates, armor/resistance math, OnHealthChanged, and the first transition into dead state.
  • The owning actor should own actor-specific results: disabling input, stopping artificial intelligence (AI), changing collision, playing animation, awarding score, respawning, or destroying itself.
  • The damage sender should know as little as possible: target actor, damage amount, instigator, causer, and damage type. It should not cast through every target class to subtract health directly.
  • In multiplayer, the server changes real health. Clients update widgets, sounds, materials, and hit feedback from replicated state or approved cosmetic events.

The responsibility split

A first project often starts like this:

Projectile hits BP_Enemy
-> Cast To BP_Enemy
-> Health = Health - 25
-> if Health <= 0, Destroy Actor

That works for one enemy. It falls apart when the same projectile can hit a player, barrel, boss weak point, shield, destructible door, or teammate.

The sturdier split is:

flowchart LR
    Sender[Weapon / projectile / hazard] --> Damage[Apply Damage]
    Damage --> Actor[Damaged actor receives damage]
    Actor --> Component[HealthComponent]
    Component --> Changed[OnHealthChanged]
    Component --> Dead[OnDeath]
    Changed --> UI[HUD / floating bar / audio / material]
    Dead --> Owner[Owner handles death-specific cleanup]

The sender says "this actor took damage." The receiver decides what that means.

What the HealthComponent owns

The component owns the reusable part of health:

Component responsibility Beginner rule
MaxHealth The normal upper bound for this actor's body or destructible state.
CurrentHealth The current server-authoritative health value. Clamp it between 0 and MaxHealth.
bIsDead or HealthState The one gate that prevents double-death.
Invulnerability A rule that rejects or ignores incoming damage before health changes.
Armor/resistance A rule that turns incoming damage into accepted damage.
OnHealthChanged(Current, Max, Delta) Broadcast whenever the public health value changes.
OnDeath(FinalHitInfo) Broadcast once when health crosses from alive to dead.
Replication hooks Replicate health/death state when the owner is networked.

That does not mean every project needs every field on day one. A clean first component can start with:

MaxHealth = 100
CurrentHealth = 100
bIsDead = false

OnHealthChanged(CurrentHealth, MaxHealth)
OnDeath()

Then add resistance, invulnerability, teams, or status effects when a real feature asks for them.

What the owning actor owns

The actor that owns the component still owns its body and presentation.

Examples:

Owner Death behavior
Player character Disable movement/input, switch to death camera, show respawn UI, ask GameMode to restart.
Enemy character Stop AI logic, stop movement, disable attack collision, play death montage, award score, clean up corpse later.
Destructible barrel Stop collision, spawn explosion effects, apply radial damage, destroy itself.
Boss weak point Disable that weak point, update boss phase, leave the boss actor alive.

Do not put all of that inside the reusable component unless every owner really shares the same result. The HealthComponent should broadcast "death happened." The actor should decide what dying means for that actor.

What the damage sender should not know

The sender should not do this:

Projectile OnHit
-> Cast To BP_Enemy
-> subtract Enemy.Health
-> Cast To BP_Barrel
-> subtract Barrel.Health
-> Cast To BP_Player
-> subtract Player.Health

That makes every weapon depend on every target class. Adding one target means editing every sender.

Prefer this:

Projectile OnHit
-> filter self / owner / team / already-hit targets
-> Apply Point Damage or Apply Damage
   Damaged Actor = Hit Actor
   Base Damage = DamageAmount
   Event Instigator = Get Instigator Controller
   Damage Causer = self projectile
   Damage Type Class = optional category

The damaged actor receives the event and forwards it into its HealthComponent:

Event AnyDamage / Event PointDamage
-> HealthComponent ApplyDamageToHealth(Damage, DamageType, Instigator, Causer)

If an actor is not damageable, it simply does not handle the damage event, or its component rejects the damage. The weapon still stays generic.

Damage intake flow

A useful component function is explicit about accepted damage:

pseudocode - project logic, not engine source

function ApplyDamageToHealth(IncomingDamage, DamageType, Instigator, Causer):
    if bIsDead:
        return 0

    if bInvulnerable:
        return 0

    AdjustedDamage = ApplyResistanceRules(IncomingDamage, DamageType, Causer)
    AcceptedDamage = Max(AdjustedDamage, 0)
    if AcceptedDamage <= 0:
        return 0

    OldHealth = CurrentHealth
    CurrentHealth = Clamp(CurrentHealth - AcceptedDamage, 0, MaxHealth)
    ActualDelta = CurrentHealth - OldHealth

    if CurrentHealth != OldHealth:
        OnHealthChanged(CurrentHealth, MaxHealth, ActualDelta)

    if OldHealth > 0 and CurrentHealth == 0 and not bIsDead:
        bIsDead = true
        OnDeath()

    return AcceptedDamage

The important details are:

  • clamp every health write;
  • reject dead targets before applying another death;
  • broadcast health change only when the visible value changed;
  • fire death from one guarded transition, not from every branch that notices Health <= 0;
  • return or expose what was accepted when the caller needs hit markers, score, or debugging.

Blueprint does not force this structure, but the structure prevents most "death ran twice" and "UI never refreshed" bugs.

What the caller gets back

Apply Damage already returns the amount the target's damage path accepted, but a reusable Blueprint component should make its own result clear too. A simple first version can return the accepted damage as a real number.

For a larger Blueprint graph, return a small enum or struct:

EHealthChangeResult
    Applied
    BlockedByInvulnerability
    BlockedBecauseDead
    NoUsefulDamage

FHealthChangeResult
    Result
    AcceptedDamage
    OldHealth
    NewHealth
    bCausedDeath

You do not need that on day one. The important part is that callers should not guess whether damage "worked" by reading a widget, checking a montage, or looking for a destroyed actor. The health owner should expose the gameplay answer it just decided.

Clamping, healing, and max health

Use one write path for health changes when possible:

ApplyHealthDelta(Delta):
    Old = CurrentHealth
    CurrentHealth = Clamp(CurrentHealth + Delta, 0, MaxHealth)
    if CurrentHealth != Old:
        OnHealthChanged(CurrentHealth, MaxHealth, CurrentHealth - Old)
    if Old > 0 and CurrentHealth == 0:
        HandleReachedZeroOnce()

Damage can call it with a negative delta. Healing can call it with a positive delta. Max-health changes should also clamp:

SetMaxHealth(NewMax):
    MaxHealth = Max(NewMax, 1)
    CurrentHealth = Clamp(CurrentHealth, 0, MaxHealth)
    OnHealthChanged(CurrentHealth, MaxHealth, 0)

If healing a dead actor should be allowed, write that as an explicit revive function. Do not let a normal health delta accidentally bring a dead actor back after death cleanup has disabled collision, input, AI, and timers.

UI refresh and event dispatchers

Widgets should not own health. They read it.

Common HUD shape:

PlayerController creates HUD widget
-> gets current pawn
-> gets HealthComponent
-> widget binds to HealthComponent.OnHealthChanged
-> widget updates once from CurrentHealth / MaxHealth

HealthComponent changes health
-> OnHealthChanged(Current, Max, Delta)
-> widget sets ProgressBar Percent = Current / Max

The initial refresh matters because a widget that binds after a damage event will not hear the old event. Bind once, update once, then update on changes.

When the pawn respawns, the controller or HUD should unbind from the old component and bind to the new component. The old HealthComponent cannot update the new body.

Replication

In multiplayer, health is gameplay state. Put the authoritative change on the server.

Beginner-safe shape:

Client presses Fire
-> local muzzle flash / recoil only
-> Run on Server Fire request
-> server validates ammo, range, team, line of sight
-> server applies damage
-> target HealthComponent changes replicated CurrentHealth / bIsDead
-> RepNotify or OnHealthChanged updates local presentation on each copy

Useful rules:

  • the owning actor must replicate before its component state can be useful to other machines;
  • component variables need intentional replication setup, not just "the component exists";
  • widgets do not replicate, so OnRep_CurrentHealth or a local component event should drive each machine's widget;
  • the server should be the only machine that decides death and score credit;
  • clients can predict or play temporary effects, but server health is the result.

For a first single-player project, you can still use the same component shape. It makes the later multiplayer move smaller because health already has one owner.

Failure modes

Symptom Likely cause
Health bar never changes Widget bound to the wrong component, never did an initial refresh, or health changed without broadcasting.
Health goes below zero Damage path subtracts directly instead of clamping through the component.
Death runs twice More than one branch checks Health <= 0 without a single bIsDead gate.
Weapon only hurts one enemy class Weapon casts to a specific target instead of using the damage pipeline or a damageable interface.
Healing revives a broken corpse Normal health delta can change a dead actor after cleanup ran.
Damage works in one client window only Client changed local health instead of asking the server to apply damage.
Floating widget shows another actor's health Widget kept an old component reference after respawn/reuse.
Invulnerability blocks UI updates The component rejected damage correctly, but no "blocked hit" presentation path exists.

Failure usually looks like a silent state mismatch. Unreal will not invent the missing architecture for you. Print the target actor, component name, authority, old health, new health, and death gate while debugging.

The pattern everyone actually uses

For a small enemy:

BP_Enemy
-> HealthComponent
-> BeginPlay: bind HealthComponent.OnDeath to HandleDeath

BP_Enemy Event AnyDamage
-> HealthComponent ApplyDamageToHealth(Damage, DamageType, InstigatedBy, DamageCauser)

HealthComponent ApplyDamageToHealth
-> clamp CurrentHealth
-> broadcast OnHealthChanged
-> if health reached 0 for the first time: broadcast OnDeath

BP_Enemy HandleDeath
-> stop attack timer / hitbox
-> stop AI movement
-> disable collision
-> play death montage or effects
-> Set Life Span 5.0 or Destroy Actor

For a player HUD:

PlayerController OnPossess / HUD setup
-> get possessed pawn HealthComponent
-> HUD SetHealthSource(HealthComponent)

HUD SetHealthSource
-> unbind old source
-> bind new source OnHealthChanged
-> update once from current values

For a projectile:

Projectile OnHit
-> Apply Point Damage to Hit Actor
-> destroy or start impact cleanup

The projectile never subtracts health directly.

Lookalikes - which one do I want?

Tool Use when Watch out
HealthComponent Several actor types share health, clamping, damage acceptance, and health-change events. Still needs the owning actor to handle body-specific death cleanup.
Health variables on the Character One prototype character has simple health and no reuse yet. Copying the same graph to enemies/props becomes a maintenance bug.
Blueprint Interface A sender needs to call a generic Damageable or Interact contract. Interfaces store no shared health state by themselves.
Event AnyDamage / Apply Damage You want Unreal's built-in damage sender/receiver message. It does not provide health or death logic.
Damage Type Class Damage category matters: fire, poison, bullet, fall, explosion. It is class data, not per-hit mutable state or automatic behavior.
Gameplay Tags You need labels such as State.Invulnerable or Damage.Fire. Tags classify; your component still implements behavior.
Gameplay Ability System The project needs attributes, effects, prediction, stacking, and networked ability architecture. It is a larger system than a first-project HealthComponent.

Rule of thumb: weapons send damage, damaged actors route damage, components own reusable health state, and actors own the consequences.

Going deeper