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: