Skip to main content

Command Palette

Search for a command to run...

Object Pool. Stop Allocating. Start Reusing.

Published
5 min read
Object Pool. Stop Allocating. Start Reusing.

Introduction

Allocating and freeing memory is slow. On consoles and mobile devices it is even worse because it fragments the heap. Over time your free memory turns into a scattered mess of tiny gaps that cannot fit anything useful. Your game crashes. Not because you ran out of memory, but because the free memory is in the wrong shape.

Object pools fix this. Allocate everything up front. Reuse it forever. No fragmentation. No allocation cost during gameplay.

How It Works.

At startup, create a fixed array of objects. All of them sit in memory, initialized to "not in use." When you need a new object, grab one from the pool and mark it as active. When you are done with it, mark it inactive. It goes back into the pool, ready to be reused.

No malloc. No free. No garbage collector. Just flipping a flag.

class Particle {
  framesLeft = 0;
  x = 0;
  y = 0;
  xVel = 0;
  yVel = 0;

  inUse() {
    return this.framesLeft > 0;
  }

  init(x: number, y: number, xVel: number, yVel: number, lifetime: number) {
    this.x = x;
    this.y = y;
    this.xVel = xVel;
    this.yVel = yVel;
    this.framesLeft = lifetime;
  }

  update() {
    if (!this.inUse()) return;
    this.framesLeft--;
    this.x += this.xVel;
    this.y += this.yVel;
  }
}

class ParticlePool {
  private particles = Array.from({ length: 1000 }, () => new Particle());

  create(x: number, y: number, xVel: number, yVel: number, lifetime: number) {
    for (const p of this.particles) {
      if (!p.inUse()) {
        p.init(x, y, xVel, yVel, lifetime);
        return;
      }
    }
    // Pool full. No particle created. Player won't notice.
  }

  update() {
    for (const p of this.particles) {
      p.update();
    }
  }
}

All 1000 particles exist from the start. Creating one is just finding an inactive slot and writing values into it. No memory allocation happens at runtime.

The Problem With Linear Search.

The code above scans the entire array to find a free slot. If the pool is large and mostly full, that scan gets slow.

Fix. Use a free list. When a particle is inactive, its memory is not doing anything useful. Reuse that memory to store a pointer to the next free particle. This chains all free particles into a linked list, built directly inside the pool's own memory.

class ParticlePool {
  private particles = Array.from({ length: 1000 }, () => new Particle());
  private firstAvailable = 0;

  constructor() {
    // Chain every particle to the next one.
    for (let i = 0; i < 999; i++) {
      this.particles[i].nextFree = i + 1;
    }
    this.particles[999].nextFree = -1; // end of list
  }

  create(x: number, y: number, xVel: number, yVel: number, lifetime: number) {
    if (this.firstAvailable === -1) return; // pool full

    const p = this.particles[this.firstAvailable];
    this.firstAvailable = p.nextFree;
    p.init(x, y, xVel, yVel, lifetime);
  }

  free(index: number) {
    this.particles[index].framesLeft = 0;
    this.particles[index].nextFree = this.firstAvailable;
    this.firstAvailable = index;
  }
}

Now creating and freeing are both O(1). No scanning. Just follow one pointer and you have your slot.

What Happens When The Pool Is Full.

Four options.

Make sure it never happens. Tune the pool size so it is always big enough. Best for critical objects like enemies or bosses where failing to create one would break the game.

Do nothing. Skip the creation. Works for particles and cosmetic effects. The screen is already full of stuff. One less sparkle is invisible.

Kill the least important active object. Good for sound effects. If all sound channels are in use, cut the quietest one. The new sound will mask the loss.

Grow the pool. Allocate more slots at runtime. Only works if your platform can afford the memory hit. Consider shrinking back later when demand drops.

Watch Out For Stale State.

Pooled objects are not freshly allocated. They still hold data from their previous life. If your init function misses a field, you get ghost state from a dead object leaking into a live one. These bugs are hard to find because the stale data often looks almost correct.

Always fully initialize every field when reusing an object. In debug builds, consider zeroing out the memory when an object is freed so stale reads are obvious.

When To Use It.

You create and destroy objects frequently during gameplay. Particles, bullets, sound effects, enemies.

Objects are the same size or close to it. Pools work best when every slot is the same size. If objects vary wildly, you waste memory padding small objects to fit the largest slot.

You are on a platform where fragmentation or allocation speed matters. Consoles, mobile, embedded. On a PC with a modern allocator and garbage collector, pools are less critical but still useful for hot paths.

One Sentence Summary.

Allocate once at startup, reuse forever, never ask the memory manager for anything during gameplay.