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:
- \(G\) is the gravitational constant (scaled for screen coordinates).
- \(m_1\) and \(m_2\) are the masses of the two interacting bodies.
- \(r\) is the distance between the centers of both bodies.
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
});
}
});