Object Pool
Reuse GameObject instances instead of Instantiate and Destroy to keep frame times stable.
Get/Releaseinstead ofInstantiate/Destroy· Prewarm at startup, reuse forever · No-op replacement:NullObjectPool
CGS removes the cost of frequently spawning and destroying objects through IObjectPoolService. Instead of calling Instantiate(prefab) and Destroy(go) over and over, you call Get(prefab) to receive a ready instance and Release(instance) to hand it back. The expensive work — allocation, Awake calls, GameObject registration — moves out of your hot path and into a one-time prewarm step (pre-creating instances at startup). The result: spawning 30 enemies at once, detonating dozens of effects, or firing rapid projectiles no longer causes frame hitches.
What it does
- Per-prefab pools. Register a prefab once with tuning options; the service keeps a stack of inactive instances for it.
- Get and release instead of create and destroy.
Getpops an inactive instance (or creates one), activates it, and hands it over.Releasedeactivates it and puts it back. - Lifecycle hooks. Components implementing
IPoolablegetOnSpawned()andOnDespawned()callbacks — the pooled equivalent ofAwake/OnDestroyfor resetting state between uses. - Prewarming. Pre-create N instances during a loading screen so the first busy combat frame never pays an allocation cost.
- Diagnostics. Active, inactive, and total counts per prefab at any time.
Getting the service
Resolve once and cache it — do not call Resolve in Update:
using CommonGameSystem.Core;
var pool = ServiceLocator.Resolve<IObjectPoolService>();
If you resolve inside a MonoBehaviour's Awake or Start, add [DefaultExecutionOrder(100)] to the class. The pool is one of the 23 services the automatic bootstrapper registers before any scene Awake runs, so it is ready when you resolve it.
API reference
Registration
void RegisterPrefab(GameObject prefab, PoolOptions options) // Create a pool for the prefab
bool IsRegistered(GameObject prefab) // Does this prefab have a pool?
void UnregisterPrefab(GameObject prefab) // Destroy the pool and all its instances
Registering the same prefab twice throws ArgumentException — register once at startup.
Get / Release
GameObject Get(GameObject prefab)
GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
bool Release(GameObject instance)
Get pops an inactive instance (or creates one, depending on the exhaustion policy), activates it, and calls OnSpawned() if the instance implements IPoolable. The three-argument overload sets the transform before activation. Release deactivates the instance, calls OnDespawned() if present, and returns it to the pool — it returns false if the instance is not owned by any pool.
Prewarm
void Prewarm(GameObject prefab, int count) // Pre-create instances to kill first-use hiccups
Diagnostics and cleanup
int CountActive(GameObject prefab) // Instances currently in use
int CountInactive(GameObject prefab) // Instances resting in the pool
int CountTotal(GameObject prefab) // Active + inactive
void Clear(GameObject prefab) // Destroy all inactive instances (registration stays)
void ClearAll() // Clear every pool
Tuning (PoolOptions)
| Option | Default | What it does |
|---|---|---|
InitialCapacity | 0 | Instances to pre-create at registration. |
MaxSize | int.MaxValue | Cap on inactive instances. Instances released beyond the cap are destroyed instead of kept. |
OnExhausted | GrowAndDestroy | What Get does when no inactive instance is available (see below). |
CallIPoolable | true | Whether to invoke the OnSpawned() / OnDespawned() hooks. |
Exhaustion policies (PoolExhaustionPolicy)
| Policy | Behavior when the pool is empty |
|---|---|
GrowAndDestroy | Get always creates a new instance (never returns null). On release, instances beyond MaxSize are destroyed. The safe default. |
GrowAndReuse | Get always creates, and release always keeps — the pool grows without bound. An explicit opt-in to memory growth. |
ReturnNull | Once the active count reaches MaxSize, Get returns null and your call site decides what to do (skip the effect, for example). |
Lifecycle hooks (IPoolable)
Implement on any component of the pooled prefab:
void OnSpawned() // Called right after Get activates the instance — reset state here
void OnDespawned() // Called right before Release deactivates it — clean up here
Pooled instances skip Awake after the first use, so OnSpawned is where per-use initialization belongs.
Full example
A bullet system: the spawner registers and fires; the bullet moves, counts down its lifetime, and releases itself.
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private GameObject _bulletPrefab;
private IObjectPoolService _pool;
private void Awake()
{
_pool = ServiceLocator.Resolve<IObjectPoolService>();
_pool.RegisterPrefab(_bulletPrefab, new PoolOptions
{
InitialCapacity = 50, // Pre-create 50 bullets at startup
MaxSize = 100, // Keep up to 100 inactive
OnExhausted = PoolExhaustionPolicy.GrowAndDestroy
});
}
public void Fire(Vector3 position, Quaternion rotation)
{
// With GrowAndDestroy this never returns null.
_pool.Get(_bulletPrefab, position, rotation);
}
}
// On the bullet prefab:
[DefaultExecutionOrder(100)]
public class Bullet : MonoBehaviour, IPoolable
{
[SerializeField] private float _speed = 20f;
[SerializeField] private float _maxLifetime = 3f;
private IObjectPoolService _pool;
private float _lifetime;
private void Awake()
{
_pool = ServiceLocator.Resolve<IObjectPoolService>();
}
public void OnSpawned()
{
// Reset per-use state — pooled instances do not re-run Awake.
_lifetime = _maxLifetime;
}
public void OnDespawned()
{
// Optional cleanup before going back to the pool.
}
private void Update()
{
transform.Translate(Vector3.forward * _speed * Time.deltaTime);
_lifetime -= Time.deltaTime;
if (_lifetime <= 0f)
{
_pool.Release(gameObject); // Back to the pool — never Destroy()
}
}
}
Turning it off
To disable pooling (for debugging, or on platforms with plenty of memory), swap in the null implementation:
ServiceLocator.Replace<IObjectPoolService>(new NullObjectPool());
NullObjectPool serves instances straight through Instantiate / Destroy. Your game code sees no difference — you just lose the performance benefit. Call sites need no null checks and no changes.
Common pitfalls
-
Main thread only. All pool methods must be called from Unity's main thread. Calls from worker threads throw
InvalidOperationExceptionin Debug builds. -
Release via the pool, never
Destroy. CallingUnityEngine.Object.Destroy(instance)on a pooled instance breaks the pool's active counter. Always useRelease(instance). -
Register prefabs explicitly. If you call
Get(prefab)on an unregistered prefab, the pool auto-registers it with default options and logs a one-time warning. It works, but you lose the chance to tuneInitialCapacity,MaxSize, andOnExhausted— register up front. -
The one-argument
Getkeeps the old transform.Get(prefab)leaves the instance wherever it was when it was released. UseGet(prefab, position, rotation)when the spawn position matters. -
Hook exceptions are contained. If your
OnSpawned()orOnDespawned()throws, the pool catches the exception, logs an error, and continues — the instance is still activated or deactivated normally. Your bug will not break the pool. -
IL2CPP stripping. If you extend the pool with your own config or event types, add a
link.xmlentry so they survive IL2CPP builds. The core types (IObjectPoolService,GameObjectPool,NullObjectPool) are already preserved by the framework.
Related pages
- Bootstrap — how services start automatically
- Service Locator — resolving and replacing services
- Audio — uses the same pooling idea internally for effect sources