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);Method B: Segmented Compound Bodies (Recommended for Pinball)
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:
isStatic: true: Locks the rail in world space so incoming ball impacts do not displace it.friction: 0.01 - 0.05: Smooth metal or plastic guide rails exhibit very little surface friction. Lower values keep the ball moving smoothly without snagging.restitution: 0.2 - 0.5: Guide rails should absorb some energy while redirecting the ball. A value that is too high will cause excessive bouncing instead of guiding the ball along the path.- Collision Detection: Enable
engine.timing.subStepsor reduce your simulation delta time (e.g., run the engine at 120Hz or higher sub-stepping) to prevent fast-moving pinballs from penetrating the curved vertices.