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.

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().

  1. Filter the raycast collisions to ignore the player body itself.
  2. If the ray intersects a static obstacle, locate the specific vertex (corner) closest to the collision point.
  3. Push that corner's coordinates into the rope.anchors stack.
  4. Update the active constraint:
    • Set constraint.pointB to the new corner.
    • Decrease the constraint’s length by the distance between the previous anchor and the new anchor.
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:

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:

  1. Look at the last corner in rope.anchors (the active anchor) and the second-to-last anchor.
  2. 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.
  3. If the sign of this cross product reverses relative to the stored winding direction, the player has unwrapped the corner.

When unwrapping occurs:

  1. Pop the active corner off the rope.anchors stack.
  2. Increase the active constraint’s length by the distance between the removed corner and the new active anchor.
  3. Reattach rope.constraint.pointB to 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.