How to Implement an Object Pool in Matter.js

This article explains how to implement an object pool for rigid bodies in the Matter.js 2D physics engine. Frequently allocating and destroying physics bodies introduces garbage collection overhead, leading to frame drops in dynamic games and simulations. By recycling inactive bodies rather than creating new instances, an object pool stabilizes frame rates and optimizes memory usage.

Why Object Pooling Matters in Matter.js

Every time you call Matter.Bodies.rectangle() or Matter.Bodies.circle(), Matter.js calculates bounds, axes, vertices, and mass properties, allocating a complex JavaScript object. Calling Matter.Composite.remove() flags the body for garbage collection. In scenarios such as projectile systems or particle effects, this allocation churn causes recurring micro-stutters. An object pool keeps pre-allocated bodies in memory, resetting their properties when spawned and setting them inactive when dismissed.

Implementation Architecture

A robust Matter.js body pool requires three main elements:

  1. The Inactive Storage: A stack or array holding bodies ready for reuse.
  2. Acquisition Logic: A method that pulls a body from the pool, resets its physical properties (position, velocity, angle), and registers it with the physics world.
  3. Release Logic: A method that neutralizes the body's momentum, disables its collision or removes it from the active composite, and returns it to storage.

Complete Implementation

Here is a complete, reusable class implementing an object pool for Matter.js:

import Matter from 'matter-js';

const { Body, Composite, Vector } = Matter;

class MatterBodyPool {
  /**
   * @param {Matter.World} world - The target Matter.js world instance.
   * @param {Function} createBodyFn - Factory function that returns a new Matter.Body.
   * @param {number} initialSize - Number of bodies to pre-allocate.
   */
  constructor(world, createBodyFn, initialSize = 20) {
    this.world = world;
    this.createBodyFn = createBodyFn;
    this.pool = [];

    // Pre-allocate bodies
    for (let i = 0; i < initialSize; i++) {
      const body = this.createBodyFn();
      this.pool.push(body);
    }
  }

  /**
   * Retrieves a body from the pool and adds it to the active world.
   * @param {number} x - Target X position.
   * @param {number} y - Target Y position.
   * @param {Object} [overrides={}] - Optional property overrides.
   * @returns {Matter.Body}
   */
  acquire(x, y, overrides = {}) {
    let body;

    if (this.pool.length > 0) {
      body = this.pool.pop();
    } else {
      // Expand pool dynamically if exhausted
      body = this.createBodyFn();
    }

    // Reset standard transform and motion vectors
    Body.setPosition(body, { x, y });
    Body.setAngle(body, 0);
    Body.setVelocity(body, { x: 0, y: 0 });
    Body.setAngularVelocity(body, 0);

    // Clear accumulated forces
    body.force = { x: 0, y: 0 };
    body.torque = 0;

    // Apply optional overrides (e.g., collisionFilter, isStatic)
    if (Object.keys(overrides).length > 0) {
      Body.set(body, overrides);
    }

    // Wake up body if it was sleeping
    Matter.Sleeping.set(body, false);

    // Add back to active simulation
    Composite.add(this.world, body);

    return body;
  }

  /**
   * Deactivates a body and returns it to the pool.
   * @param {Matter.Body} body
   */
  release(body) {
    // Remove from the physics simulation loop
    Composite.remove(this.world, body);

    // Reset velocity and force so it is inert while pooled
    Body.setVelocity(body, { x: 0, y: 0 });
    Body.setAngularVelocity(body, 0);
    body.force = { x: 0, y: 0 };
    body.torque = 0;

    // Return to pool array
    this.pool.push(body);
  }

  /**
   * Cleans up all pooled bodies to prevent memory leaks on scene teardown.
   */
  destroy() {
    this.pool.forEach((body) => {
      Composite.remove(this.world, body);
    });
    this.pool.length = 0;
  }
}

Usage Example

Below is how to initialize and use the pool within an application:

// Setup engine and world
const engine = Matter.Engine.create();
const world = engine.world;

// 1. Define factory function for specific body types
const createBullet = () => {
  return Matter.Bodies.circle(0, 0, 5, {
    restitution: 0.8,
    friction: 0.05,
    label: 'bullet'
  });
};

// 2. Initialize the pool with 50 pre-warmed bodies
const bulletPool = new MatterBodyPool(world, createBullet, 50);

// 3. Spawn a body on an event
function shootBullet(originX, originY, speedX, speedY) {
  const bullet = bulletPool.acquire(originX, originY);
  Matter.Body.setVelocity(bullet, { x: speedX, y: speedY });

  // Optional: Auto-release after lifespan expires
  setTimeout(() => {
    bulletPool.release(bullet);
  }, 2000);
}

// 4. Recycle on collision
Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    if (pair.bodyA.label === 'bullet') {
      bulletPool.release(pair.bodyA);
    }
    if (pair.bodyB.label === 'bullet') {
      bulletPool.release(pair.bodyB);
    }
  });
});

Essential State Resets for Recycled Bodies

Failing to fully reset a recycled body leads to physics glitches such as unexpected trajectory offsets or phantom collisions. Whenever acquiring or releasing bodies, ensure the following states are handled: