Create Planetary Gravity with Matter.js Attractors

Creating a planetary gravity system in Matter.js allows you to simulate realistic orbital mechanics, celestial bodies, and central gravitational wells in a 2D environment. While Matter.js applies uniform downward gravity by default, you can simulate planetary physics by disabling world gravity and applying radial attraction forces between bodies using Newton's law of universal gravitation. This guide explains how to configure the physics engine, write an attractor function that pulls orbiting bodies toward a central planet, and calculate the tangential velocity needed to establish stable circular orbits.

1. Disable Standard World Gravity

Matter.js engines default to pulling dynamic bodies down along the Y-axis. To establish a planetary system, first eliminate this uniform downward pull by setting the engine world's gravity.scale or directional components to zero.

const engine = Matter.Engine.create();
engine.gravity.x = 0;
engine.gravity.y = 0;
engine.gravity.scale = 0;

2. Implement the Gravitational Attraction Logic

Gravitational pull follows Newton’s Law of Universal Gravitation:

\[F = G \frac{m_1 \cdot m_2}{r^2}\]

Where:

To apply this force without external plugins, hook into the beforeUpdate event of the engine. For every frame, calculate the vector difference between the planet and the satellite, compute the magnitude of the force, and apply that force to the satellite using Matter.Body.applyForce.

Matter.Events.on(engine, 'beforeUpdate', () => {
    const G = 0.001; // Gravitational constant adjusted for simulation scale
    
    const dx = planet.position.x - satellite.position.x;
    const dy = planet.position.y - satellite.position.y;
    const distanceSquared = dx * dx + dy * dy;
    const distance = Math.sqrt(distanceSquared);

    // Prevent extreme forces at very close distances
    if (distance > planet.circleRadius) {
        const forceMagnitude = (G * planet.mass * satellite.mass) / distanceSquared;
        
        const force = {
            x: (dx / distance) * forceMagnitude,
            y: (dy / distance) * forceMagnitude
        };

        Matter.Body.applyForce(satellite, satellite.position, force);
    }
});

If you are using the official matter-attractors plugin, you can instead assign this logic directly to the planet's plugin.attractors array:

Matter.use('matter-attractors');

const planet = Matter.Bodies.circle(400, 300, 50, {
    isStatic: true,
    plugin: {
        attractors: [
            (bodyA, bodyB) => {
                const dx = bodyA.position.x - bodyB.position.x;
                const dy = bodyA.position.y - bodyB.position.y;
                const distanceSquared = dx * dx + dy * dy;
                const distance = Math.sqrt(distanceSquared);

                if (distance > bodyA.circleRadius) {
                    const G = 0.001;
                    const forceMagnitude = (G * bodyA.mass * bodyB.mass) / distanceSquared;
                    return {
                        x: (dx / distance) * forceMagnitude,
                        y: (dy / distance) * forceMagnitude
                    };
                }
            }
        ]
    }
});

3. Establish Stable Orbits with Tangential Velocity

If a satellite starts with zero velocity, the attractor will pull it straight into the central planet. To achieve a stable circular orbit, the satellite needs an initial velocity perpendicular to the line connecting it to the planet.

The formula for circular orbital velocity is:

\[v = \sqrt{\frac{G \cdot M}{r}}\]

Where \(M\) is the mass of the central planet, \(r\) is the orbital radius, and \(G\) is your chosen gravitational constant.

Set the initial velocity of the satellite at spawn:

const orbitalRadius = 200;
const orbitalSpeed = Math.sqrt((G * planet.mass) / orbitalRadius);

// Position satellite directly above the planet
const satellite = Matter.Bodies.circle(planet.position.x, planet.position.y - orbitalRadius, 10, {
    frictionAir: 0, // Eliminate air drag to prevent orbit decay
    friction: 0
});

// Give satellite horizontal velocity (perpendicular to radial vector)
Matter.Body.setVelocity(satellite, {
    x: orbitalSpeed,
    y: 0
});

Complete Implementation Example

const { Engine, Render, Runner, Bodies, Composite, Events, Body } = Matter;

const engine = Engine.create();
engine.gravity.scale = 0;

const render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        width: 800,
        height: 600,
        wireframes: false
    }
});

Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

const G = 0.005;
const centerX = 400;
const centerY = 300;
const orbitRadius = 180;

// Central Planet
const planet = Bodies.circle(centerX, centerY, 40, {
    isStatic: true,
    render: { fillStyle: '#e74c3c' }
});

// Satellite
const satellite = Bodies.circle(centerX, centerY - orbitRadius, 8, {
    frictionAir: 0,
    render: { fillStyle: '#3498db' }
});

// Calculate and apply initial velocity for a circular orbit
const initialSpeed = Math.sqrt((G * planet.mass) / orbitRadius);
Body.setVelocity(satellite, { x: initialSpeed, y: 0 });

Composite.add(engine.world, [planet, satellite]);

// Gravitational loop
Events.on(engine, 'beforeUpdate', () => {
    const dx = planet.position.x - satellite.position.x;
    const dy = planet.position.y - satellite.position.y;
    const distSq = dx * dx + dy * dy;
    const dist = Math.sqrt(distSq);

    if (dist > planet.circleRadius) {
        const forceMag = (G * planet.mass * satellite.mass) / distSq;
        Body.applyForce(satellite, satellite.position, {
            x: (dx / dist) * forceMag,
            y: (dy / dist) * forceMag
        });
    }
});