Object Spawner and Pooling

An entity spawning service that abstracts object pooling behind opaque handles so game logic never touches engine instantiation directly.

What the system is for

Every Unity project that spawns enemies, projectiles or hazards eventually writes the same pooling code. A queue of inactive GameObjects, a borrow-and-return pattern, scattered Instantiate and Destroy calls, and pool state that lives in a MonoBehaviour field nobody quite owns. It works until it does not.

Serenity's Game Spawner moves all of that behind a typed service interface. Game logic calls Spawn and Despawn against logical types. The pool, the prefab reference and the engine instantiation live in infrastructure where they belong.

The Unity problem

Instantiate and Destroy are engine calls. When game logic calls them directly it takes on a responsibility it should not have: knowing which prefab to use, deciding whether a pool exists, tracking active instances, and cleaning up on scene transitions. That coupling makes the spawning logic hard to test and easy to break when the pool implementation changes.

Object pooling libraries solve the performance problem but not the architecture problem. The pool is still a concrete engine object that game logic must reference. Changing the pool strategy — switching from a simple queue to a bucket-per-type implementation — still requires changes in callers.

How Serenity approaches it

Game logic interacts with IGameSpawnerService, which exposes Spawn, Despawn, DespawnAll and IsActive. Spawn takes a SpawnType value object and returns a SpawnHandle — an opaque struct that identifies the instance by a GameEntityId without exposing any engine type. The handle is the only thing game logic holds. Despawn takes the handle back and the pool decides what to do with it.

ISpawnFactory is the application port that infrastructure implements. It handles the engine-specific Create, Destroy and DestroyAll calls. Pooling is managed through PoolBucket entities in the domain, which group active and available handles per SpawnType. PoolStateSnapshot exposes active count, available count and total count for diagnostics without giving callers access to the pool internals.

How it fits into Serenity

Game Spawner lives in the Serenity.GameSpawner namespace and follows the foundation's layered structure. The Domain layer defines SpawnType, SpawnHandle, PoolStateSnapshot and the PoolBucket entity. The Application layer exposes IGameSpawnerService and the ISpawnFactory port. The Infrastructure layer provides UnityGameSpawnerService and UnitySpawnFactory, which handle prefab instantiation and the actual pooling queue. Installation wires everything through GameSpawnerInstaller and UnityGameSpawnerInstaller.

Game Spawner cooperates with the Wave and Stage systems for timed batch spawning, with the Character system so spawned characters receive their initialization context, with the Rail system for rail-bound spawn positioning, and with the Game Session system for session-scoped cleanup via DespawnAll.

Practical workflow

  1. Define a SpawnType for each logical entity category your project needs, such as enemies, projectiles or pickups.
  2. Register prefabs and pool sizes in the installer configuration so the Unity infrastructure knows what to create.
  3. Inject IGameSpawnerService into any application-layer class that needs to spawn or despawn entities.
  4. Call Spawn with a SpawnType to get a SpawnHandle; hold the handle to track the instance.
  5. Call Despawn with the handle when the entity is no longer needed; the pool handles the rest.
  6. Call GetPoolState with a SpawnType to read a PoolStateSnapshot for diagnostics or tuning.

What you get

  • IGameSpawnerService with Spawn, Despawn, DespawnAll and IsActive
  • SpawnHandle — opaque struct combining GameEntityId and SpawnType for safe instance tracking
  • SpawnType — immutable value object that identifies a logical entity category by string key
  • PoolBucket domain entity that manages active and available handles per type
  • PoolStateSnapshot value object with ActiveCount, AvailableCount and TotalCount for diagnostics
  • ISpawnFactory application port that keeps engine instantiation out of the business layer
  • UnitySpawnFactory and UnityGameSpawnerService as drop-in infrastructure implementations
  • DespawnAll overloads for targeted type cleanup and full session-wide cleanup

When to use this

  • Projects that spawn repeating entities such as enemies, bullets or effects and need controlled object reuse.
  • Games where game logic must remain decoupled from Unity prefab references and instantiation details.
  • Codebases that need traceable, handle-based entity references instead of scattered GameObject fields.
  • Projects that integrate with Wave, Stage or Game Session systems and need coordinated bulk despawning.

Related systems

Use Serenity when you want spawning and pooling that the game logic can call without knowing anything about GameObjects, prefabs or pool queues — and still get full diagnostic visibility through pool state snapshots.

Back to the home page