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_CurrentHealthor 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¶
- Actor Components as reusable gameplay
- the general component pattern behind this page.
- Death and defeat cleanup flow - what the
owning actor should do after
OnDeathfires once. - Event AnyDamage / Apply Damage
- the generic damage sender/receiver flow and authority-only damage event.
- Apply Point Damage / Apply Radial Damage / Damage Type
- richer hit data, explosions, and damage categories.
- Status effects and damage-over-time with timers
- poison, burn, slow, stun, stacks, refresh rules, and timed cleanup.
- Event Dispatchers -
broadcasting
OnHealthChangedandOnDeathto listeners. - Updating a Widget: Bindings vs Events
- pushing health changes into UI without per-frame polling.
- RepNotify variables and local presentation
- replicated health driving client-local UI and effects.
- Replication basics - server authority for gameplay state.
- Official docs: Components, Apply Damage, Event AnyDamage, UActorComponent, and AActor::TakeDamage.
- Engine source (requires engine access - we do not reproduce it here):
UActorComponentlifecycle and replication helpers inEngine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp;UGameplayStatics::ApplyDamageinEngine/Source/Runtime/Engine/Private/GameplayStatics.cpp;AActor::TakeDamage,AActor::ReceiveAnyDamage, andAActor::OnTakeAnyDamageinEngine/Source/Runtime/Engine/Private/Actor.cpp; declarations inEngine/Source/Runtime/Engine/Classes/GameFramework/Actor.h.