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.

Prerequisites
The CrimsonObjectPool plugin enabled. A UCrimsonNPCData asset, an ACrimsonEnemySpawner placed in the level, and an ACrimsonNPCBase subclass to spawn. See CrimsonObjectPool -> Overview for the pool itself.
One checkbox
Pooling is opt-in per data asset. Tick 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.

StepWhoWhat happens
RequestACrimsonEnemySpawnerRequestSpawnEnemy(Class, NPCData, Transform, Spawner) enqueues; nothing spawns yet.
ServeUCrimsonEnemySpawnerSubsystemOne request per queue pass. If bCanBePooled, it calls AcquireActor; otherwise it spawns outright.
ActivateACrimsonNPCBaseThe pool calls OnAcquiredFromPool, which runs ActivateFromPool - unhide, restore collision, re-init the ASC, spawn a controller.
ConfigureUCrimsonEnemySpawnerSubsystemInitializeFromData grants ability sets, then SetSpawnerOrigin, then ResetHealthForPoolActivation for a recycled instance.
RecycleACrimsonNPCBaseOn death, OnDeathFinished calls RecycleEnemy, which releases the NPC back to the pool instead of destroying it.
An NPC without bCanBePooled still works
It is spawned outright and destroyed on death, exactly as before. The flag only decides whether the pool is involved.

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.

Never move a pooled NPC to a graveyard location
Parking pooled actors at something like (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 runs once per actor lifetime, not once per spawn
This is the single most common pooling bug. Any per-use state initialised in 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

text
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.

Verify
Kill and respawn the same enemy type twenty times, then dump. 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 / APIPurpose
Crimson.ObjectPool.Bypass 1Turns pooling off everywhere, NPCs included. If a bug survives this, recycling is not the cause.
UCrimsonEnemySpawnerSubsystem::ClearSpawnedEnemiesDestroys every recycled NPC and clears the queue. Use when streaming out a large area.
UCrimsonEnemySpawnerSubsystem::RecycleEnemyReleases 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