Concept: The Reuse Contract

A pooled actor is spawned once and used many times. Every assumption that this runs when the actor comes into existence is wrong from the second use onwards. Nearly every pooling bug is one of three violations of that.

BeginPlay runs once per lifetime, not once per use

This is the rule people break first. BeginPlay fires when the actor is created and never again, no matter how many times it is recycled. Anything per-use initialised there is stale on the second use.

The fix is to move that work into an activation function that runs on every use. ACrimsonProjectile is a worked example: every per-flight value - remaining pierces, the lifetime timer, the hit window, the expiry guard - is set in LaunchProjectile, and BeginPlay does nothing but wire up the things that genuinely are once-per-lifetime.

This is also why the pool cannot tell you whether an instance was recycled
Acquire Actor deliberately does not report it. If your code needs to know, it is because per-use work is happening in BeginPlay - and the fix is to move that work, not to branch on it. Run per-use initialisation unconditionally on every acquire and the question stops mattering.

One-way latches break reuse

A flag that is only ever set to true - bHasFired, bFinished, bExpiring - does its job on the first use and then permanently disables the actor. The second use ends the moment it starts.

Every such guard must be reset on acquire. The projectile's bExpiring flag is reset at the top of every launch, and that single line is what makes a recycled projectile usable at all.

Delegates bound per use multiply

Binding a handler on every acquire adds a second handler on the second use, a third on the third. Damage applies twice, then three times. Bind once, in BeginPlay; if a binding genuinely has to be per-use, unbind it on release.

Who owns the actor's state

By default the pool owns reactivation: it hides, disables collision, stops movement on release, and reverses that on acquire. Implementing ICrimsonPoolable transfers that ownership wholesale to the actor. There is no middle setting, and that is intentional - two owners writing the same state is the failure mode, not a feature.

Two concrete ways shared ownership breaks, both of which the exclusivity rule prevents:

  1. Visibility. An actor that drives bHidden from a replicated property, while the pool also writes bHidden directly: bHidden replicates on its own, the actor's OnRep never fires because its own state did not change, and clients see a visible actor the server considers parked.
  2. Collision. The pool's default deactivation clears the actor-level collision flag, which overrides every component. An actor that re-enables its own components on activation has no reason to know that gate exists - it comes back looking right and hitting nothing.

See also

  • How-To: Make an Actor Poolable
  • Concept: Replication and Recycling