Spawning Pooled NPCs
Goal: have enemies recycle instead of being spawned and destroyed, so a wave costs almost nothing after the first one - and understand the one rule that makes pooled NPCs behave correctly in multiplayer.
UCrimsonNPCData asset, an ACrimsonEnemySpawner placed in the level, and an ACrimsonNPCBase subclass to spawn. See CrimsonObjectPool -> Overview for the pool itself.bCanBePooled on the UCrimsonNPCData and everything below happens automatically - there is no code to write.How a pooled spawn flows
ACrimsonEnemySpawner calls RequestSpawnEnemy on UCrimsonEnemySpawnerSubsystem, which queues it. The queue is time-sliced so a burst never spawns everything in one frame. When a request is served, recycling is delegated to UCrimsonObjectPoolSubsystem - the spawner subsystem itself no longer owns a free list.
| Step | Who | What happens |
|---|---|---|
| Request | ACrimsonEnemySpawner | RequestSpawnEnemy(Class, NPCData, Transform, Spawner) enqueues; nothing spawns yet. |
| Serve | UCrimsonEnemySpawnerSubsystem | One request per queue pass. If bCanBePooled, it calls AcquireActor; otherwise it spawns outright. |
| Activate | ACrimsonNPCBase | The pool calls OnAcquiredFromPool, which runs ActivateFromPool - unhide, restore collision, re-init the ASC, spawn a controller. |
| Configure | UCrimsonEnemySpawnerSubsystem | InitializeFromData grants ability sets, then SetSpawnerOrigin, then ResetHealthForPoolActivation for a recycled instance. |
| Recycle | ACrimsonNPCBase | On death, OnDeathFinished calls RecycleEnemy, which releases the NPC back to the pool instead of destroying it. |
The multiplayer rule that matters
A pooled NPC stays exactly where it died - hidden, non-collidable, and put into net dormancy by the pool. It is tempting to move pooled actors somewhere out of the way, and that is the one thing you must not do.
(0, 0, 50000) puts them outside their net cull distance. That makes them non-relevant, which closes their channel for relevancy - and a channel closed for that reason makes every client destroy its copy. The next reuse then has to recreate the NPC on every client, which is the entire cost pooling exists to avoid.Net dormancy closes the channel for a reason clients preserve instead, which is why the pool uses it. This system used to do the graveyard teleport and it was a real bug; do not reintroduce it in a
DeactivateForPooling override.The trap is subtler than it looks: hiding an actor and disabling its root collision is already enough to make it non-relevant, with or without a teleport. Net dormancy is what actually solves it, and the pool applies that for you.
Subclassing an NPC that gets pooled
ACrimsonNPCBase implements ICrimsonPoolable, so the pool never writes its visibility, collision or tick - the NPC owns all of that. Override ActivateFromPool and DeactivateForPooling to add your own setup and teardown, and call Super first in both.
Use Event On Enemy Spawned From Pool and Event On Enemy Pooled on your NPC Blueprint. They fire only for instances that have actually been recycled, so a first-time spawn does not trigger them.
BeginPlay is stale the second time an NPC is used, and any flag that is only ever set to true will make its second life end instantly. Put per-use setup in ActivateFromPool and reset every latch there. Full detail in CrimsonObjectPool -> Concept: The Reuse Contract.Checking it actually works
Crimson.ObjectPool.Dump
Your enemy classes appear alongside every other pooled class. spawnsAvoided rising while created stays flat means recycling is working. If created keeps climbing during steady play, NPCs are being spawned and never recycled - check bCanBePooled is ticked.
created should have stopped rising well before twenty. Then run Play As Client with 2 clients, kill a pooled enemy, and wait more than five seconds - both clients must still have the actor. That is the exact regression the dormancy rule prevents, and it is invisible in single player.| Command / API | Purpose |
|---|---|
Crimson.ObjectPool.Bypass 1 | Turns pooling off everywhere, NPCs included. If a bug survives this, recycling is not the cause. |
UCrimsonEnemySpawnerSubsystem::ClearSpawnedEnemies | Destroys every recycled NPC and clears the queue. Use when streaming out a large area. |
UCrimsonEnemySpawnerSubsystem::RecycleEnemy | Releases an NPC back to the pool by hand. Normally called for you from OnDeathFinished. |
See also
- CrimsonObjectPool -> Concept: The Reuse Contract
- CrimsonObjectPool -> How-To: Pool a Replicated Actor
- CrimsonObjectPool -> How-To: Size and Prewarm a Pool
- Character Pipeline