Death, Downed & Respawn

Death in CrimsonCore is a gameplay ability, not component code. UCrimsonHealthComponent only reports the fact - it sends GameplayEvent.Death when health runs out and flips DeathState when told to. What that fact means for a particular character is authored per character: an NPC falls over and is pooled, a player bleeds out and can be picked back up. Both are the same ability class family listening to the same event.

The state machine

text
health reaches 0
|
v
UCrimsonHealthComponent::HandleOutOfHealth
sends GameplayEvent.Death to the owner's ASC [server]
|
v
the granted death ability activates [ServerInitiated, ReplicateYes]
cancel every ability without Ability.Behavior.SurvivesDeath
SetCanBeCanceled(false)
|
+-- UCrimsonCoreGameplayAbility_Death ------------------------+
| StartDeath() -> DeathState replicates |
| DeathMontage (or FallbackDeathDuration) |
| FinishDeath() -> DeathState replicates |
| ACrimsonCharacter: destroyed next tick |
| ACrimsonNPCBase: recycled if bCanBePooled |
| owning client only: respawn dialog |
| -> APlayerController::ServerRestartPlayer() |
| |
+-- UCrimsonCoreGameplayAbility_Downed -----------------------+
EnterDowned()
bleed-out end stamped in server time
component replicates bDowned + end time + duration
every machine: Status.Death.Downed, movement lock,
sphere QueryOnly, DownedMontage
owning client: HUD.Slot.Downed readout
|
+-- Revive(Instigator) -> ResetForRespawn, health to
| ReviveHealthPercent, get-up
| montage, abilities re-activated,
| EndAbility (never died)
|
+-- FinishOff(Instigator) --+
+-- bleed-out expired ------+--> BeginDying() -> the
generic path above

Who runs where

The server owns every decision; every other machine replays presentation from replicated state. There is no client-authored step anywhere in this system.

MachineWhat it does
ServerRuns the ability, cancels the other abilities, owns the bleed-out timer and the damage penalty, calls StartDeath / FinishDeath, decides every revive and finish-off, writes the revived health.
Every machineReplays OnDeathStarted / OnDeathFinished from the replicated DeathState; applies Status.Death.Downed, the movement lock and the sphere's collision from the component's OnRep_Downed; plays the montages through the ability's replicated montage task.
Owning client (or listen host)Additionally: the HUD.Slot.Downed readout, and the respawn dialog once DeathFinished lands. Both are gated on IsLocallyControlledPlayer() / IsLocallyControlled(), so a dedicated server and every simulated proxy skip them.
The respawn binding is deliberately never unbound
UCrimsonCoreGameplayAbility_Death binds OnDeathFinished on the owning client and leaves it bound through EndAbility. The ability's end and the health component's DeathState reach the client through different actor channels, so the end can land first - unbinding there would drop the very notification the prompt is waiting for. The ability is InstancedPerActor and outlives its own end; the binding dies with the pawn's component and cannot leak past it.

The ragdoll replicates as a cue, not as physics

The death ability exists on the server and on the owning client, and nowhere else - so it cannot reach a simulated proxy by calling a function or firing a multicast. What it does instead is add a persistent gameplay cue, GameplayCue.Crimson.Death.Ragdoll, to the ability system. That cue lands in the ability system's replicated active-cue list, which goes out to every connection and is replayed for players who join afterwards, so a late joiner sees a corpse that is already on the floor rather than one standing up. The server runs the handler locally too, which is what a listen host sees. Every machine then simulates its own skeletal mesh through UCrimsonCoreGameplayCueNotify_Ragdoll -> UCrimsonCoreRagdollStatics::SetRagdollEnabled. Nothing about the physics itself is replicated, so two players will not see a body land in exactly the same pose; that is the deliberate trade against replicating a full rigid-body state for a corpse nobody interacts with again.

EndAbility removes the cue on the server. The corpse is hidden and destroyed in that same moment so the restore is invisible - but the ability lives on the PlayerState's ability system, which outlives the pawn, and an active cue left behind would fire on the next avatar the player possesses. Restoring reads the collision profile, the collision mode and the relative transform back off the mesh component's archetype rather than off a snapshot taken on the way in: the archetype is the one description of how the mesh is supposed to sit on the actor that is still correct after a pooled actor's previous life, or after a re-entrant call.

Downed is not dead

Entering the downed state never touches DeathState. IsDeadOrDying() stays false, the capsule keeps its collision, and only movement is locked - SetIgnoreMoveInput(true) on the controller plus StopMovementImmediately() and DisableMovement() on the character movement component. That is a deliberate choice, not an omission: lock-on, nameplates and hostile AI go on treating a downed pawn as a live target, which is exactly what "damage shortens the bleed-out" requires.

