Object Pooling for Matter.js Projectiles

Implementing an object pool for projectile bodies in Matter.js improves physics simulation performance by recycling existing rigid bodies rather than continuously instantiating and destroying them. Frequent allocation and removal of bodies trigger garbage collection spikes and force the physics engine to repeatedly reindex its spatial broadphase structure. This guide provides a generic, reusable object pool pattern specifically tailored to managing Matter.js rigid bodies efficiently.

The Generic Object Pool Class

A generic object pool manages a collection of pre-allocated objects, providing methods to acquire an idle instance and release it back to the pool when it is no longer needed.

class ObjectPool {
  constructor(factory, resetFn, initialSize = 20) {
    this.factory = factory;
    this.resetFn = resetFn;
    this.pool = [];

    for (let i = 0; i < initialSize; i++) {
      this.pool.push(this.factory());
    }
  }

  acquire(...args) {
    const item = this.pool.length > 0 ? this.pool.pop() : this.factory();
    this.resetFn(item, ...args);
    return item;
  }

  release(item) {
    this.pool.push(item);
  }
}

Implementing Matter.js Projectile Recycling

When deactivating a Matter.js body, you must neutralize its physics properties so it does not interact with the active simulation. When reactivating it, reset its transform, velocities, and collision filters.

const { Bodies, Body, Composite } = Matter;

function createProjectilePool(world, initialSize = 50) {
  // Factory: create an inactive body off-screen
  const factory = () => {
    const body = Bodies.circle(-1000, -1000, 5, {
      isSensor: true,
      label: 'projectile',
      collisionFilter: { group: -1, mask: 0 } // No collision while inactive
    });

    Composite.add(world, body);
    return body;
  };

  // Reset: reactivate and reposition body for firing
  const resetFn = (body, x, y, velocity, collisionCategory = 0x0001, collisionMask = 0xFFFFFFFF) => {
    // Reset velocities and forces
    Body.setVelocity(body, { x: 0, y: 0 });
    Body.setAngularVelocity(body, 0);
    Body.setPosition(body, { x, y });
    Body.setAngle(body, 0);

    // Re-enable collisions
    body.isSensor = false;
    body.collisionFilter.category = collisionCategory;
    body.collisionFilter.mask = collisionMask;
    body.collisionFilter.group = 0;

    // Apply launch trajectory
    Body.setVelocity(body, velocity);
  };

  return new ObjectPool(factory, resetFn, initialSize);
}

Deactivating and Releasing Projectiles

When a projectile hits an obstacle or exceeds its lifetime, return it to the inactive state instead of calling Composite.remove().

function deactivateProjectile(pool, body) {
  // Move off-screen and disable interaction
  Body.setPosition(body, { x: -1000, y: -1000 });
  Body.setVelocity(body, { x: 0, y: 0 });
  Body.setAngularVelocity(body, 0);
  
  body.isSensor = true;
  body.collisionFilter.mask = 0;

  // Return to pool for reuse
  pool.release(body);
}

Handling Collisions and Lifespans

Track active projectiles during your simulation update loop to handle automatic despawns, and listen to collision events to recycle them on impact.

const projectilePool = createProjectilePool(engine.world, 30);
const activeProjectiles = new Set();

// Fire a projectile
function fire(x, y, targetX, targetY, speed = 15) {
  const angle = Math.atan2(targetY - y, targetX - x);
  const velocity = {
    x: Math.cos(angle) * speed,
    y: Math.sin(angle) * speed
  };

  const body = projectilePool.acquire(x, y, velocity);
  body.spawnTime = performance.now();
  activeProjectiles.add(body);
}

// Check lifetimes in your game loop
function updateProjectiles(currentTime, maxLifetimeMs = 3000) {
  for (const body of activeProjectiles) {
    if (currentTime - body.spawnTime > maxLifetimeMs) {
      activeProjectiles.delete(body);
      deactivateProjectile(projectilePool, body);
    }
  }
}

// Handle collision events
Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    [pair.bodyA, pair.bodyB].forEach((body) => {
      if (body.label === 'projectile' && activeProjectiles.has(body)) {
        activeProjectiles.delete(body);
        deactivateProjectile(projectilePool, body);
      }
    });
  });
});

Keeping bodies inside the Matter.js composite structure with disabled collision filters eliminates the overhead of dynamic memory allocation and tree balancing during high-frequency combat sequences.