Serialize Matter.js Bodies for WebSockets

Transmitting real-time physics data from a Matter.js server to connected clients requires stripping out circular engine references and minimizing network payload size. This article explains how to isolate critical dynamic properties—specifically position, angle, linear velocity, and angular velocity—and serialize them efficiently using lean JSON structures or high-performance binary buffers for WebSocket streaming.

The Challenge with Matter.js Objects

Attempting to run JSON.stringify() directly on a Matter.js Body instance will throw a TypeError: Converting circular structure to JSON. A Matter.js body contains circular references through properties like body.parent and body.parts, alongside extensive static metadata such as collision filters, axes, and vertex arrays that rarely change during a simulation. For efficient network synchronization, you only need to transmit the dynamic properties that update on every physics tick.

Extracting Dynamic State

The absolute minimum state required to synchronize a rigid body across the network consists of its unique identifier, 2D coordinates, orientation, and motion vectors:

Method 1: Lightweight JSON Serialization

For rapid prototyping or low body counts, extract properties into flat objects or index-based arrays to minimize JSON overhead.

// Server: Extract minimal dynamic state
function serializeBodiesJSON(bodies) {
  return JSON.stringify(
    bodies.map(body => [
      body.id,
      Math.round(body.position.x * 100) / 100,
      Math.round(body.position.y * 100) / 100,
      Math.round(body.angle * 1000) / 1000,
      Math.round(body.velocity.x * 100) / 100,
      Math.round(body.velocity.y * 100) / 100,
      Math.round(body.angularVelocity * 1000) / 1000
    ])
  );
}

// Send over WebSocket
ws.send(serializeBodiesJSON(engine.world.bodies));

Using flat arrays instead of key-value pairs reduces the transmitted byte size significantly by eliminating repeated string keys across frames.

When broadcasting at 30 to 60 Hz to multiple clients, text-based JSON introduces garbage collection pauses and high bandwidth usage. Packing properties into an ArrayBuffer using typed arrays provides the best performance.

Each body can be represented by 7 numerical fields using 32-bit floats (4 bytes each), requiring only 28 bytes per body:

// Server: Pack bodies into a Float32Array binary buffer
function serializeBodiesBinary(bodies) {
  const fieldsPerBody = 7;
  const buffer = new Float32Array(bodies.length * fieldsPerBody);

  for (let i = 0; i < bodies.length; i++) {
    const body = bodies[i];
    const offset = i * fieldsPerBody;

    buffer[offset] = body.id;
    buffer[offset + 1] = body.position.x;
    buffer[offset + 2] = body.position.y;
    buffer[offset + 3] = body.angle;
    buffer[offset + 4] = body.velocity.x;
    buffer[offset + 5] = body.velocity.y;
    buffer[offset + 6] = body.angularVelocity;
  }

  return buffer.buffer; // Send raw ArrayBuffer
}

// Send binary buffer directly via WebSocket
ws.send(serializeBodiesBinary(engine.world.bodies));

Client-Side Deserialization and Application

Ensure the client socket is configured to receive binary data by setting ws.binaryType = 'arraybuffer'.

const ws = new WebSocket('ws://localhost:8080');
ws.binaryType = 'arraybuffer';

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const data = new Float32Array(event.data);
    const fieldsPerBody = 7;

    for (let i = 0; i < data.length; i += fieldsPerBody) {
      const id = data[i];
      const posX = data[i + 1];
      const posY = data[i + 2];
      const angle = data[i + 3];
      const velX = data[i + 4];
      const velY = data[i + 5];
      const angVel = data[i + 6];

      const clientBody = bodyMap.get(id);
      if (clientBody) {
        // Direct assignment bypassing internal Matter.js integration steps
        Matter.Body.setPosition(clientBody, { x: posX, y: posY });
        Matter.Body.setAngle(clientBody, angle);
        Matter.Body.setVelocity(clientBody, { x: velX, y: velY });
        Matter.Body.setAngularVelocity(clientBody, angVel);
      }
    }
  }
};

Applying both position and velocity ensures that rendering interpolation, client-side prediction, and local continuous collision detection remain accurate between network updates.