The bleed-out clock is a server timer. What replicates is the stamp: DownedEndServerTime against server world time, plus DownedDurationSeconds. Clients never derive an end time of their own - GetRemainingSeconds() subtracts AGameStateBase::GetServerWorldTimeSeconds() from the replicated stamp, so a client's countdown agrees with the server's timer without any per-tick traffic. Each damaging hit while down subtracts DamageBleedOutPenaltySeconds, re-arms the timer for the new remainder, and republishes the stamp, which is why the HUD number jumps rather than drifting.

Why the penalty listens to damage, not to health
The binding is to UCrimsonHealthSet::OnDamageTaken, which fires for any damaging hit even at zero health. OnHealthChanged would miss a hit fully absorbed by a barrier or a shield - and being shot while helpless should cost the player time whether or not the number moved.

Who may revive, and who may finish

The downed component offers both options to every scan. What decides which one a given player actually sees is a UCrimsonInteractionCondition_TeamAttitude (from CrimsonTeams) attached to each option, with FailureBehavior = Hide - so exactly one survives per viewer, the single-key prompt works, and the context menu shows one row.

OptionFilter propertyDefaultPasses for
ReviveReviveAllowedAttitudesFriendly & Neutral (Support)Anyone the interacting character is not hostile toward
Finish OffFinishOffAllowedAttitudesHostile OnlyEnemies of the downed character

The attitude comes from UCrimsonTeamStatics::GetTeamAttitudeBetween, evaluated from the interacting character toward the downed one. Friendly already covers self, anyone sharing a team tag, and party members - a party mate reads Friendly even across faction lines, so no party-specific wiring is needed. Neutral is its own answer and not a weak Friendly: an actor with no team agent, or one whose tags neither overlap nor conflict, reads Neutral and would fail a FriendlyOnly filter. That is why the Revive default is Friendly and Neutral.

The client's option list is presentation only
UCrimsonCoreGameplayAbility_ReviveDowned and UCrimsonCoreGameplayAbility_FinishOffDowned are granted to the interacting character by the nearby-interaction scan, and both do nothing outside IsNetAuthority(). Before either reaches ExecuteAction, UCrimsonCoreGameplayAbility_InteractionHandler::PassesInteractionConditions re-gathers the target's options on the server and re-evaluates the attitude condition, so a client firing the event for an option it never qualified for is refused. They are two separate classes because nearby-interaction grants are cached per ability class - one class with a flag would only ever be granted once.

Coming back up

Revive does four things in order, all on the server:

  1. LeaveDowned() - clear the timers, unbind the damage hook and the buff tag watches, end the self-revive wait task, and tell the component to lift the downed state (which lifts the tag, the movement lock and the HUD on every machine).
  2. UCrimsonHealthSet::ResetForRespawn(), then SetNumericAttributeBase(Health, EffectiveMaxHealth * ReviveHealthPercent). The reset matters because the set lives on the PlayerState and latched bOutOfHealth when the character went down - without clearing it the revived character could never die again.
  3. GetUpMontage if one is authored, otherwise StopMontageIfCurrent(DownedMontage) - targeted rather than a blanket stop, because something else may legitimately be playing by then.
  4. TryActivateAbilitiesOnSpawn() on the ability system component.
Why the abilities have to be re-activated
Going down canceled every ability that did not carry Ability.Behavior.SurvivesDeath - including the always-on, spawn-activated ones: movement, the interact ability, the native input handlers. Those normally come back only when a new avatar is assigned, and a revived character keeps the avatar it had. TryActivateAbilitiesOnSpawn skips specs that are still active, so nothing is double-activated. Skip this and a revived player stands up unable to move or interact.

Respawn is the same idea one level up. FinishDeath leads to DetachFromControllerPendingDestroy, which puts the controller into the Inactive state - exactly the state APlayerController::ServerRestartPlayer_Implementation acts on. The dialog's button therefore calls the engine's existing reliable RPC and nothing new was needed. Any dialog result respawns, not just Confirmed: the dialog has one button, so any other result means the player dismissed it with a back action, and a dismissal that did not respawn would strand the controller Inactive with no pawn and no second prompt.

Known limitation

Kill credit stays with whoever downed the character
OnDeathStarted credits the health component's last damage instigator - the character that put the target down - not the one who delivered the finisher. This is documented rather than worked around: a scoring or bounty system that needs the finisher should read it from On Finished Off, which reports the finishing instigator (and null on a bleed-out expiry).

API reference

Types

