Gameplay · Updated 2552.07.31.08.07
Respawn System
Halo-style player respawn built on top of the existing death ability (BPGASLDeath) and UE's RestartPlayer flow. Death triggers a server-side timeout that starts the respawn countdo
Halo-style player respawn built on top of the existing death ability (BP_GA_SL_Death) and UE's RestartPlayer flow. Death triggers a server-side timeout that starts the respawn countdown independently of any animation notify, shows a countdown HUD with the killer's name, and re-initializes the character's ASC on the new pawn.
#Flow Overview
USLHealthAttributeSet::PostGameplayEffectExecute — CurrentHealth hits 0 (authority)
→ Character->PendingKiller = EffectCauser
→ ASC->HandleGameplayEvent(Events.Character.Death) — activates GA_Death
→ Character->StartServerDeathTimeout() — server timer starts immediately
GA_Death activates (authority + locally predicted on owner)
→ Client_OnDeathStarted() — calls OnClientDeathStarted() BP event (no DisableInput)
→ Death animation plays (AnimBP reads bIsDead from AnimStateSnapshot)
→ USLAnimNotify_DeathFinished at end of death anim state
→ Character->EnableRagdoll() — physics on mesh, capsule collision off (cosmetic only)
Server death timeout fires (after DeathAnimationTimeout seconds)
→ ASLGameModeBase::RequestRespawn(PC, PendingKiller)
→ Client_OnRespawnBegin(RespawnDelay, KillerName) — start countdown HUD
→ PC->UnPossess()
→ [timer: RespawnDelay seconds]
→ RestartPlayer(PC)
→ PossessedBy() on new pawn (server)
→ Remove Dead + shield + InventoryLoaded tags / GEs
→ TakeFromASC + re-grant CombatantAbilitySet + LoadoutAbilitySet
→ Apply GE_Respawn (reset health + shield)
→ IsLocallyControlled: InitializeLocalPlayerHUD() + OnPawnInitialized(true)
→ else: Client_PawnInitialized(true) RPC
→ LoadDefaultLoadout() — spawns weapons, adds to CarriedWeapons
→ IsLocallyControlled: SetBooleanStateTag(InventoryLoaded, true) directly
→ else: Client_OnLoadoutReady() RPC → client adds InventoryLoaded to ASC
→ Client_PawnInitialized arrives on client
→ Remove stale InventoryLoaded from ASC (persists from previous pawn via PlayerState)
→ InitializeLocalPlayerHUD() — restores HUD visibility, re-inits health/damage widgets
→ OnPawnInitialized(bIsRespawn=true) BP event
→ hides respawn overlay, caches PlayerController / PlayerHUD refs
→ BeginLoadoutEquipWatch() — C++, watches for both handshake tags
→ Client_OnLoadoutReady arrives on client
→ SetBooleanStateTag(InventoryLoaded, true) on ASC
→ BeginLoadoutEquipWatch (ASLPlayerCharacter, owning client)
→ checks InventoryLoaded AND ReadyToFinishLoadout, then listens if not yet satisfied
→ on both present: consumes ReadyToFinishLoadout
→ Server_RequestEquipFirst() → server equips CarriedWeapons[0]
→ OnLoadoutEquipRequested() BP event
→ End Ability
Key point: Respawn is driven entirely by the server-side timeout, not by the AnimNotify. The AnimNotify only activates ragdoll physics. If the notify is missing from the Anim BP, the ragdoll won't play — but the player will still respawn at the correct time.
#Game Mode — ASLGameModeBase
#Properties
| Property | Type | Default | Notes |
|---|---|---|---|
bRespawnDelay | bool | true | If false, RestartPlayer fires instantly with no countdown. |
RespawnDelay | float | 5.0 | Seconds before the player respawns. Only used when bRespawnDelay is true. |
DeathAnimationTimeout | float | 8.0 | How long the server waits after death before starting the respawn countdown. Set to match the death animation length. If 0, respawn starts immediately. |
RagdollDestroyDelay | float | 0.0 | Seconds after respawn before the dead pawn is destroyed. 0 = destroy immediately. |
#Functions
**RequestRespawn(APlayerController PC, AActor Killer)**
- Authority only.
- Extracts killer name from
Killer->GetPlayerState()->GetPlayerName(). Falls back to"Unknown".
- If
bRespawnDelayis false: callsPC->UnPossess()thenRestartPlayerimmediately, no countdown sent.
- If
bRespawnDelayis true: sendsClient_OnRespawnBegin(RespawnDelay, KillerName), callsPC->UnPossess(), then starts a per-controllerFTimerHandlethat callsRestartPlayerafterRespawnDelayseconds.
PC->UnPossess()is required beforeRestartPlayer— if the PC still possesses a pawn,AGameModeBase::RestartPlayerreuses/teleports the existing pawn instead of spawning a new one.
- Stores active timers in
TMap<TObjectPtr<APlayerController>, FTimerHandle> RespawnTimerHandles.
Logoutoverride clears the timer for any disconnecting player.
#Character — ASLCharacterBase
#Death Timeout
StartServerDeathTimeout() (public, authority only)
- Called from
USLHealthAttributeSet::PostGameplayEffectExecuteimmediately when health hits 0.
- Reads
DeathAnimationTimeoutfromASLGameModeBase(falls back to 8s if GameMode is not anASLGameModeBase).
- If timeout is 0: calls
RequestRespawndirectly (SetTimer with rate 0 clears the timer rather than firing).
- Otherwise: sets
ServerDeathTimeoutHandleto fireRequestRespawnafterDeathAnimationTimeoutseconds.
- Debug prints gated behind
bDebug = trueon the character defaults.
#RPCs
Client_OnDeathStarted() (Client, Reliable)
- Calls
OnClientDeathStarted()BlueprintImplementableEvent for BP-side effects (hide HUD, camera effects).
- Does not call
DisableInput— input is gated byIsCharacterDead()checks in eachASLPlayerControllerhandler instead (see Controller section below).
- Overridden in
ASLPlayerCharacterto also attach the camera boom topelvis, extend the arm, enable camera lag, and disable pawn control rotation.
Client_OnRespawnBegin(float Delay, const FString& KillerName) (Client, Reliable)
- Fires
OnRespawnBegin(Delay, KillerName)BlueprintImplementableEvent.
- BP drives the countdown widget and killer message.
#BlueprintImplementableEvents
OnClientDeathStarted() — fires on the owning client immediately when death begins. Implement in BP to: hide the HUD (Set Visibility → Collapsed), call WeaponsComponent → UnbindAllDelegates, play camera shake, show respawn overlay via ShowRespawnOverlay on the PlayerController.
OnRespawnBegin(float Delay, FText KillerName) — implement in BP to show the respawn countdown overlay with the killer name.
OnPawnInitialized(bool bIsRespawn) — fires on the owning client when the new pawn is fully initialized (controller + ASC both valid). Fires on first spawn (bIsRespawn = false) and on every respawn (bIsRespawn = true). Use this instead of BeginPlay for any initialization that requires a valid controller or ASC. In BP, use this to: update HUD character/weapons references, rebind WeaponsComponent delegates, remove the respawn overlay (via PlayerController reference), cast and cache PlayerController / PlayerState references.
#BlueprintPure
IsDead() — returns true if ragdoll is active OR if the ASC has the States.Character.Dead tag. Safe to call on all machines. Ragdoll latch prevents the shared ASC tag removal (on new pawn possession) from flipping dead characters back to alive mid-ragdoll.
#Properties
RespawnEffectClass (EditDefaultsOnly) — set to GE_Respawn in BP defaults.
bDebug (EditDefaultsOnly, default false) — when true, prints on-screen messages showing when the server death timeout starts, fires, and if it fails.
LastHitDirectionReplicated (Replicated, ESLHitDirection) — set by USLHealthAttributeSet on authority whenever damage lands. All machines read this as a fallback in BuildAnimSnapshots when PendingHitDirection is None (e.g. simulated proxies who don't receive Client_OnTookDamage).
#Debug
Server_DebugForceRespawn() (BlueprintCallable, Server, Reliable — stripped in Shipping)
- Immediately calls
UnPossess+RestartPlayer+Destroyon the caller's pawn.
- Use from a keyboard input in BP to test the respawn flow without dying.
#Death Camera — ASLPlayerCharacter
When Client_OnDeathStarted fires on the locally controlled player:
- Camera boom re-attached to the
pelvisbone (follows ragdoll, not the fallen capsule).
TargetArmLengthextended toDeathCameraArmLength(default 150).
bEnableCameraLagenabled withCameraLagSpeed = DeathCameraLagSpeed(default 6) to smooth ragdoll jitter.
bUsePawnControlRotationdisabled.
TickDeathCamera() runs every frame when ragdoll is active + locally controlled. It rotates the camera to always face the spine_03 bone so the camera orbits the body naturally.
| Property | Default | Notes |
|---|---|---|
DeathCameraArmLength | 150 | Arm length during death camera. |
DeathCameraLagSpeed | 6 | Camera lag speed to smooth ragdoll physics jitter. |
#Respawn Initialization — PossessedBy / Client_PawnInitialized
On the server in PossessedBy, after InitAbilityActorInfo:
- Detect respawn: check if ASC already has
States.Character.Deadtag.
- If respawning:
RemoveActiveEffectsWithGrantedTags(Dead, ShieldRechargingDelay, ShieldRecharging, Overshield)— removes all persistent GEs from the shared ASC. Also removesInventoryLoadedloose tag if present.
TakeFromASConPlayerState.CombatantGrantedHandles+PlayerState.LoadoutGrantedHandles— clears old specs before re-granting. Handles live on PlayerState so they survive pawn destruction.
- Re-grant
DefaultCombatantAbilitySetand loadout set into PlayerState handles.
- If respawning: apply
RespawnEffectClass(Instant, overrides CurrentHealth and CurrentShield to their Max values).
- Listen server host (locally controlled authority): calls
InitializeLocalPlayerHUD(ASC)then firesOnPawnInitialized(bIsRespawn)directly.
- Dedicated server / remote client: sends
Client_PawnInitialized(bIsRespawn)RPC.
LoadDefaultLoadout()— spawns new weapon actors fromASLGameModeBase::DefaultLoadout, adds them toCarriedWeaponson the new pawn.
- Listen server host:
SetBooleanStateTag(InventoryLoaded, true)directly on the local ASC.
- Remote clients: sends
Client_OnLoadoutReady()RPC.
Client_PawnInitialized(bool bIsRespawn) (Client, Reliable) on ASLPlayerCharacter:
- Removes stale
InventoryLoadedtag from the ASC before callingOnPawnInitialized. The ASC lives on PlayerState and persists across respawns — the tag from the previous pawn is still present and would let the equip fire immediately against an emptyCarriedWeapons.
- If
ASCis ready: callsInitializeLocalPlayerHUD(ASC)+OnPawnInitialized(bIsRespawn).
- If
PlayerStatehas not yet replicated (ASCis null): setsbPendingPawnInit = trueand waits forOnRep_PlayerStateto fire the same path.
Client_OnLoadoutReady() (Client, Reliable) on ASLPlayerCharacter:
- Adds
InventoryLoadedloose tag to the client's ASC.
- Sent by the server after
LoadDefaultLoadout()completes. Using an explicit RPC instead ofOnRep_CarriedWeaponsbecauseOnRepfires before the new pawn's PlayerState has replicated, makingGetAbilitySystemComponent()return null and silently dropping the tag.
- If the ASC is null when this RPC arrives (new pawn's PlayerState not yet replicated), it sets
bPendingInventoryLoadedandOnRep_PlayerStateapplies the tag once the ASC is valid. Earlier this case silently dropped the tag, which — combined with the bug below — caused intermittent stuck respawns (overlay frozen at "RESPAWN IN 1") because the equip waits onInventoryLoadedforever. Worse with 3+ players (more replication contention).
OnRep_PlayerStatemust NOT removeInventoryLoadedbased onCarriedWeapons.Num(). An earlier version removed the tag wheneverCarriedWeaponshadn't replicated yet, which raced against the authoritativeClient_OnLoadoutReadyRPC and could permanently strip a validly-set tag → stuck respawn. It now only ever adds the tag (driven bybPendingInventoryLoaded, withCarriedWeapons.Num() > 0as a secondary fallback). The stale-tag clear still happens inClient_PawnInitialized's immediate path.
InitializeLocalPlayerHUD(ASC) (public, local only):
- Restores HUD visibility (
SetVisibility(SelfHitTestInvisible)).
- Calls
InitializeHealthWidget(ASC)onUSLHealthWidget.
- Calls
InitializeDamageOverlayWidget(ASC)onUSLDamageOverlayWidget.
- Calls
BindWeaponDelegates(WeaponsComponent)onUSLHUDWidget— bindsOnEquippedWeaponChangedandOnAmmoChangedbefore any equip fires.
- Called from all three possible arrival orderings:
PossessedBy(listen server host),Client_PawnInitialized(client, PlayerState ready),OnRep_PlayerState(client, RPC arrived first).
- Also called from
ASLPlayerHUD::BeginPlayif the pawn is already possessed — necessary because on first spawn,PossessedByfires before the HUD widget exists, soGetHUDWidget()returns null duringPossessedBy.ASLPlayerHUD::BeginPlaycatches this and re-runs initialization after the widget is constructed.
#Loadout & Default Equip — ASLPlayerCharacter::BeginLoadoutEquipWatch
The spawn loadout is equipped by the pawn, not by a gameplay ability. BP_GA_EquipDefaultLoadout and its Wait Gameplay Tag Query were deleted on 2026-07-30; the history is below because it explains a constraint that is easy to walk back into.
Flow:
BeginLoadoutEquipWatch()is called from all three possession paths —PossessedBy(host),
Client_PawnInitialized (client) and OnRep_PlayerState (whichever of those two loses the race). It is idempotent; the first one to find a valid ASC wins.
- It checks for
InventoryLoaded+ReadyToFinishLoadoutimmediately, and only registers tag callbacks
if they are not already satisfied.
- When both are present it sets
bLoadoutEquipRequested, unregisters, consumesReadyToFinishLoadout, calls
Server_RequestEquipFirst() and fires the OnLoadoutEquipRequested Blueprint event.
bLoadoutEquipRequestedis per-pawn, and a respawn is a new pawn, so it resets naturally.
Server_RequestEquipFirst() (Server, Reliable) on ASLCharacterBase looks up CarriedWeapons[0] server-side. The client passes no weapon reference — CarriedWeapons may not have replicated yet when the request goes out.
### ⚠ Why this is on the pawn and not in an ability
An ability cannot outlive its own spec, and the respawn path removes it. PossessedBy does
TakeFromASC + re-grant of the ability sets; that removes the running instance. Measured on a client
respawn, with the ability logging its own lifecycle:
```
ACTIVATED on C_3 (client) — Inventory=0 Ready=0
requirements NOT met — listening for tag changes
tag changed — Inventory=1 Ready=0
EndAbility (cancelled=0) ← two frames after activating
```
The intro's view-mode transition does not complete for another ~35 frames. The ability was always going
to be dead before the thing it was waiting for arrived. On the host the re-grant happens before
OnPawnInitialized, so the ordering never bites — which is why this only ever showed on clients.
The old Blueprint worked by coincidence, and that is the part worth remembering. Because
ReadyToFinishLoadout was never cleared, after the first life it was permanently present, so the query went
true the instant InventoryLoaded arrived — two frames before the instance died. It never actually waited
for the intro on a respawn. The never-removed tag was load-bearing: it kept the wait short enough to beat
the ability's own destruction. Three separate principled-looking fixes (clear it on BeginPlay, consume it
at the point of use, reimplement the ability in C++) each removed that coincidence without replacing it, and
each regressed a different respawn case.
The pawn exists for the whole window. There is no instance to lose and no ordering to get right.
### ⚠ Check first, then listen
BeginLoadoutEquipWatch evaluates the tags before registering callbacks, and that is not defensive
padding. A tag callback never fires for state that is already true, so an implementation that only listens
hangs whenever it starts after the thing it is waiting for. On a client the RPCs can land in either order,
so both cases genuinely happen. Same defect family as BUG-028's reticle.
#Handle Persistence — Why All Handles Live on ASLPlayerState
The ASC lives on PlayerState and persists across respawns. The pawn (and all its components) is destroyed and recreated by RestartPlayer on each respawn.
Any FSLAbilitySet_GrantedHandles or FGameplayAbilitySpecHandle stored on the pawn or WeaponsComponent is destroyed with the pawn. On the new pawn, those handles are empty, so TakeFromASC/ClearAbility are no-ops — old ability specs accumulate on the shared ASC. Each respawn adds another copy.
All handles are stored on ASLPlayerState:
| Handle | Granted by | Used in |
|---|---|---|
CombatantGrantedHandles | DefaultCombatantAbilitySet in PossessedBy | PossessedBy (TakeFromASC before re-grant) |
LoadoutGrantedHandles | PlayerLoadoutAbilitySet in PossessedBy | PossessedBy (TakeFromASC before re-grant) |
WeaponFireGrantedHandles | Per fire mode in GrantWeaponAbilities | GrantWeaponAbilities (TakeFromASC before re-grant) |
EquipAbilityHandle | Per-weapon TP equip ability in SetPendingEquipWeapon | SetPendingEquipWeapon + UnequipWeapon |
FPEquipAbilityHandle | Per-weapon FP equip ability in SetPendingEquipWeapon | SetPendingEquipWeapon + UnequipWeapon |
USLWeaponsComponent uses a fallback pattern for bot characters (no PlayerState):
ASLPlayerState* SLPS = Pawn->GetPlayerState<ASLPlayerState>();
FSLAbilitySet_GrantedHandles& FireHandles = SLPS ? SLPS->WeaponFireGrantedHandles : WeaponFireGrantedHandles;
#Input During Death — ASLPlayerController
Input is never disabled on death. Instead, each input handler checks IsCharacterDead() at the top and early-returns if the character is dead. This avoids all DisableInput / EnableInput / SetInputMode state management and eliminates PIE context bleeding bugs.
IsCharacterDead() (private) — casts GetPawn() to ASLCharacterBase, returns IsDead(). Returns false if pawn is null (safe during transitions).
| Handler | Gated on dead? | Reason |
|---|---|---|
Move, Jump, StopJumping, Crouch, StopCrouch | Yes | No movement while dead |
StartPrimaryFire, StartSecondaryFire | Yes | GAS also blocks via ActivationBlockedTags, but gated for consistency |
StopPrimaryFire, StopSecondaryFire | No | Ability cancellation should still run to clean up any in-flight fire state |
Interact, DropWeapon | Yes | Can't interact or drop while dead |
Look | No | Death camera responds to look input |
#Respawn Overlay — ASLPlayerController
The respawn overlay widget is stored on the PlayerController (not the character) because it must survive the pawn swap. The character BP calls methods on the PC to show/hide it.
ShowRespawnOverlay(Delay, Message)— createsWBP_SL_RespawnOverlay, stores the reference on the PC, callsAdd to Player Screen. Called fromOnClientDeathStartedon the character.
HideRespawnOverlay()— callsRemove from Parenton the stored widget, clears the reference. Called fromOnPawnInitializedon the new character.
Do not usePush to Game Stackfor the respawn overlay. CommonUI activatable widgets capture input when pushed onto a stack, causing the cursor to appear as a side effect. UseAdd to Player Screenfor any widget that is purely visual and does not need input focus.
#AnimNotify — USLAnimNotify_DeathFinished
Placed at the end of the death animation state in the Anim BP. Only enables ragdoll physics. Has no effect on respawn timing.
Notify fires → EnableRagdoll() on the owning ASLCharacterBase
Respawn is driven by the server-side timeout started at PostGameplayEffectExecute time, completely independent of this notify.
#Blueprint Work Required
#GE_Respawn ✅
- Duration Policy: Instant
- Modifier 1:
CurrentHealth— Override — Attribute Based —MaxHealth(Base Value, coefficient 1.0, source Target)
- Modifier 2:
CurrentShield— Override — Attribute Based —MaxShield(Base Value, coefficient 1.0, source Target)
#BP_GA_SL_Death
- On ability activation: cast avatar to
ASLCharacterBase→ callClient On Death Started.
#Anim BP — Death State
- Add
USLAnimNotify_DeathFinishedat the end of the death animation state.
- This enables ragdoll. It no longer has any involvement in respawn timing.
#BP_SL_MasterChief
- Set
RespawnEffectClass = GE_Respawnin defaults.
- Implement
OnClientDeathStarted(): Set Visibility → Collapsedon HUD widget.Get WeaponsComponent → UnbindAllDelegates.Get Owning Player Controller → ShowRespawnOverlay(Delay, KillerName).
- Implement
OnPawnInitialized(bIsRespawn): - Cache
PlayerController,PlayerState,HUD Widgetreferences (safe to cast here — controller and state are guaranteed valid). - Hide the respawn overlay via the cached
PlayerController. Do not activate anything to equip the weapon —ASLPlayerCharacter::BeginLoadoutEquipWatchhandles that in C++ and starts itself from the same possession paths.
- Implement
OnRespawnBegin(Delay, KillerName)— passDelay+KillerNameinto the respawn overlay.
#Loadout equip — no Blueprint work required
Handled entirely by ASLPlayerCharacter::BeginLoadoutEquipWatch. There is deliberately no ability, no Wait Gameplay Tag Query, and nothing to activate. BP_GA_EquipDefaultLoadout was deleted 2026-07-30 — see the Loadout & Default Equip section for why an ability could not do this job.
Optional: implement OnLoadoutEquipRequested on the character BP if anything cosmetic should happen at the exact moment the equip is requested (the respawn overlay currently hides earlier, at OnPawnInitialized).
#BP_SL_GameMode
bRespawnDelay = true,RespawnDelay = 5.0,DeathAnimationTimeout = 8.0in defaults.
- Set
bRespawnDelay = falsefor instant-respawn match configs.
#Respawn Countdown Widget
WBP_SL_RespawnOverlay✅ — exists atContent/SystemLink/UI/HUD/HealthAndShield/.
- Added to screen via
Add to Player Screen(notPush to Game Stack) — no input capture, no cursor side effect.
- Reference stored on
ASLPlayerController(persists across pawn swap).
- Drive countdown from
Delayparam passed intoOnRespawnBegin.
- Removed via
HideRespawnOverlaycalled fromOnPawnInitialized(bIsRespawn=true)on the new character.
#What Is Not Yet Built
- Respawn spawn point selection strategy (currently uses UE default
PlayerStartselection).
- "Corpse lingers" mode —
RagdollDestroyDelay > 0support exists in GameMode but the dead pawn detach-before-respawn path is not implemented.
- Kill feed / scoreboard integration.
WBP_SL_RespawnOverlaywidget — Blueprint work.