Probably Playable
Back to Codons

Object Pool

Stable

Stop creating and destroying objects every frame — reuse them

by Probably Playable — v1.0.0

Description

The problem

Your AI creates new Bullet() every time the player shoots and lets the garbage collector clean up. 200 bullets per second = GC stutters every few seconds. The AI doesn't think about memory because it solves one feature at a time.

The solution

A generic pool that recycles objects instead of creating new ones. Give it a factory function, and it handles the rest.

  • acquire() — get an object (reused or new)
  • release(obj) — return to pool
  • Prewarm — pre-create objects at init to avoid first-frame spikes

40 lines. Generic over any type. Zero dependencies.

Dependencies

None — zero external dependencies.

Phaser Three.js Vanilla JS Any Framework
Technical Details
Version
1.0.0
Status
Stable
License
MIT
Size
1 KB
Author
Probably Playable
Updated
2025-04-04
poolmemoryGCperformanceparticlesbulletsoptimization
AI Integration Skill

Drop into .claude/skills/ — your AI handles the rest.

AI Skill codon.skill.md

ObjectPool — Integration Skill

Use this skill when the user asks to "object pool", "reduce garbage collection",

"reuse objects", "particle pool", "bullet pool", or "avoid GC spikes".

WHAT IT DOES

A generic object pool that pre-allocates and recycles objects to eliminate garbage collection pressure. Instead of creating and destroying objects every frame (bullets, particles, enemies, damage numbers), you acquire them from the pool and release them back when done. The pool grows on demand if empty and supports optional pre-warming. This is essential for any game with frequently spawned/destroyed objects — GC pauses cause visible frame drops.

REQUIREMENTS

  • Framework: Any (Phaser, Three.js, vanilla JS, Node.js)
  • Language: TypeScript or JavaScript
  • Dependencies: None
  • Files: 1 TypeScript file (~40 LOC)

INSTALL

  1. Copy the ObjectPool class into your project (e.g., src/utils/ObjectPool.ts).
    1. Import wherever needed:
    2. `typescript

      import { ObjectPool } from './utils/ObjectPool';

      `

      1. Create a pool with a factory function:
      2. `typescript

        const bulletPool = new ObjectPool(() => new Bullet(), 100);

        `

        INTEGRATION

        Core Implementation

        
        class ObjectPool<T> {
          private pool: T[] = [];
          private factory: () => T;
        
          /**
           * @param factory - Function that creates a new instance of T
           * @param prewarmCount - Number of objects to pre-allocate (optional)
           */
          constructor(factory: () => T, prewarmCount: number = 0) {
            this.factory = factory;
            for (let i = 0; i < prewarmCount; i++) {
              this.pool.push(factory());
            }
          }
        
          /** Get an object from the pool (creates one if pool is empty) */
          acquire(): T {
            return this.pool.length > 0 ? this.pool.pop()! : this.factory();
          }
        
          /** Return an object to the pool for reuse */
          release(obj: T): void {
            this.pool.push(obj);
          }
        
          /** Number of available (inactive) objects in the pool */
          get size(): number {
            return this.pool.length;
          }
        
          /** Clear the pool, optionally calling a destructor on each object */
          clear(destructor?: (obj: T) => void): void {
            if (destructor) {
              for (const obj of this.pool) {
                destructor(obj);
              }
            }
            this.pool.length = 0;
          }
        }
        

        Bullet Pool Example

        
        class Bullet {
          x = 0;
          y = 0;
          vx = 0;
          vy = 0;
          active = false;
        
          reset(x: number, y: number, vx: number, vy: number): void {
            this.x = x;
            this.y = y;
            this.vx = vx;
            this.vy = vy;
            this.active = true;
          }
        }
        
        const bulletPool = new ObjectPool(() => new Bullet(), 200);
        const activeBullets: Bullet[] = [];
        
        function fireBullet(x: number, y: number, angle: number, speed: number) {
          const bullet = bulletPool.acquire();
          bullet.reset(x, y, Math.cos(angle) * speed, Math.sin(angle) * speed);
          activeBullets.push(bullet);
        }
        
        function updateBullets() {
          for (let i = activeBullets.length - 1; i >= 0; i--) {
            const b = activeBullets[i];
            b.x += b.vx;
            b.y += b.vy;
        
            if (b.x < 0 || b.x > WORLD_W || b.y < 0 || b.y > WORLD_H) {
              b.active = false;
              activeBullets.splice(i, 1);
              bulletPool.release(b);
            }
          }
        }
        

        Particle Pool Example

        
        interface Particle {
          x: number;
          y: number;
          vx: number;
          vy: number;
          life: number;
          maxLife: number;
          color: string;
        }
        
        const particlePool = new ObjectPool<Particle>(() => ({
          x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, color: '#fff',
        }), 500);
        
        function spawnExplosion(x: number, y: number, count: number) {
          for (let i = 0; i < count; i++) {
            const p = particlePool.acquire();
            const angle = Math.random() * Math.PI * 2;
            const speed = 1 + Math.random() * 3;
            p.x = x;
            p.y = y;
            p.vx = Math.cos(angle) * speed;
            p.vy = Math.sin(angle) * speed;
            p.life = 30 + Math.random() * 30;
            p.maxLife = p.life;
            p.color = '#ff4400';
            activeParticles.push(p);
          }
        }
        

        CONFIGURATION

        OptionTypeDefaultDescription
        factory() => T*required*Function that creates a new instance
        prewarmCountnumber0Number of objects to pre-allocate at construction

        Choosing prewarmCount: Set it to your expected peak usage. For bullets, if you expect at most 200 on screen, prewarm 200. This avoids allocation spikes during gameplay. The pool grows beyond this if needed.

        API REFERENCE

        MethodSignatureReturnsDescription
        constructornew ObjectPool<T>(factory, prewarmCount?)ObjectPool<T>Create pool with factory and optional pre-warming
        acquire.acquire()TGet an object (from pool or newly created)
        release.release(obj: T)voidReturn an object to the pool
        size.sizenumberNumber of available objects in the pool (getter)
        clear.clear(destructor?)voidEmpty the pool, optionally calling destructor on each

        GOTCHAS

        • You must reset objects before or after acquiring. The pool does not clean objects — it returns them in whatever state they were released. Always reset properties (position, velocity, health, visibility) when acquiring.
        • Do not release an object twice. Releasing the same object twice means acquire() will return it twice, causing two systems to share the same instance. Track active/inactive state yourself.
        • Hide objects before releasing. If the object has a visual representation (sprite, mesh), make it invisible or move it off-screen before releasing. The pool does not handle rendering.
        • Pool only tracks inactive objects. The size property shows how many are available in the pool, not how many are active in the game. Track active objects separately if needed.
        • Do not hold references to released objects. Once released, the object may be acquired by something else at any time. Remove it from all active lists before releasing.

This codon is provided "as is" without warranty of any kind, express or implied. Probably Playable assumes no responsibility for any damages, data loss, security vulnerabilities, or defects arising from its use. You are solely responsible for reviewing, testing, and validating this code before integrating it into your project. Use at your own risk.