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 integer2. 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();
});