Gameplay · Updated Apr 15, 2026
Health & Damage System
Health & Damage System
Halo-style health and shield system built on GAS. All damage flows through a single UGameplayEffectExecutionCalculation that encodes the shield-first absorption rule — no ability or caller needs to know the priority logic.
#Attribute Set — USLHealthAttributeSet
Four attributes, all replicated.
| Attribute | Default | Notes |
|---|---|---|
CurrentHealth | 100 | Clamped [0, MaxHealth]. Hits 0 → death. |
MaxHealth | 100 | Set per character via GE_CharacterStats. |
CurrentShield | 50 | Clamped [0, MaxShield]. Recharges after delay. |
MaxShield | 50 | Set per character via GE_CharacterStats. Can be exceeded by overshield (up to MaxOvershield). |
PostGameplayEffectExecute clamps all attributes and triggers death when CurrentHealth reaches 0.
#Damage Execution — USLDamageExecution
A UGameplayEffectExecutionCalculation subclass. Applied by every GE_Damage effect. Encodes the shield-first rule so no caller needs to handle it.
Logic (runs on authority only):
incoming = DamageMagnitude captured from the effect
if CurrentShield > 0:
shieldAbsorbed = min(incoming, CurrentShield)
CurrentShield -= shieldAbsorbed
incoming -= shieldAbsorbed
if CurrentShield == 0:
apply GameplayCue: GameplayCues.Character.ShieldDepleted
if incoming > 0:
CurrentHealth -= incoming
if CurrentHealth <= 0:
CurrentHealth = 0
→ trigger death (see Death section)
Capturing:
CurrentShieldandCurrentHealthare captured asSnapshot = false(live values at execution time).
DamageMagnitudeis aSetByCallertag (SLTags::Data::Damage) on the calling GE.
No death logic lives in the execution — it only modifies attributes. Death is handled in PostGameplayEffectExecute on the attribute set itself.
#Damage Effect — GE_SL_Damage
One reusable Instant GE. Callers set magnitude via SetByCaller.
| Field | Value |
|---|---|
| Duration | Instant |
| Execution | USLDamageExecution |
SetByCaller tag | SLTags::Data::Damage |
AssetTags | SLTags::Effects::Damage (used to cancel shield regen on hit) |
Every damage source (bullets, melee, grenades, thrown weapon) applies this GE with the appropriate magnitude. No one calls ApplyPointDamage.
#Shield Regeneration
Two-phase GAS approach. No timer polling — GEs handle the scheduling.
#Phase 1 — Regen Delay (GE_SL_ShieldRegenDelay)
Applied on authority whenever damage hits the shield or health (including the initial application and any re-application on subsequent hits).
| Field | Value |
|---|---|
| Duration | 3 seconds (configurable via curve) |
| Period | None |
GrantedTags | SLTags::States::Character::ShieldRechargingDelay |
RemoveGameplayEffectsWithTags | itself (so re-applying resets the timer) |
StackingType | Replace on Apply |
| On Expire | Grants GE_SL_ShieldRegen |
The granted tag ShieldRechargingDelay is replicated automatically by GAS — clients use it to drive the "delay before recharge" FX (shield crackle fading, HUD state).
#Phase 2 — Regen Tick (GE_SL_ShieldRegen)
Applied when the delay GE expires. Ticks until shield is full.
| Field | Value |
|---|---|
| Duration | Infinite |
| Period | 0.1 seconds |
GrantedTags | SLTags::States::Character::ShieldRecharging |
| Modifier | CurrentShield += ShieldRechargeRate * 0.1 |
PostGameplayEffectExecute removes GE_SL_ShieldRegen when CurrentShield >= MaxShield. That also broadcasts OnShieldRechargeComplete.
#Interrupt on Damage
GE_SL_ShieldRegenDelay has RemoveGameplayEffectsWithTags = SLTags::States::Character::ShieldRecharging in its RemoveEffectsWithTags list. Applying any damage GE also applies (or re-applies) the delay GE — both the regen tick and the delay timer reset cleanly.
#Overshield
Activated by a pickup or power-up, not by the player directly.
#GE_SL_OvershieldCharge (Infinite, Periodic)
| Field | Value |
|---|---|
| Duration | Infinite |
| Period | 0.1 seconds |
| Modifier | CurrentShield += OvershieldChargeRate * 0.1 |
GrantedTags | SLTags::States::Character::Overshield |
| Max stack cap | MaxShield * 2 — clamped in PostGameplayEffectExecute |
When the Overshield GE is removed (e.g. all overshield burned off), CurrentShield is clamped back to MaxShield and normal regen resumes.
The Overshield tag is replicated — HUD and FX Blueprint use it to show the overshield ring.
#Death
Triggered inside USLHealthAttributeSet::PostGameplayEffectExecute when CurrentHealth reaches 0.
Authority sequence (all runs in PostGameplayEffectExecute on the server):
CurrentHealthclamped to 0.
- Attribute set uses
ASC->GetAvatarActor()to reach the character (NOTGetOwningActor()— that returns PlayerState when ASC lives on PlayerState).
- Guard against re-entry: skip if
States.Character.Deadtag already present.
- Store
EffectCauserinCharacter->PendingKillerfor use by the respawn flow.
- Send
Events.Character.Deathgameplay event → activatesGA_Death(handles GE_Dead, CancelAllAbilities, cue dispatch, collision/movement off).
- Call
Character->StartServerDeathTimeout()— starts the server-side respawn timer independently of any animation.
StartServerDeathTimeout() — reads DeathAnimationTimeout from ASLGameModeBase (default 8s). After that many seconds, calls ASLGameModeBase::RequestRespawn(PC, PendingKiller). Works on dedicated servers (no animation runs there) and is immune to AnimNotify RPC loss. If timeout is 0, RequestRespawn is called immediately.
OnDeathStarted (BlueprintNativeEvent on ASLCharacterBase) — BP override point. _Implementation is a no-op in C++. BP override does NOT need to call parent.
OnDeathFinished (BlueprintNativeEvent on ASLCharacterBase) — BP override point. _Implementation is a no-op in C++. Respawn is NOT triggered from here. Override in Blueprint for any per-character effects at the end of the death animation.
USLAnimNotify_DeathFinished — placed at the end of the death animation state in the Anim BP. Calls Character->EnableRagdoll() only. Has no effect on respawn timing. If missing, ragdoll won't play but the player will still respawn at the correct time.
GC_Death (GameplayCueNotify_Static, tag GameplayCue.Character.Death) — handles everything cosmetic on all clients:
Is Locally Controlled→ switch to TP view (input is gated byIsCharacterDead()in the controller — noDisableInputcall)
- Play death montage on TP mesh
- Death SFX/VFX
- Cue BP lives in
Content/SystemLink/AbilitySystem/Cues/
See RespawnSystem.md for the full respawn flow.
#Hit Direction
When any damage is applied (shield or health), PostGameplayEffectExecute calculates the hit direction and sends it to the owning client via Client_OnTookDamage(ESLHitDirection). The direction is also stored in LastHitDirectionReplicated (replicated) so simulated proxies can read it.
#Enums — Types/SLHitDirection.h
ESLHitDirection — 8-way, relative to the target's control rotation (camera facing, not mesh facing — correct when strafing):
None | Forward | Back | Left | Right | ForwardLeft | ForwardRight | BackLeft | BackRight |
|---|
Used for the HUD damage overlay (8-way vignette) and for CalculateHitDirection.
ESLHitDirection4 — 4-way cardinal collapse of the above:
None | Forward | Back | Left | Right |
|---|
Used by FSLAnimState (and therefore Anim BPs). Diagonals map to their dominant cardinal (ForwardLeft/ForwardRight → Forward, BackLeft/BackRight → Back).
#Calculation
USL_BlueprintLibrary::CalculateHitDirection(HitFromWorldLocation, HitActor) — static BP function. Pass attacker world location + target actor → returns ESLHitDirection. Uses dominant-axis dot product against Forward/Right vectors.
The collapse from 8-way to 4-way happens inside BuildAnimSnapshots via a local CollapseDirection lambda.
#FSLAnimState Fields (both ESLHitDirection4)
| Field | Lifetime | Use |
|---|---|---|
HitDirection | One frame only — resets to None next tick | Hit reaction animations: check != None to detect a new hit this frame |
LastHitDirection | Persists until next hit | Death animations: Anim BP reads this when entering the death state |
Source priority for LastHitDirection (evaluated in BuildAnimSnapshots):
PendingHitDirection— set this frame byClient_OnTookDamageon the owning client
AnimStateSnapshot.LastHitDirection— retained from last frame (owning client)
LastHitDirectionReplicated— server-set replicated value, read by simulated proxies
#HUD Hooks — USLHealthWidget
USLHealthWidget is a C++ base (Public/UI/SLHealthWidget.h) that subscribes to GAS attribute delegates and fires BlueprintImplementableEvents. Subclass in Blueprint (WBP_SL_HealthWidget) for all visuals and sounds.
| Event | Signature | When | Implement in BP to… |
|---|---|---|---|
OnHealthChanged | (float Percent) | Any health attribute change | Update health bar cells/fill |
OnShieldChanged | (float Percent) | Any shield attribute change | Update shield bar cells/fill |
OnImpact | (ESLHitDirection Direction) | Owning client takes any damage | Play Bink damage vignette on the correct screen edge |
Sound placement in WBP_SL_HealthWidget:
| Sound | Trigger | Notes |
|---|---|---|
| Low health warning (loop) | OnHealthChanged — Percent crosses below threshold | Cache prev percent; start loop on crossing down, stop on crossing back up. Threshold is a tunable property. |
| Shield regen start hum | OnShieldChanged — Percent begins increasing from < 1.0 | Cache prev percent to detect direction of change |
| Shield regen complete chime | OnShieldChanged — Percent hits 1.0 (was < 1.0) | Stop hum, play one-shot chime |
InitializeHealthWidget(ASC) is called by ASLPlayerCharacter::InitializeLocalPlayerHUD(ASC) — a private C++ helper that fires from all three possession paths (listen server host via PossessedBy, client via Client_PawnInitialized RPC, and OnRep_PlayerState as a fallback). It runs on first spawn and every respawn. Do not call it from BeginPlay — the ASC is not valid there.
#Gameplay Cues (built)
| Cue Tag | Trigger | Effect |
|---|---|---|
GameplayCues.Character.Death | GA_Death via DeathCueTag on character | Disable input, TP view switch, death SFX/VFX on all clients |
#Gameplay Cues (planned)
| Cue Tag | Trigger | Effect |
|---|---|---|
GameplayCues.Character.ShieldDepleted | Shield hits 0 inside damage execution | Shield break sound + flash FX, replicated |
#Character Stats GE — GE_SL_CharacterStats
An Infinite GE applied on BeginPlay (via USLAbilitySet) that sets the baseline values.
| Modifier | Op | Value | Purpose |
|---|---|---|---|
MaxHealth | Override | 100 | Sets max health cap |
MaxShield | Override | 50 | Sets max shield cap |
ShieldRechargeRate | Override | 10 | Units/sec during regen (captured by GE_SL_ShieldRegen) |
CurrentHealth and CurrentShield are not set here — the attribute set constructor initializes them and PostGameplayEffectExecute clamps them to their Max. Setting Current values in an Infinite GE would hold them at a fixed value, preventing damage from reducing them.
Per-character tuning: change the Override scalars in the GE Blueprint, or subclass it per character type.
#Implementation Status
#Done
USLHealthAttributeSet— attributes +PostGameplayEffectExecute(includesShieldRechargeRate)
USLDamageExecution— shield-first absorption
GE_SL_DamageBlueprint — wired into both fire paths
GE_DeadBlueprint +Deadtag blocks on all abilities
BP_GA_SL_Death(USLGameplayAbility_Death) — full death sequence
GC_DeathBlueprint — cosmetics on all clients
GE_SL_ShieldRegenDelay+GE_SL_ShieldRegenBlueprints — confirmed working
GE_SL_CharacterStatsBlueprint — sets MaxHealth, MaxShield, ShieldRechargeRate viaAS_AbilitySet_Default
USLHealthWidget+WBP_SL_HealthWidget— health/shield bars, replicated to clients
ESLHitDirection+CalculateHitDirection+FSLAnimStatehit direction fields
Client_OnTookDamageRPC →OnImpact(Direction)on both health widget and damage overlay widget
ASLTestDamageEmitter— dev actor for testing directional damage pipeline
- Shield regen audio events on
ASLCharacterBase—OnShieldRechargeStarted(Percent),OnShieldRechargeInterrupted,OnShieldRechargeFull,OnHitImpact(Direction)
USLHealthWidgetdetects regen state transitions in C++ and calls character audio events directly
USLDamageOverlayWidget— full-screen C++ base for the directional hit vignette;OnImpact(Direction)BlueprintImplementableEvent. Hosted inDamageOverlayContainer(BindWidget) onUSLHUDWidget, constructed inNativeConstructalongside the health widget. Subclass asWBP_SL_DamageOverlay.
#Done (continued)
ASLHealthPickup—OnCollectedchecks health not full, applies heal GE, callsClient_ShowHealthPickupNotification. Subclass in BP, setHealEffectClass.
USL_BlueprintLibrary::SelectSoundByHealthPercent— returns null above 50%, remaps[50%→0%]across a sound array for progressive low-health audio.
GE_RespawnBlueprint — Instant GE, resetsCurrentHealthandCurrentShieldto their Max values via Attribute Based Override modifiers.
ASLPlayerHUD::PushToGameStack / PushToMenuStack / PushToModalStack— convenience BP-callable methods wrappingUSLPrimaryGameLayout::PushWidgetToLayer.
ESLHitDirection4— 4-way enum;FSLAnimState.HitDirectionandLastHitDirectionchanged from 8-way to 4-way. Collapse happens inBuildAnimSnapshots.
LastHitDirectionReplicated— replicated property onASLCharacterBase; set by authority inPostGameplayEffectExecute; used as fallback for simulated proxies inBuildAnimSnapshots.
IsDead()—BlueprintPureonASLCharacterBase; checks ragdoll latch OR Dead gameplay tag. Safe on all machines.
OnClientDeathStarted()—BlueprintImplementableEvent BlueprintCallableonASLCharacterBase; fires on owning client fromClient_OnDeathStarted. Use in BP to hide HUD, play camera shake, etc.
- Server-side death timeout —
StartServerDeathTimeout()onASLCharacterBase; called fromPostGameplayEffectExecute; readsDeathAnimationTimeoutfromASLGameModeBase; firesRequestRespawnindependently of any AnimNotify. Works on dedicated servers.
- Death camera —
ASLPlayerCharacterre-attaches camera boom topelvison death, extends arm, enables lag, and ticksTickDeathCamera()to look atspine_03during ragdoll.
- HUD restore on respawn —
ASLPlayerCharacter::BeginPlaycallsSetVisibility(SelfHitTestInvisible)on the HUD widget so it reappears after respawn.
bDebugonASLCharacterBase— gates all death/respawn on-screen debug prints. Default false.
Server_DebugForceRespawn()— stripped in Shipping; forces immediate respawn for testing.
#Done (continued)
OnPawnInitialized(bool bIsRespawn)—BlueprintImplementableEvent BlueprintCallableonASLCharacterBase; fires on the owning client when controller + ASC are both valid; covers first spawn (false) and respawn (true). ReplacesBeginPlayas the BP initialization hook.
Client_PawnInitialized(bool bIsRespawn)— Client RPC onASLPlayerCharacter; two-flag coordination (bPendingPawnInit,bPendingIsRespawn) handles both orderings: RPC beforeOnRep_PlayerState, or vice versa.
InitializeLocalPlayerHUD(ASC)— private C++ helper onASLPlayerCharacter; restores HUD visibility + re-inits health/damage overlay widgets; called from all three possession paths.
ASLPlayerController::IsCharacterDead()— private helper; all input handlers exceptLookearly-return if this returns true. NoDisableInputorSetInputModecalls anywhere in the death/respawn flow.
USLWeaponsComponent::UnbindAllDelegates()—BlueprintCallable; clears all BP bindings onOnEquippedWeaponChangedandOnAmmoChanged. Call fromOnClientDeathStartedbefore the pawn is destroyed.
- Respawn overlay stored on
ASLPlayerController(ShowRespawnOverlay/HideRespawnOverlayBP methods) — survives pawn swap; created withAdd to Player Screen(notPush to Game Stack) to avoid CommonUI input capture side effects.
- HUD WeaponsComponent rebind pattern —
OnClientDeathStartedunbinds viaUnbindAllDelegates;GA_SL_EquipDefaultLoadoutrebindsOnEquippedWeaponChanged/OnAmmoChangedto the new character's WeaponsComponent after bothInventoryLoadedandPlayerReadytags are present (i.e. just before the weapon equips). Binding here rather than directly inOnPawnInitializedensures the weapon is actually equipped before delegate handlers run.
#Done (continued)
- Overshield —
GE_SL_OvershieldCharge(Infinite Periodic, AttributeBased OvershieldChargeRate 0.1 per tick, grants Overshield tag).OvershieldChargeRateattribute added toUSLHealthAttributeSet.PostGameplayEffectExecutecaps CurrentShield at MaxShield 2 when Overshield tag present; auto-removes GE when fully charged or burned off.ASLOvershieldPickupC++ pickup class.OnOvershieldChanged(float OvershieldPercent)BlueprintImplementableEvent onUSLHealthWidgetfires whenever overshield changes (0 = none, 1 = full).
#Done (continued)
GC_SL_ShieldDepleted—GameplayCueNotify_Burst; fires on all clients viaExecuteGameplayCueinUSLDamageExecutionwhen shield hits 0; plays shield-break SFX + activatesC_ShieldDepletedFXcomponent on the character.
GC_SL_OvershieldActive—GameplayCueNotify_Static; firesOnActive/OnRemoveon all clients viaAddGameplayCue/RemoveGameplayCueinASLOvershieldPickupandUSLHealthAttributeSet; drivesC_OvershieldActiveFXandC_OvershieldDepletionFXcomponents.
- Overshield health guard —
USLDamageExecutionchecksOvershieldtag before calculating health damage; health cannot be damaged while overshield is active.
USLAnimNotify_Footstep— subclass ofUAnimNotify_PlaySound; place once, extend later for surface detection without touching placed instances.