How to Simulate Banking Curves in Matter.js
Matter.js is a 2D physics engine, which means it cannot natively represent the third dimension (tilt or banking) required to keep marbles on a curved track through gravitational and normal force interactions. This article explains how to simulate the physical effects of banked turns in Matter.js using inward centripetal force injection, dynamic sensor zones, and velocity redirection to keep high-speed marbles securely on the track.
The 2D Limitation of Banking
In real-world tracks, a banked turn angles the surface inward. The normal force from the track tilts, providing an inward horizontal force component (centripetal force) that counters the marble's inertia. Because Matter.js operates solely on the X and Y axes, a marble traveling through a flat 2D bend relies purely on collision boundaries to turn. At high velocities, the marble will rebound, tunnel through the wall, or bounce off unrealistically.
To simulate banking, you must artificially create the inward acceleration that a tilted floor would otherwise provide.
Method 1: Applying Artificial Centripetal Force
The most physically accurate way to simulate a banked curve is to apply a continuous inward force toward the curve’s center of curvature whenever a marble enters the turn.
Calculate the Required Force: The ideal centripetal force formula is \(F = \frac{m \cdot v^2}{r}\), where \(m\) is the marble's mass, \(v\) is its current speed, and \(r\) is the radius of the turn. For a banked curve, you scale this force by the simulated bank angle (\(\theta\)):
\[\vec{F}_{\text{inward}} = m \cdot \frac{v^2}{r} \cdot \sin(\theta)\]
Apply the Force Each Tick: Attach a listener to the
beforeUpdateevent to apply the force vector pointing toward the arc's center:
Matter.Events.on(engine, 'beforeUpdate', () => {
const center = { x: 300, y: 300 }; // Center of the curve
const radius = 150;
const bankFactor = 0.6; // Simulates the banking angle steepness
const dx = center.x - marble.position.x;
const dy = center.y - marble.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Check if the marble is within the curve radius
if (distance > radius - 20 && distance < radius + 20) {
const speed = Matter.Vector.magnitude(marble.velocity);
const forceMagnitude = (marble.mass * (speed * speed) / radius) * bankFactor;
// Normalized vector toward the center
const normal = { x: dx / distance, y: dy / distance };
Matter.Body.applyForce(marble, marble.position, {
x: normal.x * forceMagnitude,
y: normal.y * forceMagnitude
});
}
});Method 2: Curve Sensors for Localized Behavior
Instead of calculating global coordinates, place an invisible sensor
body (isSensor: true) shaped like the curved section of the
track.
- Create a curved compound body or polygonal zone covering the turn.
- Listen for collision events via
Matter.Events.on(engine, 'collisionActive', callback). - When the marble intersects the sensor, modify its physical
properties dynamically:
- Increase Friction: Set
marble.frictionhigher to simulate track grip. - Nullify Restitution: Set
marble.restitution = 0while inside the curve to prevent elastic bouncing off outer walls. - Apply Radial Damping: Slightly damp the component of velocity directed away from the curve center.
- Increase Friction: Set
Method 3: Velocity Vector Redirection
If applying dynamic forces causes instability, you can directly alter the velocity vector to guide the marble smoothly along the track tangent.
- Find the unit tangent vector of the track at the marble's current position.
- Decompose the marble's velocity into tangential and radial components.
- Dampen or eliminate the outward radial velocity component completely, mimicking an infinitely steep bank:
Matter.Events.on(engine, 'beforeUpdate', () => {
if (inBankedZone(marble)) {
const normal = getNormalToTrack(marble.position);
const radialVelocity = Matter.Vector.dot(marble.velocity, normal);
// If the marble is moving outward toward the wall, cancel that velocity
if (radialVelocity > 0) {
Matter.Body.setVelocity(marble, {
x: marble.velocity.x - normal.x * radialVelocity,
y: marble.velocity.y - normal.y * radialVelocity
});
}
}
});Optimizing Track Boundaries
To prevent tunneling or clipping when marbles travel at extreme speeds around curves:
- Increase Engine Iterations: Increase
engine.positionIterationsandengine.velocityIterationsfrom the default6to10or higher. - Segment Outer Walls: Build outer walls out of dense, overlapping convex hulls or chamfered rectangles rather than single thin polygons.
- Use Continuous Collision Detection (CCD): If using high speeds, ensure step sizes are small using fixed sub-stepping in your main loop rather than relying on variable frame deltas.