Skip to content

Pools

Reuse entities instead of creating and destroying them. Creating an entity costs an entity, its components, a physics body and a C# object — doing that twenty times a second for bullets is sustained churn in three allocators at once, and on a phone you feel it.

A pool pays that cost once, up front, then hands the same entities out forever. See Performance for the wider picture.

Setting one up

  1. Put one instance in your scene, configured exactly how spawned ones should look.
  2. Name it. The pool's name is the template entity's name.
  3. Create the pool from a script.
csharp
public class Gun : GameEntity
{
    private Pool m_Bullets;

    protected override void OnCreate()
    {
        m_Bullets = Pools.Create("Bullet", prewarm: 64, growth: PoolGrowth.Double, max: 512);
    }

    private void Fire(Vector3 muzzle, Vector2 direction)
    {
        Bullet? bullet = m_Bullets.Get<Bullet>();
        if (bullet == null)
            return;                       // at the cap

        bullet.Translation = muzzle;
        bullet.Launch(direction);
    }
}

The engine disables your template for you and never hands the template itself out, so you won't get a stray bullet parked at the origin.

Pools.Create(string name, int prewarm = 16, PoolGrowth growth = PoolGrowth.Double, int max = 256)

Creates the pool, or returns the one already created under that name.

ParameterWhat it does
nameThe template entity's name in the current scene
prewarmHow many instances to build immediately — the entire point. Pay for 64 bullets during a load screen instead of during a firefight
growthWhat happens when it runs dry
maxHard cap on total instances

Check IsValid on the result: creation fails, and logs why, if no scene is running or no entity of that name exists.

Pools.Find(string name)

A handle to a pool created earlier — by another script, or in an earlier frame. IsValid is false if no such pool exists.

PoolGrowth

ValueBehaviour
FixedNever grows. Get() returns null once everything is in use — right when running out is a design rule ("only three pickups on screen at once"), not an accident
LinearAdds its prewarm count again each time it runs dry
DoubleDoubles. Reaches a high-water mark in a handful of steps instead of dozens, at the cost of overshooting. The right default for bullets

Every pool has a hard max, deliberately: an unbounded pool under a runaway spawn bug is an out-of-memory kill on a phone, while a capped one is a visible stutter and one log line naming the pool.

Pool

A struct holding only the pool's name — passing one around, storing it in a field or returning it from a method allocates nothing.

Get()

Takes an entity from the pool, or null if it's at its cap.

Get<T>()

The same, already cast to the pooled entity's own script type. null if the pool was at its cap or if what came out has no script of type T.

csharp
Bullet? b = m_Bullets.Get<Bullet>();   // instead of m_Bullets.Get()?.As<Bullet>()

Release(GameEntity entity)

Returns an entity to the pool: fires OnDespawn on its script, then disables it. Harmless on an entity that isn't pooled or was already released — both happen in real games where two systems each decide a bullet is finished.

ReleaseAll()

Returns every currently-spawned entity. The "the wave ended, clean up" call, instead of tracking each one you lost sight of.

Statistics

Free, InUse, Capacity, Max, AtCap and IsValid are readable at any time.

csharp
if (m_Bullets.AtCap)
    Log.Warn($"bullet pool exhausted: {m_Bullets.InUse}/{m_Bullets.Max}");

Name

The pool's name — which is also its template entity's name.

The one thing that will bite you

Your state is not reset for you

A pooled entity keeps everything from its previous life: velocity, animation frame, timers, and every field on your script. A recycled bullet that arrives already moving is not an engine bug — it is an empty OnDespawn.

Pooled entities are deactivated, not destroyed, so OnCreate and OnDestroy fire once each for the whole life of the instance. The hooks that fire on every reuse are OnSpawn and OnDespawn.

csharp
public class Bullet : GameEntity
{
    private float m_Lifetime;

    protected override void OnSpawn()
    {
        m_Lifetime = 3.0f;
    }

    protected override void OnDespawn()
    {
        // Clear anything that would carry into the next life.
        GetComponent<Rigidbody2DComponent>()?.LinearVelocity = Vector2.Zero;
    }
}

OnDespawn runs before the entity is disabled, so physics and audio are still live there — zeroing a velocity works.

Growth under a frame hitch

A slow frame can grow your pool

If you spawn at a rate — credit += rate * ts — one slow frame emits a burst, and nothing can be released yet because none of those objects have reached the end of their lifetime. In-flight briefly spikes and the pool grows.

Either prewarm above your worst-case burst, or clamp how many you allow in a single frame:

csharp
int spawnsThisFrame = 0;
while (m_Credit >= 1.0f && spawnsThisFrame < 16)
{
    m_Credit -= 1.0f;
    spawnsThisFrame++;
    // ... spawn ...
}

Pools and scenes

Pools are cleared when their scene unloads. To keep one across a scene change, call Persist() on the template entity before creating the pool — the pool lives wherever its template lives.

Pooled entities freeze with everything else when Time.TimeScale is 0.

See also