How-To: Pool a Replicated Actor
Goal: pool an actor that replicates, without clients losing it, missing a use, or acting on stale data.
1. Acquire and release on the server only
The pool refuses both calls on a client for any class that replicates, and logs a warning. Non-replicated cosmetic actors are unaffected, so a client can still pool its own effects. Nothing to configure - just do not write client code that acquires.
AcquireActor refused on a client must never appear in the log.2. Never use COND_InitialOnly
This is the single most likely mistake. COND_InitialOnly only sends in an actor's first bunch. A pooled actor's channel is already open on its second use, so that state is never resent and clients keep acting on the first use's data - silently, with no error.
void AMyPooledActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{Super::GetLifetimeReplicatedProps(OutLifetimeProps);// WRONG for a pooled actor - only ever sent on the first use.// DOREPLIFETIME_CONDITION(AMyPooledActor, ActivationState, COND_InitialOnly);DOREPLIFETIME(AMyPooledActor, ActivationState);}
ForcePropertyCompare on acquire to mitigate this, but do not build load-bearing logic on owner-conditional state in a pooled actor.3. Put a counter in your activation state
If an actor is recycled with identical parameters, a byte-identical property is not seen as changed and the use is skipped entirely on clients. A counter that increments every use guarantees the property is always dirty.
USTRUCT()struct FMyActivationState{GENERATED_BODY()UPROPERTY() FVector_NetQuantize100 Location = FVector::ZeroVector;UPROPERTY() bool bActive = false;/** Incremented every activation. Wrapping is harmless - only inequality between two* consecutive activations matters, and 256 uses cannot elapse between net updates. */UPROPERTY() uint8 UseCounter = 0;};
4. Leave net dormancy on
Project Settings > Crimson > Crimson Object Pool > Use Net Dormancy While Pooled is on by default. Leave it on. A released actor is hidden and non-colliding, which is exactly the condition the engine treats as not relevant - without dormancy the channel closes for relevancy a few seconds after each release and every client destroys its copy.
NetCullDistance, which loses relevancy and destroys the client copy just as surely. The pool leaves released actors exactly where they were, hidden and non-collidable. Do not move them in On Released To Pool either.5. Check your class is poolable at all
An actor with bOnlyRelevantToOwner cannot be pooled safely: changing the owner on reuse makes it non-relevant to the previous owner, whose client destroys it. The pool warns once per class if you try.
See also
- Concept: Replication and Recycling
- Concept: The Reuse Contract
- How-To: Diagnose Pool Problems