How to Simulate Maglev Trains in Matter.js

This guide explains how to simulate a stable magnetic levitation (maglev) train floating above track bodies using the Matter.js 2D physics engine. Because Matter.js does not provide built-in electromagnetic field mechanics, achieving stable levitation requires continuously measuring the distance between the train and the track and applying dynamic restorative forces. By utilizing a Proportional-Derivative (PD) controller within the engine’s update loop, you can eliminate perpetual bouncing, prevent clipping, and keep the train hovering smoothly.

1. The Physics Strategy: PD Control Loop

Simulating magnetic repulsion with a simple upward spring force causes the train to oscillate violently or fly off the track. To achieve equilibrium, use a Proportional-Derivative (PD) controller:

The force applied per physics tick follows this formula: \[\text{Force}_y = -(k_p \times (\text{targetHeight} - \text{currentHeight}) - k_d \times \text{velocity}_y)\]

2. Setting Up Bodies and Collision Filters

Create the track as a static body and the train as a dynamic body. To avoid unwanted physics glitches when forces balance, disable friction on both objects. You can also use Matter.js collision categories to prevent physical mesh collisions between the train and the track, letting the custom repulsion force handle all separation.

const track = Matter.Bodies.rectangle(400, 500, 800, 40, { 
    isStatic: true, 
    friction: 0 
});

const train = Matter.Bodies.rectangle(400, 300, 150, 40, { 
    friction: 0,
    frictionAir: 0.001,
    mass: 5
});

Matter.Composite.add(engine.world, [track, train]);

3. Implementing the Levitation Loop

Attach a listener to the beforeUpdate event of your Matter.js engine. In each frame, determine the clearance beneath multiple points of the train (such as the front and rear) to maintain pitch stability.

const TARGET_HOVER_DISTANCE = 40;
const kP = 0.005; // Repulsion strength
const kD = 0.08;  // Damping strength

Matter.Events.on(engine, 'beforeUpdate', () => {
    // Measure suspension points (left and right of center for pitch stability)
    const suspensionOffsets = [-50, 50];

    suspensionOffsets.forEach(offsetX => {
        const rayOrigin = { 
            x: train.position.x + offsetX, 
            y: train.position.y + 20 
        };
        
        // Raycast down toward the track
        const rayEnd = { x: rayOrigin.x, y: rayOrigin.y + 100 };
        const collisions = Matter.Query.ray([track], rayOrigin, rayEnd);

        if (collisions.length > 0) {
            const hit = collisions[0];
            const currentDistance = hit.point.y - rayOrigin.y;
            const distanceError = TARGET_HOVER_DISTANCE - currentDistance;

            // Calculate vertical velocity at this corner
            const verticalVelocity = train.velocity.y;

            // Compute PD response
            const upwardForceMagnitude = (distanceError * kP) - (verticalVelocity * kD);

            // Apply force upwards when within repulsion range
            if (currentDistance < TARGET_HOVER_DISTANCE * 1.5) {
                Matter.Body.applyForce(
                    train, 
                    { x: rayOrigin.x, y: rayOrigin.y }, 
                    { x: 0, y: -Math.max(0, upwardForceMagnitude) }
                );
            }
        }
    });
});

4. Lateral Guidance and Propulsion

Hovering eliminates contact friction, meaning any horizontal force will slide the train indefinitely: