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.

  1. 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)\]

  2. Apply the Force Each Tick: Attach a listener to the beforeUpdate event 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.

  1. Create a curved compound body or polygonal zone covering the turn.
  2. Listen for collision events via Matter.Events.on(engine, 'collisionActive', callback).
  3. When the marble intersects the sensor, modify its physical properties dynamically:
    • Increase Friction: Set marble.friction higher to simulate track grip.
    • Nullify Restitution: Set marble.restitution = 0 while 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.

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.

  1. Find the unit tangent vector of the track at the marble's current position.
  2. Decompose the marble's velocity into tangential and radial components.
  3. 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: