Simulate Orbital Mechanics in Matter.js
This article explains how to create a 2D orbital mechanics simulation around a massive planet using Matter.js by overriding the default physics behavior. You will learn how to disable ambient engine gravity, set up a stable circular or elliptical trajectory with initial tangential velocity, and compute custom gravitational acceleration step-by-step using Matter.js engine update hooks.
1. Disable Default World Gravity
Matter.js defaults to a downward gravitational pull suited for platformer-style games. Orbital mechanics require a central, radial force field instead. Set the world gravity to zero during engine initialization:
const engine = Matter.Engine.create();
engine.gravity.x = 0;
engine.gravity.y = 0;
engine.gravity.scale = 0;2. Create the Planet and Satellite Bodies
Create the central planet as a static body with a high virtual mass and place the satellite at an offset position.
const { Bodies, World } = Matter;
// Central massive body (Planet)
const planet = Bodies.circle(400, 300, 40, {
isStatic: true,
mass: 100000
});
// Orbiting body (Satellite)
const satellite = Bodies.circle(400, 100, 10, {
mass: 1,
frictionAir: 0 // Eliminate air resistance to preserve orbital energy
});
World.add(engine.world, [planet, satellite]);3. Set Initial Tangential Velocity
An orbit requires sufficient perpendicular velocity to counterbalance gravitational collapse. For a circular orbit, the necessary orbital speed \(v\) is calculated using:
\[v = \sqrt{\frac{G \cdot M}{r}}\]
Where \(G\) is the gravitational constant, \(M\) is the planet's mass, and \(r\) is the orbital radius.
const G = 0.001; // Gravitational constant scaled for simulation
const dx = planet.position.x - satellite.position.x;
const dy = planet.position.y - satellite.position.y;
const distance = Math.hypot(dx, dy);
// Velocity magnitude for a circular orbit
const orbitalSpeed = Math.sqrt((G * planet.mass) / distance);
// Assign initial perpendicular velocity vector
Matter.Body.setVelocity(satellite, {
x: orbitalSpeed,
y: 0
});4.
Implement Custom Velocity Steps with beforeUpdate
While Matter.js uses Verlet integration internally, you can execute
custom velocity steps using the beforeUpdate event.
Calculate the gravitational acceleration vector toward the planet and
integrate it into the satellite's velocity each tick:
Matter.Events.on(engine, 'beforeUpdate', (event) => {
// Use event.delta (in milliseconds) converted to seconds
const dt = event.delta / 1000;
// Vector from satellite to planet
const rx = planet.position.x - satellite.position.x;
const ry = planet.position.y - satellite.position.y;
const rSquared = rx * rx + ry * ry;
const r = Math.sqrt(rSquared);
// Avoid division by zero or singularity at close distances
if (r < 5) return;
// Gravitational acceleration: a = (G * M) / r^2
const accelerationMagnitude = (G * planet.mass) / rSquared;
// Normalized direction vector multiplied by acceleration
const ax = (rx / r) * accelerationMagnitude;
const ay = (ry / r) * accelerationMagnitude;
// Update satellite velocity (Semi-Implicit Euler step)
const currentVelocity = satellite.velocity;
Matter.Body.setVelocity(satellite, {
x: currentVelocity.x + ax * dt * 60, // Scale by frame rate factor
y: currentVelocity.y + ay * dt * 60
});
});5. Managing Numerical Stability
Standard Euler velocity integration introduces slight energy drift over long simulations. To maintain a stable orbit in Matter.js:
- Set
satellite.frictionAir = 0to prevent decay from drag. - Maintain small, fixed time steps
(
engine.timing.isFixed = true). - Add a softening parameter (\(\epsilon\)) to the distance calculation (\(r^2 + \epsilon^2\)) to prevent extreme acceleration spikes if the satellite passes close to the center of the planet.