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 gravityThe 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}\]
- \(G\): Gravitational constant (a scalar value you adjust to control the strength of the simulation).
- \(m_1, m_2\): Masses of the central body and the orbiting body.
- \(r\): Distance between the centers of the two bodies.
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
- Event Timing: Always apply continuous forces inside
the
beforeUpdateevent. Applying them outside the engine tick can result in jittery motion and numerical integration errors. - Mass Invariance: Because
Body.applyForcedivides force by body mass internally (\(a = F / m\)), multiplying bybody.massensures all objects accelerate at the exact same rate regardless of their mass, matching real-world gravitational behavior. - Event Horizon Simulation: If an object gets too
close to the black hole, you can simulate an event horizon by checking
if
distanceis smaller than the black hole's radius, and then callingComposite.remove(world, body)to consume the object.