TypeHeaderPurpose
UCrimsonCoreGameplayAbility_DeathAbilities/Death/CrimsonCoreGameplayAbility_Death.hThe generic death. Config: DeathMontage, FallbackDeathDurationSeconds, bRagdollOnDeath, RagdollDelaySeconds, RagdollDurationSeconds, RagdollGameplayCueTag, RespawnHeader, RespawnBody, RespawnButtonText, RespawnButtonDelaySeconds. Blueprint: Request Respawn, Enable Ragdoll, Disable Ragdoll (all callable), Show Respawn Prompt (native event, override to draw your own screen), On Death Started.
UCrimsonCoreRagdollStaticsCharacter/CrimsonCoreRagdollStatics.hBlueprint function library. Set Ragdoll Enabled(Actor, bEnabled, RagdollCollisionProfile = "Ragdoll") and Is Ragdoll Enabled(Actor). Both are local to the machine they run on - nothing is replicated and no authority is checked. Enabling stops the mesh's active montage so the animation stops fighting the physics; disabling restores the collision profile, collision mode and relative transform from the mesh component's archetype and re-attaches it to the actor's root.
UCrimsonCoreGameplayCueNotify_RagdollGameplayCues/CrimsonCoreGameplayCueNotify_Ragdoll.hUGameplayCueNotify_Static handler for GameplayCue.Crimson.Death.Ragdoll. OnActive, WhileActive and OnExecute enable the ragdoll; OnRemove disables it. The cue manager discovers handlers by scanning asset paths, so the actual handler is a Blueprint of this class named GCN_Crimson_Death_Ragdoll in /CrimsonCore/GameplayCues - create it there by hand with Blueprint Class -> All Classes -> `CrimsonCoreGameplayCueNotify_Ragdoll`; it needs no graph and no properties.
UCrimsonCoreGameplayAbility_DownedAbilities/Death/CrimsonCoreGameplayAbility_Downed.hSubclass adding the bleed-out. Config: DownedMontage, GetUpMontage, DownedDurationSeconds, DamageBleedOutPenaltySeconds, ReviveHealthPercent, SelfReviveEventTag, SelfReviveBuffTags, SelfReviveBuffDelaySeconds. Blueprint: Is Downed (pure), On Downed, On Revived, On Finished Off. C++: Revive(AActor*), FinishOff(AActor*), both authority-only.
UCrimsonCoreDownedComponentCharacter/CrimsonCoreDownedComponent.hUSphereComponent + ICrimsonInteractableTarget. Blueprint: Find Downed Component (static, pure), Request Revive / Request Finish Off (authority-only), Is Downed, Get Remaining Seconds, Get Downed End Server Time, Get Downed Duration Seconds, and the On Downed State Changed(bool) / On Downed End Time Changed(float) delegates. Replicated: bDowned, DownedEndServerTime, DownedDurationSeconds.
UCrimsonCoreGameplayAbility_ReviveDownedAbilities/Death/CrimsonCoreGameplayAbility_ReviveDowned.hInteraction handler granted to the reviver; forwards to RequestRevive on the target's component. Authority only.
UCrimsonCoreGameplayAbility_FinishOffDownedAbilities/Death/CrimsonCoreGameplayAbility_FinishOffDowned.hThe hostile counterpart; forwards to RequestFinishOff. Authority only.
UCrimsonCoreDownedStatusWidgetUserInterface/Death/CrimsonCoreDownedStatusWidget.hUCommonUserWidget readout. Config: TitleText, CountdownFormat, UpdateIntervalSeconds. Optional bindings: Text_Title, Text_Countdown (UCommonTextBlock), Bar_BleedOut (UProgressBar). Blueprint: On Bleed Out Tick(RemainingSeconds, Fraction), On Downed Ended, Get Downed Component.

Gameplay tags (all four are native tags in CrimsonGameplayTags, CrimsonCore)

TagSet byMeaning
Status.Death.DownedUCrimsonCoreDownedComponent::ApplyDownedState, as a loose tag on every machineBleeding out but not yet dying. A child of Status.Death, so the SurvivesDeath gate and the Ability.ActivateFail.IsDead reason cover it too.
GameplayEvent.Downed.SelfReviveYour content - a consumable's use ability, a Blueprint Send Gameplay Event to Actor, a cheatSent to a downed pawn's own ability system to revive it. The default value of SelfReviveEventTag.
HUD.Slot.DownedUsed as the default DownedStatusSlotTagUI extension slot for the downed player's bleed-out readout. Place a CrimsonUIExtensionPoint with this tag in your HUD layout.
GameplayCue.Crimson.Death.RagdollUCrimsonCoreGameplayAbility_Death::EnableRagdoll, on the server, as a persistent cueThe corpse should be simulating physics. The default value of RagdollGameplayCueTag, and the tag GCN_Crimson_Death_Ragdoll answers to. Removed when the ability ends.

See also

  • How-To: Death, Downed & Respawn - the setup checklist, step by step, in Blueprint and C++.
  • Character Pipeline - ability set grants, and the NPC object pool a dead NPC returns to.
  • Vitals: Shield, Barrier & Reservation - the health, absorption and reservation the death event comes out of.
  • Interaction Integration - how the Revive and Finish Off prompts are discovered and held.
  • Nameplates - why a downed pawn keeps its plate.