How to Object Pool planck.js Bodies?

Object pooling in planck.js—a 2D JavaScript physics engine based on Box2D—is a crucial optimization technique for maintaining a stable frame rate in physics-heavy games or simulations. Constantly creating and destroying rigid bodies triggers frequent garbage collection spikes, leading to noticeable frame drops. By reusing a fixed set of bodies instead of destroying them, you can keep memory allocation flat. This article covers the essential steps to implement an efficient object pool for planck.js bodies, including spawning, recycling, and properly resetting a body’s state.

Why Pool Physics Bodies?

In standard JavaScript development, creating a new object is relatively cheap. However, in a physics engine like planck.js, a World.createBody() call does heavy lifting behind the scenes. It allocates memory for the body, its transform states, velocity vectors, and attached fixtures.

If you are building a game like a bullet hell or an endless runner where hundreds of projectiles or obstacles appear and disappear every second, standard destruction will quickly bottleneck your application.

The Garbage Collection Trap: When you call world.destroyBody(body), you don’t instantly free memory; you just remove the reference. The JavaScript Garbage Collector (GC) will eventually run to clean up those objects, causing micro-stutters during gameplay.

Step 1: Initializing the Object Pool

An object pool is essentially an array or a list that holds pre-allocated objects. When you initialize your game scene, you should create the maximum number of bodies you expect to need at any given time.

class BodyPool {
  constructor(world, poolSize) {
    this.world = world;
    this.inactiveBodies = [];

    // Pre-allocate bodies
    for (let i = 0; i < poolSize; i++) {
      // Create a generic dynamic body at world origin
      const body = this.world.createBody({
        type: 'dynamic',
        position: pl.Vec2(0, 0)
      });
      
      // Attach a default fixture (e.g., a small circle)
      body.createFixture(pl.Circle(0.5), {
        density: 1.0,
        friction: 0.3
      });

      // Deactivate it immediately so it doesn't interact with the world
      body.setActive(false);
      
      this.inactiveBodies.push(body);
    }
  }
}

Step 2: Spawning and Activating a Body

When you need a new object in your game, instead of calling world.createBody(), you request one from your pool. You must activate the body and set its new position and velocity before it rejoins the physics simulation.

get(position, linearVelocity) {
  if (this.inactiveBodies.length === 0) {
    // Optional: Expand the pool dynamically if it runs out
    return null; 
  }

  const body = this.inactiveBodies.pop();
  
  // Teleport the body to the starting position
  body.setPosition(position);
  body.setLinearVelocity(linearVelocity);
  body.setAngularVelocity(0);
  
  // Wake up the body and make it active
  body.setActive(true);
  body.setAwake(true);

  return body;
}

Step 3: Properly Resetting and Recycling Bodies

The most critical part of object pooling in planck.js is cleaning the slate when a body goes off-screen or gets destroyed. If you don’t completely reset the physical properties, the reused body will carry over residual forces, velocities, and contact states, causing unpredictable physics glitches.

To safely recycle a body back into the pool, use the following checklist:

recycle(body) {
  // Deactivate physics interactions
  body.setActive(false);
  
  // Wipe out physical momentum
  body.setLinearVelocity(pl.Vec2(0, 0));
  body.setAngularVelocity(0);
  
  // Clear any pending force accumulations
  body.setForce(pl.Vec2(0, 0));
  body.setTorque(0);

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

Handling Contacts and Collisions During Pooling

When you deactivate a body using body.setActive(false), planck.js automatically destroys any active contacts touching that body. However, if you are caching data inside the user data property (body.setUserData()), make sure to clear or update that reference when recycling. If your game relies on contact listeners, always check body.isActive() within your beginContact loops to ensure you are not processing logic for a body that was just returned to the pool during the same physics step.