How-To: Make an Actor Poolable
Goal: take control of how your actor comes back into play, optionally let it reclaim itself so callers never pair an acquire with a release, and pool a class you cannot edit through an adapter.
1. Decide whether you need the interface
Implement ICrimsonPoolable (include Pooling/ICrimsonPoolable.h) 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.
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.
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.bHidden next to the pool. Set the flag on the server in both hooks and call its OnRep by hand - the server never runs its own OnRep - and clients follow through replication. Reset every per-use counter and latch in On Acquired From Pool, never in BeginPlay.UPROPERTY(ReplicatedUsing = OnRep_Active)bool bActive = false;void AMyPooledActor::OnRep_Active(){SetActorHiddenInGame(!bActive);SetActorEnableCollision(bActive);}void AMyPooledActor::OnAcquiredFromPool_Implementation(const FTransform& Transform){SetActorTransform(Transform, /*bSweep*/ false, nullptr, ETeleportType::TeleportPhysics);ElapsedSeconds = 0.0f; // per-use state, reset on every acquirebReleaseRequested = false; // one-way latch, reset on every acquireif (HasAuthority()){bActive = true; // clients follow through OnRep_ActiveOnRep_Active(); // the server never calls its own OnRep}}void AMyPooledActor::OnReleasedToPool_Implementation(){if (HasAuthority()){bActive = false;OnRep_Active();}}
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.
// Headervirtual FCrimsonOnPoolableReleaseRequested* GetPoolReleaseDelegate() override{return &PoolReleaseRequested;}private:FCrimsonOnPoolableReleaseRequested PoolReleaseRequested;// Wherever the actor decides it is done:PoolReleaseRequested.Broadcast(this);
GetPoolReleaseDelegate is not a UFUNCTION, because a Blueprint cannot hand out a delegate reference. A Blueprint actor that wants the same behavior calls Release Actor on the pool itself when it finishes, or broadcasts its own event dispatcher and lets the code that acquired it release it on that dispatcher.HasAuthority().Release Actor. Releasing twice is guarded against and returns false, but relying on that guard hides a real bug in your ownership model.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. A projectile, for example, should ask whether the flight is still active and if so close its hit window, clear its lifetime timer and stop its 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.
| Approach | Owner | Use it when |
|---|---|---|
Release Actor After(Actor, Seconds) | The caller | Any 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 parameters | The actor | The actor has real completion semantics - it should raise its own finished events and tidy its own state. Right for anything gameplay-relevant. |
A projectile is the worked example of the second. Give it a per-shot parameter struct of your own carrying 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 broadcasting its release delegate to be reclaimed.
Give your projectile a Launch Projectile function taking a per-shot parameter struct of your own. Call it straight after Acquire Actor, split the struct pin, and set Velocity plus Lifetime Override and Max Range if this shot needs them. Leave either at 0 to fall back to the actor's own defaults.
Max Range into the same lifetime timer and let the projectile never tick. Only a path that curves - gravity, homing or bouncing - has to be measured in flight, because it covers less ground per second than a straight line and a derived time would fire too late.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.5. Pool a class you cannot modify
When the actor class belongs to another plugin or to third-party code, you cannot add ICrimsonPoolable to it. Register an ICrimsonPoolableAdapter for the class instead: it supplies the same hooks from outside, and the pool calls it wherever it would have called the interface.
Adapters are C++ only - they are registered once at module or engine-subsystem startup, not placed in a level. From Blueprint, pool the class with the default handling (implement nothing), or wrap it in a Blueprint child class of your own that implements Crimson Poolable as in step 2.
ICrimsonPoolable itself always takes precedence over any adapter. Everything the interface's exclusivity rule says applies here too: while an adapter is registered, the pool never applies its default activation or deactivation to that class.See also
- Concept: The Reuse Contract
- How-To: Pool a Replicated Actor
- API Reference