Inverse-Square Gravity in Matter.js

This article explains how to implement custom inverse-square gravitational forces toward a central body, such as a black hole, using the Matter.js 2D physics engine. By default, Matter.js only provides uniform directional gravity, so creating a radial gravitational pull requires disabling the global gravity field, calculating distance-based vectors on each engine update step, and applying continuous forces to affected bodies.

Disabling Default Gravity

Before applying a custom gravitational field, disable the default world gravity to prevent bodies from falling downward along the Y-axis:

const engine = Matter.Engine.create();
engine.gravity.scale = 0; // Disables standard downward gravity

The Inverse-Square Force Formula

Newton’s law of universal gravitation dictates that the gravitational force between two objects is inversely proportional to the square of the distance between their centers:

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

In a simulation, as \(r\) approaches zero, the force approaches infinity, which can cause bodies to launch uncontrollably across the screen. To prevent this, apply a distance clamping or a softening parameter (\(\epsilon\)).

Implementation via beforeUpdate

Use the Matter.Events.on(engine, 'beforeUpdate', callback) event to calculate and apply forces before the physics engine computes body positions for the next frame.

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

// 1. Setup Engine & World
const engine = Engine.create();
engine.gravity.scale = 0;
const world = engine.world;

// 2. Create the Central Black Hole
const blackHole = Bodies.circle(400, 300, 30, {
    isStatic: true, // Prevents the black hole from being moved by other bodies
    mass: 1000      // High mass for strong attraction
});

// 3. Create Orbiting Bodies
const particle = Bodies.circle(400, 150, 10, {
    mass: 1
});

// Impart an initial tangential velocity for an orbit
Body.setVelocity(particle, { x: 4, y: 0 });

Composite.add(world, [blackHole, particle]);

// 4. Apply Inverse-Square Forces
const G = 0.5;            // Gravitational constant
const minDistance = 25;   // Softening limit to avoid division-by-zero singularities

Events.on(engine, 'beforeUpdate', () => {
    const bodies = Composite.allBodies(world);

    bodies.forEach(body => {
        // Do not apply gravity to static bodies or the black hole itself
        if (body.isStatic || body === blackHole) return;

        // Calculate offset vector from body to black hole
        const deltaX = blackHole.position.x - body.position.x;
        const deltaY = blackHole.position.y - body.position.y;
        
        // Calculate Euclidean distance
        const distance = Math.hypot(deltaX, deltaY);

        // Clamp minimum distance to avoid extreme force spikes
        const clampedDistance = Math.max(distance, minDistance);

        // Compute force magnitude: F = G * (m1 * m2) / (r^2)
        const forceMagnitude = (G * blackHole.mass * body.mass) / (clampedDistance * clampedDistance);

        // Normalize direction and apply magnitude
        const force = {
            x: (deltaX / distance) * forceMagnitude,
            y: (deltaY / distance) * forceMagnitude
        };

        // Apply force to the center of the body
        Body.applyForce(body, body.position, force);
    });
});

Key Considerations