Curved Pinball Rails with Smooth Vertices in Matter.js

This guide explains how to construct smooth, curved pinball guide rails in Matter.js using polygonal vertex sets and physics bodies. You will learn how to generate smooth mathematical curves using Bézier equations, expand those curves into thick physical polygons, decompose non-convex shapes for accurate collisions, and tune physical properties like friction and elasticity for authentic pinball gameplay.

1. Generate the Centerline Points

To build a smooth curve, calculate an ordered sequence of 2D coordinates along a mathematical path, such as a Cubic Bézier curve. The curve is defined by a start point (\(P_0\)), two control points (\(P_1, P_2\)), and an end point (\(P_3\)).

Sample points along the curve by incrementing a parameter \(t\) from 0 to 1:

function getCubicBezierPoint(p0, p1, p2, p3, t) {
  const cx = 3 * (p1.x - p0.x);
  const bx = 3 * (p2.x - p1.x) - cx;
  const ax = p3.x - p0.x - cx - bx;

  const cy = 3 * (p1.y - p0.y);
  const by = 3 * (p2.y - p1.y) - cy;
  const ay = p3.y - p0.y - cy - by;

  const tSquared = t * t;
  const tCubed = tSquared * t;

  return {
    x: (ax * tCubed) + (bx * tSquared) + (cx * t) + p0.x,
    y: (ay * tCubed) + (by * tSquared) + (cy * t) + p0.y
  };
}

function generateCurvePoints(p0, p1, p2, p3, segments = 30) {
  const points = [];
  for (let i = 0; i <= segments; i++) {
    const t = i / segments;
    points.push(getCubicBezierPoint(p0, p1, p2, p3, t));
  }
  return points;
}

A higher segment count yields a smoother curve, but between 20 and 40 segments is typically sufficient to balance performance and collision precision.

2. Thicken the Curve into a Polygon

Matter.js cannot simulate zero-width lines. You must extrude the curve into a closed polygon with physical thickness by calculating normal vectors for each segment and generating an inner and outer edge.

function createThickRailVertices(points, thickness = 10) {
  const halfWidth = thickness / 2;
  const leftSide = [];
  const rightSide = [];

  for (let i = 0; i < points.length; i++) {
    const current = points[i];
    const next = points[i + 1] || current;
    const prev = points[i - 1] || current;

    // Calculate tangent vector
    const dx = next.x - prev.x;
    const dy = next.y - prev.y;
    const length = Math.hypot(dx, dy) || 1;

    // Calculate normal vector perpendicular to tangent
    const nx = -dy / length;
    const ny = dx / length;

    // Push outer and inner boundary coordinates
    leftSide.push({ x: current.x + nx * halfWidth, y: current.y + ny * halfWidth });
    rightSide.unshift({ x: current.x - nx * halfWidth, y: current.y - ny * halfWidth });
  }

  // Return a closed loop of vertices
  return leftSide.concat(rightSide);
}

3. Create the Matter.js Rigid Body

Curved rails form non-convex (concave) shapes. Matter.js uses the Separating Axis Theorem (SAT), which requires shapes to be convex. To handle concave polygons, Matter.js requires a concave decomposition library like poly-decomp.

Method A: Using poly-decomp

Ensure poly-decomp is loaded globally on your window or passed directly:

// Provide poly-decomp to Matter.js
Matter.Common.setDecomp(window.decomp);

const curvePoints = generateCurvePoints(
  { x: 100, y: 300 },
  { x: 150, y: 150 },
  { x: 250, y: 150 },
  { x: 300, y: 300 },
  30
);

const railVertices = createThickRailVertices(curvePoints, 12);

// Position coordinates correspond to the placement offset
const railBody = Matter.Bodies.fromVertices(200, 200, railVertices, {
  isStatic: true,
  friction: 0.05,
  restitution: 0.3
});

Matter.Composite.add(engine.world, railBody);

High-speed pinball balls can occasionally tunnel through thin decomposed vertices. A robust alternative is creating a compound body made of overlapping rectangular segments along the curve:

function createSegmentedRail(points, thickness = 10, options = {}) {
  const parts = [];

  for (let i = 0; i < points.length - 1; i++) {
    const p1 = points[i];
    const p2 = points[i + 1];

    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const length = Math.hypot(dx, dy);
    const angle = Math.atan2(dy, dx);
    const midX = (p1.x + p2.x) / 2;
    const midY = (p1.y + p2.y) / 2;

    // Create a small rectangular section with slight overlap
    const segment = Matter.Bodies.rectangle(midX, midY, length + 2, thickness, {
      angle: angle,
      isStatic: true
    });

    parts.push(segment);
  }

  return Matter.Body.create(Object.assign({
    parts: parts,
    isStatic: true
  }, options));
}

4. Fine-Tuning Physics Attributes

Pinball rails require specific material settings to guarantee predictable ball behavior: