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.

AttributeDefaultNotes
CurrentHealth100Clamped [0, MaxHealth]. Hits 0 → death.
MaxHealth100Set per character via GE_CharacterStats.
CurrentShield50Clamped [0, MaxShield]. Recharges after delay.
MaxShield50Set 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:

  • CurrentShield and CurrentHealth are captured as Snapshot = false (live values at execution time).
  • DamageMagnitude is a SetByCaller tag (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.

FieldValue
DurationInstant
ExecutionUSLDamageExecution
SetByCaller tagSLTags::Data::Damage
AssetTagsSLTags::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).

FieldValue
Duration3 seconds (configurable via curve)
PeriodNone
GrantedTagsSLTags::States::Character::ShieldRechargingDelay
RemoveGameplayEffectsWithTagsitself (so re-applying resets the timer)
StackingTypeReplace on Apply
On ExpireGrants 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.

FieldValue
DurationInfinite
Period0.1 seconds
GrantedTagsSLTags::States::Character::ShieldRecharging
ModifierCurrentShield += 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)

FieldValue
DurationInfinite
Period0.1 seconds
ModifierCurrentShield += OvershieldChargeRate * 0.1
GrantedTagsSLTags::States::Character::Overshield
Max stack capMaxShield * 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):

  1. CurrentHealth clamped to 0.
  1. Attribute set uses ASC->GetAvatarActor() to reach the character (NOT GetOwningActor() — that returns PlayerState when ASC lives on PlayerState).
  1. Guard against re-entry: skip if States.Character.Dead tag already present.
  1. Store EffectCauser in Character->PendingKiller for use by the respawn flow.
  1. Send Events.Character.Death gameplay event → activates GA_Death (handles GE_Dead, CancelAllAbilities, cue dispatch, collision/movement off).
  1. 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 by IsCharacterDead() in the controller — no DisableInput call)
  • 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):

NoneForwardBackLeftRightForwardLeftForwardRightBackLeftBackRight

Used for the HUD damage overlay (8-way vignette) and for CalculateHitDirection.

ESLHitDirection4 — 4-way cardinal collapse of the above:

NoneForwardBackLeftRight

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)

FieldLifetimeUse
HitDirectionOne frame only — resets to None next tickHit reaction animations: check != None to detect a new hit this frame
LastHitDirectionPersists until next hitDeath animations: Anim BP reads this when entering the death state

Source priority for LastHitDirection (evaluated in BuildAnimSnapshots):

  1. PendingHitDirection — set this frame by Client_OnTookDamage on the owning client
  1. AnimStateSnapshot.LastHitDirection — retained from last frame (owning client)
  1. 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.

EventSignatureWhenImplement in BP to…
OnHealthChanged(float Percent)Any health attribute changeUpdate health bar cells/fill
OnShieldChanged(float Percent)Any shield attribute changeUpdate shield bar cells/fill
OnImpact(ESLHitDirection Direction)Owning client takes any damagePlay Bink damage vignette on the correct screen edge

Sound placement in WBP_SL_HealthWidget:

SoundTriggerNotes
Low health warning (loop)OnHealthChanged — Percent crosses below thresholdCache prev percent; start loop on crossing down, stop on crossing back up. Threshold is a tunable property.
Shield regen start humOnShieldChanged — Percent begins increasing from < 1.0Cache prev percent to detect direction of change
Shield regen complete chimeOnShieldChanged — 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 TagTriggerEffect
GameplayCues.Character.DeathGA_Death via DeathCueTag on characterDisable input, TP view switch, death SFX/VFX on all clients

#Gameplay Cues (planned)

Cue TagTriggerEffect
GameplayCues.Character.ShieldDepletedShield hits 0 inside damage executionShield break sound + flash FX, replicated

#Character Stats GE — GE_SL_CharacterStats

An Infinite GE applied on BeginPlay (via USLAbilitySet) that sets the baseline values.

ModifierOpValuePurpose
MaxHealthOverride100Sets max health cap
MaxShieldOverride50Sets max shield cap
ShieldRechargeRateOverride10Units/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

  1. USLHealthAttributeSet — attributes + PostGameplayEffectExecute (includes ShieldRechargeRate)
  1. USLDamageExecution — shield-first absorption
  1. GE_SL_Damage Blueprint — wired into both fire paths
  1. GE_Dead Blueprint + Dead tag blocks on all abilities
  1. BP_GA_SL_Death (USLGameplayAbility_Death) — full death sequence
  1. GC_Death Blueprint — cosmetics on all clients
  1. GE_SL_ShieldRegenDelay + GE_SL_ShieldRegen Blueprints — confirmed working
  1. GE_SL_CharacterStats Blueprint — sets MaxHealth, MaxShield, ShieldRechargeRate via AS_AbilitySet_Default
  1. USLHealthWidget + WBP_SL_HealthWidget — health/shield bars, replicated to clients
  1. ESLHitDirection + CalculateHitDirection + FSLAnimState hit direction fields
  1. Client_OnTookDamage RPC → OnImpact(Direction) on both health widget and damage overlay widget
  1. ASLTestDamageEmitter — dev actor for testing directional damage pipeline
  1. Shield regen audio events on ASLCharacterBaseOnShieldRechargeStarted(Percent), OnShieldRechargeInterrupted, OnShieldRechargeFull, OnHitImpact(Direction)
  1. USLHealthWidget detects regen state transitions in C++ and calls character audio events directly
  1. USLDamageOverlayWidget — full-screen C++ base for the directional hit vignette; OnImpact(Direction) BlueprintImplementableEvent. Hosted in DamageOverlayContainer (BindWidget) on USLHUDWidget, constructed in NativeConstruct alongside the health widget. Subclass as WBP_SL_DamageOverlay.

