Object Pool

Reuse GameObject instances instead of Instantiate and Destroy to keep frame times stable.

Get/Release instead of Instantiate/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. Get pops an inactive instance (or creates one), activates it, and hands it over. Release deactivates it and puts it back.
  • Lifecycle hooks. Components implementing IPoolable get OnSpawned() and OnDespawned() callbacks — the pooled equivalent of Awake/OnDestroy for 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)

OptionDefaultWhat it does
InitialCapacity0Instances to pre-create at registration.
MaxSizeint.MaxValueCap on inactive instances. Instances released beyond the cap are destroyed instead of kept.
OnExhaustedGrowAndDestroyWhat Get does when no inactive instance is available (see below).
CallIPoolabletrueWhether to invoke the OnSpawned() / OnDespawned() hooks.

Exhaustion policies (PoolExhaustionPolicy)

PolicyBehavior when the pool is empty
GrowAndDestroyGet always creates a new instance (never returns null). On release, instances beyond MaxSize are destroyed. The safe default.
GrowAndReuseGet always creates, and release always keeps — the pool grows without bound. An explicit opt-in to memory growth.
ReturnNullOnce 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 InvalidOperationException in Debug builds.

  • Release via the pool, never Destroy. Calling UnityEngine.Object.Destroy(instance) on a pooled instance breaks the pool's active counter. Always use Release(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 tune InitialCapacity, MaxSize, and OnExhausted — register up front.

  • The one-argument Get keeps the old transform. Get(prefab) leaves the instance wherever it was when it was released. Use Get(prefab, position, rotation) when the spawn position matters.

  • Hook exceptions are contained. If your OnSpawned() or OnDespawned() 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.xml entry so they survive IL2CPP builds. The core types (IObjectPoolService, GameObjectPool, NullObjectPool) are already preserved by the framework.

  • Bootstrap — how services start automatically
  • Service Locator — resolving and replacing services
  • Audio — uses the same pooling idea internally for effect sources