Realistic Rope Wrapping Around Corners in Matter.js
This article explains how to build a realistic grappling rope mechanic that wraps around obstacle corners in Matter.js. While native Matter.js constraints are straight, point-to-point connections that pass through solid geometry, you can achieve natural wrapping by dynamically creating and removing pivot points using raycasting and angular winding checks.
The Problem with Default Constraints
Matter.js provides Matter.Constraint to connect two
bodies or a body to a fixed world point. However, constraints do not
have collision geometry. If a player swings around a rectangular
obstacle, the constraint line simply clips through the block. Building a
rope out of a chain of tiny rigid bodies (a composite chain) often
causes extreme instability, stretching, or tunneling when subjected to
high tension around sharp corners.
The standard, performant solution is an anchor-stack algorithm that tracks collision points along the rope.
Step 1: Represent the Rope as an Anchor Stack
Instead of treating the entire rope as a single constraint, maintain an array of anchor points.
- Base Anchor: The original fixed point where the grappling hook attached.
- Intermediate Anchors: Vertices of obstacles that the rope has bent around.
- Active Anchor: The most recent corner point in the stack.
- Player Body: The moving
Matter.Body.
Create a single active Matter.Constraint linking the
player body to the active anchor:
const rope = {
anchors: [{ x: hookX, y: hookY }],
constraint: Matter.Constraint.create({
bodyA: playerBody,
pointB: { x: hookX, y: hookY },
stiffness: 1,
length: initialDistance
})
};
Matter.Composite.add(world, rope.constraint);Step 2: Detect Corner Collisions (Wrapping)
On every physics tick (e.g., inside beforeUpdate), cast
a ray from the player’s current position to the active anchor using
Matter.Query.ray().
- Filter the raycast collisions to ignore the player body itself.
- If the ray intersects a static obstacle, locate the specific vertex (corner) closest to the collision point.
- Push that corner's coordinates into the
rope.anchorsstack. - Update the active constraint:
- Set
constraint.pointBto the new corner. - Decrease the constraint’s
lengthby the distance between the previous anchor and the new anchor.
- Set
const activeAnchor = rope.anchors[rope.anchors.length - 1];
const collisions = Matter.Query.ray(obstacles, player.position, activeAnchor);
if (collisions.length > 0) {
const hitBody = collisions[0].body;
const corner = getClosestVertex(collisions[0], hitBody);
// Offset corner slightly outward along the normal to prevent snagging
const offsetCorner = offsetPointFromCorner(corner, hitBody);
rope.anchors.push(offsetCorner);
// Update constraint length and target point
const segmentLength = Matter.Vector.magnitude(
Matter.Vector.sub(offsetCorner, activeAnchor)
);
rope.constraint.pointB = offsetCorner;
rope.constraint.length = Math.max(0, rope.constraint.length - segmentLength);
}Step 3: Track Winding Direction
To know when the rope should unwrap, you must record the winding direction at each corner. Compute the 2D cross product of the rope segments when the wrap occurs:
\[\text{crossProduct} = (A_x - C_x)(P_y - C_y) - (A_y - C_y)(P_x - C_x)\]
Where:
- \(C\) is the new corner point.
- \(A\) is the previous anchor point.
- \(P\) is the player’s position.
Store the sign of this cross product (+1 for clockwise,
-1 for counter-clockwise) alongside the corner data in your
anchor stack.
Step 4: Detect Unwrapping
On each update, evaluate whether the player has swung back across the line of the current anchor segment:
- Look at the last corner in
rope.anchors(the active anchor) and the second-to-last anchor. - Calculate the cross product between the vector from the second-to-last anchor to the active corner, and the vector from the active corner to the player.
- If the sign of this cross product reverses relative to the stored winding direction, the player has unwrapped the corner.
When unwrapping occurs:
- Pop the active corner off the
rope.anchorsstack. - Increase the active constraint’s
lengthby the distance between the removed corner and the new active anchor. - Reattach
rope.constraint.pointBto the new active anchor.
Step 5: Render the Multi-Segment Rope
Because Matter.js only simulates the physics between the player and
the active corner, the default renderer will not draw the wrapped
sections. Disable default rendering for the active constraint
(render: { visible: false }) and draw the rope manually via
the canvas API:
Matter.Events.on(render, 'afterRender', () => {
const ctx = render.context;
ctx.beginPath();
ctx.moveTo(player.position.x, player.position.y);
// Draw lines through all stored anchors in reverse order
for (let i = rope.anchors.length - 1; i >= 0; i--) {
ctx.lineTo(rope.anchors[i].x, rope.anchors[i].y);
}
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
});This system provides clean, taut wrapping and unwrapping with zero physics oscillation, stable swing momentum, and minimal computational overhead.