How to Build a Procedural Ragdoll in Matter.js

This guide demonstrates how to construct a procedural ragdoll composed of a head, torso, and limbs in the Matter.js 2D physics engine. By assembling primitive rigid bodies into a composite structure, linking them via distance constraints, managing collision groups to prevent self-intersection, and enforcing joint limits through custom angle clamping in the physics update loop, you can build a stable, dynamic ragdoll suitable for games and simulations.


1. Collision Filtering

To prevent the ragdoll's connected limbs from colliding with each other while still interacting with the environment, assign all ragdoll parts to the same negative collision group. Matter.js treats bodies sharing the same negative collisionFilter.group value as non-colliding.

const ragdollGroup = Matter.Body.nextGroup(true); // Generates a unique negative integer

2. Defining Bodies

Construct the individual segments using Matter.Bodies.rectangle and Matter.Bodies.circle. Scale coordinates procedurally relative to an (x, y) origin point.

const { Bodies, Body, Composite, Constraint } = Matter;

function createRagdoll(x, y, scale = 1) {
  const defaultFilter = { group: ragdollGroup };

  // Body Dimensions
  const headRadius = 20 * scale;
  const torsoWidth = 40 * scale;
  const torsoHeight = 70 * scale;
  const limbWidth = 14 * scale;
  const upperArmLength = 35 * scale;
  const lowerArmLength = 30 * scale;
  const upperLegLength = 40 * scale;
  const lowerLegLength = 35 * scale;

  // Create Segments
  const head = Bodies.circle(x, y - torsoHeight / 2 - headRadius, headRadius, {
    collisionFilter: defaultFilter,
    density: 0.001
  });

  const torso = Bodies.rectangle(x, y, torsoWidth, torsoHeight, {
    collisionFilter: defaultFilter,
    density: 0.002
  });

  // Arms
  const leftUpperArm = Bodies.rectangle(x - torsoWidth / 2 - limbWidth / 2, y - torsoHeight / 4, limbWidth, upperArmLength, { collisionFilter: defaultFilter });
  const leftLowerArm = Bodies.rectangle(x - torsoWidth / 2 - limbWidth / 2, y - torsoHeight / 4 + upperArmLength, limbWidth, lowerArmLength, { collisionFilter: defaultFilter });
  const rightUpperArm = Bodies.rectangle(x + torsoWidth / 2 + limbWidth / 2, y - torsoHeight / 4, limbWidth, upperArmLength, { collisionFilter: defaultFilter });
  const rightLowerArm = Bodies.rectangle(x + torsoWidth / 2 + limbWidth / 2, y - torsoHeight / 4 + upperArmLength, limbWidth, lowerArmLength, { collisionFilter: defaultFilter });

  // Legs
  const leftUpperLeg = Bodies.rectangle(x - torsoWidth / 4, y + torsoHeight / 2 + upperLegLength / 2, limbWidth, upperLegLength, { collisionFilter: defaultFilter });
  const leftLowerLeg = Bodies.rectangle(x - torsoWidth / 4, y + torsoHeight / 2 + upperLegLength + lowerLegLength / 2, limbWidth, lowerLegLength, { collisionFilter: defaultFilter });
  const rightUpperLeg = Bodies.rectangle(x + torsoWidth / 4, y + torsoHeight / 2 + upperLegLength / 2, limbWidth, upperLegLength, { collisionFilter: defaultFilter });
  const rightLowerLeg = Bodies.rectangle(x + torsoWidth / 4, y + torsoHeight / 2 + upperLegLength + lowerLegLength / 2, limbWidth, lowerLegLength, { collisionFilter: defaultFilter });

3. Connecting Parts with Constraints

Matter.js uses Constraint.create to create pin joints. Set length: 0 and stiffness: 0.9 to create pivots with local anchor points (pointA and pointB).

  // Helper to connect joints at specific local offsets
  function makeJoint(bodyA, bodyB, pointA, pointB) {
    return Constraint.create({
      bodyA,
      bodyB,
      pointA,
      pointB,
      stiffness: 0.9,
      length: 0,
      render: { visible: false }
    });
  }

  // Neck
  const neck = makeJoint(torso, head, { x: 0, y: -torsoHeight / 2 }, { x: 0, y: headRadius });

  // Shoulders
  const leftShoulder = makeJoint(torso, leftUpperArm, { x: -torsoWidth / 2, y: -torsoHeight / 3 }, { x: 0, y: -upperArmLength / 2 });
  const rightShoulder = makeJoint(torso, rightUpperArm, { x: torsoWidth / 2, y: -torsoHeight / 3 }, { x: 0, y: -upperArmLength / 2 });

  // Elbows
  const leftElbow = makeJoint(leftUpperArm, leftLowerArm, { x: 0, y: upperArmLength / 2 }, { x: 0, y: -lowerArmLength / 2 });
  const rightElbow = makeJoint(rightUpperArm, rightLowerArm, { x: 0, y: upperArmLength / 2 }, { x: 0, y: -lowerArmLength / 2 });

  // Hips
  const leftHip = makeJoint(torso, leftUpperLeg, { x: -torsoWidth / 4, y: torsoHeight / 2 }, { x: 0, y: -upperLegLength / 2 });
  const rightHip = makeJoint(torso, rightUpperLeg, { x: torsoWidth / 4, y: torsoHeight / 2 }, { x: 0, y: -upperLegLength / 2 });

  // Knees
  const leftKnee = makeJoint(leftUpperLeg, leftLowerLeg, { x: 0, y: upperLegLength / 2 }, { x: 0, y: -lowerLegLength / 2 });
  const rightKnee = makeJoint(rightUpperLeg, rightLowerLeg, { x: 0, y: upperLegLength / 2 }, { x: 0, y: -lowerLegLength / 2 });

4. Implementing Joint Limits

Matter.js does not provide native angular limit properties on constraints. To prevent limbs from rotating unnatural degrees (such as knees bending forward), enforce limits during the beforeUpdate engine event by clamping the relative angle between parent and child bodies.

  // Store joint limit definitions
  const jointLimits = [
    { parent: torso, child: head, min: -Math.PI / 6, max: Math.PI / 6 },
    { parent: torso, child: leftUpperArm, min: -Math.PI / 2, max: Math.PI / 1.5 },
    { parent: torso, child: rightUpperArm, min: -Math.PI / 1.5, max: Math.PI / 2 },
    { parent: leftUpperArm, child: leftLowerArm, min: -Math.PI / 1.2, max: 0 },
    { parent: rightUpperArm, child: rightLowerArm, min: 0, max: Math.PI / 1.2 },
    { parent: torso, child: leftUpperLeg, min: -Math.PI / 4, max: Math.PI / 2 },
    { parent: torso, child: rightUpperLeg, min: -Math.PI / 2, max: Math.PI / 4 },
    { parent: leftUpperLeg, child: leftLowerLeg, min: 0, max: Math.PI / 1.2 },
    { parent: rightUpperLeg, child: rightLowerLeg, min: 0, max: Math.PI / 1.2 }
  ];

  function enforceLimits() {
    for (let i = 0; i < jointLimits.length; i++) {
      const { parent, child, min, max } = jointLimits[i];
      let relAngle = child.angle - parent.angle;

      // Normalize angle to range [-PI, PI]
      relAngle = Math.atan2(Math.sin(relAngle), Math.cos(relAngle));

      if (relAngle < min) {
        Body.setAngle(child, parent.angle + min);
        Body.setAngularVelocity(child, parent.angularVelocity);
      } else if (relAngle > max) {
        Body.setAngle(child, parent.angle + max);
        Body.setAngularVelocity(child, parent.angularVelocity);
      }
    }
  }

5. Assembling the Composite

Bundle all created bodies and constraints into a single Composite container and attach the limit enforcement to the engine loop.

  const ragdollComposite = Composite.create({ label: 'Procedural Ragdoll' });

  Composite.add(ragdollComposite, [
    head, torso,
    leftUpperArm, leftLowerArm, rightUpperArm, rightLowerArm,
    leftUpperLeg, leftLowerLeg, rightUpperLeg, rightLowerLeg,
    neck, leftShoulder, rightShoulder, leftElbow, rightElbow,
    leftHip, rightHip, leftKnee, rightKnee
  ]);

  return {
    composite: ragdollComposite,
    update: enforceLimits
  };
}

To run the ragdoll, add the composite to your Matter.js world and register the update method:

const ragdoll = createRagdoll(400, 200, 1);
Composite.add(engine.world, ragdoll.composite);

Matter.Events.on(engine, 'beforeUpdate', () => {
  ragdoll.update();
});