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:
- The Inactive Storage: A stack or array holding bodies ready for reuse.
- 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.
- 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:
- Forces and Torques: Setting velocity is not enough;
explicit assignment to
body.force = { x: 0, y: 0 }andbody.torque = 0prevents leftover impulse forces from applying in the subsequent physics step. - Sleeping State: Use
Matter.Sleeping.set(body, false)when acquiring a body. If a body entered a sleeping state before being pooled, it will not simulate motion until disturbed unless explicitly awakened. - Composite Membership: Always verify that a body is
actually in
this.worldbefore callingComposite.remove()inside the release method to avoid reference errors if a body is double-released.