How-To: Make an Actor Poolable

Goal: take control of how your actor comes back into play, and optionally let it reclaim itself so callers never pair an acquire with a release.

Prerequisites
Quick Start finished. Note that most actors need none of this - if hide, disable-collision and stop-moving is enough, implement nothing.

1. Decide whether you need the interface

Implement ICrimsonPoolable (from CrimsonCommon) when either of these is true:

  • Your actor already drives its own visibility, collision or movement from its own state - especially from a replicated property.
  • Your actor knows when it is finished, and you would rather it reclaimed itself than made every caller remember.
The interface is exclusive, not additive
Implementing it opts your actor out of the pool's default reactivation entirely - on its first use as well as every use after. You become responsible for transform, visibility, collision, tick and components. The pool will still handle ownership, net dormancy and its own bookkeeping.

2. Implement the two hooks

Class Settings -> Interfaces -> Add Crimson Poolable. Then implement Event On Acquired From Pool (Transform input) and Event On Released To Pool. In the acquire event: Set Actor Transform (Teleport Physics), Set Actor Hidden In Game false, Set Actor Enable Collision true. In the release event: the reverse.

Do not forget the actor-level collision flag
SetActorEnableCollision(false) clears a gate that overrides every component. If you disable it on release and forget to re-enable it on acquire, your actor comes back looking correct but colliding with nothing - and re-enabling individual components will not fix it.
Verify
Acquire, run, release, acquire again. The second use must look identical to the first. If anything is missing the second time, see Concept: The Reuse Contract.

3. Optional - let the actor reclaim itself

When the actor knows it is finished, publish a release delegate. The pool binds it once per instance and reclaims the actor automatically - the call site then has nothing to pair with the acquire.

cpp
// Header
virtual FCrimsonOnPoolableReleaseRequested* GetPoolReleaseDelegate() override
{
return &PoolReleaseRequested;
}
private:
FCrimsonOnPoolableReleaseRequested PoolReleaseRequested;
// Wherever the actor decides it is done:
PoolReleaseRequested.Broadcast(this);
C++ only
GetPoolReleaseDelegate is not a UFUNCTION, because a Blueprint cannot hand out a delegate reference. A Blueprint actor that wants the same behaviour simply calls Release Actor on the pool itself when it finishes.
Pick one or the other
If the actor reclaims itself, the caller must not also call Release Actor. Releasing twice is guarded against and returns false, but relying on that guard hides a real bug in your ownership model.
On Released To Pool must handle being called early
Do not write it assuming the actor's own completion path already ran. Release Actor After, Drain Pool and any manual Release Actor can all release an actor that is still mid-use - and if the hook does nothing in that case, the actor is parked in the free list still live and visible, then handed straight back out to the next caller.

Check your own active state and stand down if needed. ACrimsonProjectile does exactly this: if the flight is still active it closes the hit window, clears the lifetime timer and stops the movement, rather than trusting that expiry already did so.

4. Choosing how a use ends

There are two ways to give a pooled instance a bounded life, and they are not interchangeable. Pick by asking who owns the decision.

ApproachOwnerUse it when
Release Actor After(Actor, Seconds)The callerAny pooled actor. A blunt cutoff - the pool releases it, and the actor gets On Released To Pool with no notion of having 'finished'. Right for cosmetics: impacts, decals, casings.
The actor's own per-use parametersThe actorThe actor has real completion semantics - it should raise its own finished events and tidy its own state. Right for anything gameplay-relevant.

ACrimsonProjectile is the worked example of the second. Its per-shot parameters carry the lifetime and range for that shot, so one projectile Blueprint serves a snap shot and a long charged shot, and expiry runs the projectile's proper path - closing the hit window, raising its expired events, then asking to be reclaimed.

Split the Launch Params struct pin on Launch Projectile and set Velocity, plus Lifetime Override and Max Range if this shot needs them. Leave either at 0 to use the projectile's own defaults.

Range costs nothing on an ordinary shot
A straight shot at constant speed reaches a given distance at a predictable time, so Max Range is folded into the lifetime timer and the projectile never ticks. Only a path that curves - gravity, homing or bouncing - is measured in flight, because it covers less ground per second than a straight line and a derived time would fire too late.
Do not stack the two
Calling Release Actor After on a projectile that already has its own lifetime gives it two independent cutoffs, and whichever lands first wins. That is rarely what anyone means - set the lifetime on the shot instead.

See also

  • Concept: The Reuse Contract
  • How-To: Pool a Replicated Actor
  • API Reference