How-To: Persist Combat State Across Levels

Goal: keep buffs, cooldowns and attribute values across a ServerTravel instead of resetting the player every time they walk through a door.

Prerequisites
Quick Start.

How it works

A UCrimsonAbilityStateCarrierSubsystem (a game instance subsystem, so it outlives the world) holds a snapshot between levels. The old world stashes; the new world consumes.

Server-side only
The snapshot is authoritative state captured on the server. Clients receive the restored state through normal ASC replication - there is nothing to do on the client.

1. Choose what persists

Tag your effects with Crimson.Persistence.Persist to opt in, or Crimson.Persistence.DoNotPersist to opt out, then set the default policy in Project Settings -> Crimson -> Ability State Persistence. Loose tags added by game code can be kept out of the snapshot with Excluded Loose Tags in the same settings - matched hierarchically, so excluding a parent tag excludes every child under it. The exclusion applies on restore too, which heals snapshots taken before a tag joined the list.

What is kept, what is deliberately left out, and the order it comes back in.
Never capturedWhy
Effects granted by an ability set or equipmentThe loadout re-grants them next session. Capturing them too would apply them twice.
Instant effectsTheir result is already baked into the attribute base values, which are captured.
Effects built at runtimeA NewObject<UGameplayEffect> has no class of its own - its configuration lives on that one instance, so only the bare UGameplayEffect would be stored and nothing could be rebuilt from it. Author a real GE asset for anything that must survive a save.
Tags an active ability holds via ActivationOwnedTagsThey end with the ability, and nothing re-applies them after travel - capturing them would bring them back as permanently stuck loose tags (a lock-on state tag outliving the lock-on, for example). The capture tallies what every active ability is holding and subtracts it automatically.
Loose tags listed under Excluded Loose TagsThe project's own escape hatch for transient status tags added by game code that must not survive a travel.
Pools come back as a fraction, not a number
List a pool and its maximum under Pool Attributes in the settings (Health / MaxHealth, Shield / MaxShield) and the snapshot stores how full it was rather than how many points it held. This matters because the maximum is rarely the same on both sides of a save - whatever raises it is re-established at its own moment during load. A pool saved at 100/100 while a +50 buff was absent returns as 150/150 once it is back, instead of a permanently reduced 100/150.
Verify
The settings section lists your policy and the tag filters.

2. Stash and restore

Get Game Instance Subsystem (CrimsonAbilityStateCarrierSubsystem) then Stash Snapshot before travel and Consume Snapshot after the new level loads. Has Snapshot and Peek Snapshot let you check without consuming.

SaveGame is not Replicated
Restored values are applied on the server. If a value looks right on the server and stale on a client, the property is missing its replication - restoring does not replicate on your behalf.
Verify
Buff yourself, travel, and confirm the buff and its remaining duration survive. Then repeat with Play As Client and confirm the client sees the restored state too.

3. Effect-level persistence

UCrimsonEffectPersistenceComponent handles the per-effect bookkeeping if you want persistence without driving the carrier yourself. Add it to the actor that owns the ASC.

Verify
Effects tagged to persist survive travel with their remaining duration intact.

See also

  • Concept: Ability System Component
  • Concept: Multiplayer and Prediction