#Done (continued)

  1. ASLHealthPickupOnCollected checks health not full, applies heal GE, calls Client_ShowHealthPickupNotification. Subclass in BP, set HealEffectClass.
  1. USL_BlueprintLibrary::SelectSoundByHealthPercent — returns null above 50%, remaps [50%→0%] across a sound array for progressive low-health audio.
  1. GE_Respawn Blueprint — Instant GE, resets CurrentHealth and CurrentShield to their Max values via Attribute Based Override modifiers.
  1. ASLPlayerHUD::PushToGameStack / PushToMenuStack / PushToModalStack — convenience BP-callable methods wrapping USLPrimaryGameLayout::PushWidgetToLayer.
  1. ESLHitDirection4 — 4-way enum; FSLAnimState.HitDirection and LastHitDirection changed from 8-way to 4-way. Collapse happens in BuildAnimSnapshots.
  1. LastHitDirectionReplicated — replicated property on ASLCharacterBase; set by authority in PostGameplayEffectExecute; used as fallback for simulated proxies in BuildAnimSnapshots.
  1. IsDead()BlueprintPure on ASLCharacterBase; checks ragdoll latch OR Dead gameplay tag. Safe on all machines.
  1. OnClientDeathStarted()BlueprintImplementableEvent BlueprintCallable on ASLCharacterBase; fires on owning client from Client_OnDeathStarted. Use in BP to hide HUD, play camera shake, etc.
  1. Server-side death timeout — StartServerDeathTimeout() on ASLCharacterBase; called from PostGameplayEffectExecute; reads DeathAnimationTimeout from ASLGameModeBase; fires RequestRespawn independently of any AnimNotify. Works on dedicated servers.
  1. Death camera — ASLPlayerCharacter re-attaches camera boom to pelvis on death, extends arm, enables lag, and ticks TickDeathCamera() to look at spine_03 during ragdoll.
  1. HUD restore on respawn — ASLPlayerCharacter::BeginPlay calls SetVisibility(SelfHitTestInvisible) on the HUD widget so it reappears after respawn.
  1. bDebug on ASLCharacterBase — gates all death/respawn on-screen debug prints. Default false.
  1. Server_DebugForceRespawn() — stripped in Shipping; forces immediate respawn for testing.

#Done (continued)

  1. OnPawnInitialized(bool bIsRespawn)BlueprintImplementableEvent BlueprintCallable on ASLCharacterBase; fires on the owning client when controller + ASC are both valid; covers first spawn (false) and respawn (true). Replaces BeginPlay as the BP initialization hook.
  1. Client_PawnInitialized(bool bIsRespawn) — Client RPC on ASLPlayerCharacter; two-flag coordination (bPendingPawnInit, bPendingIsRespawn) handles both orderings: RPC before OnRep_PlayerState, or vice versa.
  1. InitializeLocalPlayerHUD(ASC) — private C++ helper on ASLPlayerCharacter; restores HUD visibility + re-inits health/damage overlay widgets; called from all three possession paths.
  1. ASLPlayerController::IsCharacterDead() — private helper; all input handlers except Look early-return if this returns true. No DisableInput or SetInputMode calls anywhere in the death/respawn flow.
  1. USLWeaponsComponent::UnbindAllDelegates()BlueprintCallable; clears all BP bindings on OnEquippedWeaponChanged and OnAmmoChanged. Call from OnClientDeathStarted before the pawn is destroyed.
  1. Respawn overlay stored on ASLPlayerController (ShowRespawnOverlay / HideRespawnOverlay BP methods) — survives pawn swap; created with Add to Player Screen (not Push to Game Stack) to avoid CommonUI input capture side effects.
  1. HUD WeaponsComponent rebind pattern — OnClientDeathStarted unbinds via UnbindAllDelegates; GA_SL_EquipDefaultLoadout rebinds OnEquippedWeaponChanged / OnAmmoChanged to the new character's WeaponsComponent after both InventoryLoaded and PlayerReady tags are present (i.e. just before the weapon equips). Binding here rather than directly in OnPawnInitialized ensures the weapon is actually equipped before delegate handlers run.

#Done (continued)

  1. Overshield — GE_SL_OvershieldCharge (Infinite Periodic, AttributeBased OvershieldChargeRate 0.1 per tick, grants Overshield tag). OvershieldChargeRate attribute added to USLHealthAttributeSet. PostGameplayEffectExecute caps CurrentShield at MaxShield 2 when Overshield tag present; auto-removes GE when fully charged or burned off. ASLOvershieldPickup C++ pickup class. OnOvershieldChanged(float OvershieldPercent) BlueprintImplementableEvent on USLHealthWidget fires whenever overshield changes (0 = none, 1 = full).

#Done (continued)

  1. GC_SL_ShieldDepletedGameplayCueNotify_Burst; fires on all clients via ExecuteGameplayCue in USLDamageExecution when shield hits 0; plays shield-break SFX + activates C_ShieldDepletedFX component on the character.
  1. GC_SL_OvershieldActiveGameplayCueNotify_Static; fires OnActive / OnRemove on all clients via AddGameplayCue / RemoveGameplayCue in ASLOvershieldPickup and USLHealthAttributeSet; drives C_OvershieldActiveFX and C_OvershieldDepletionFX components.
  1. Overshield health guard — USLDamageExecution checks Overshield tag before calculating health damage; health cannot be damaged while overshield is active.
  1. USLAnimNotify_Footstep — subclass of UAnimNotify_PlaySound; place once, extend later for surface detection without touching placed instances.