FERRETGame engine over Babylon.js
Ferret

Gravity

Package @ferret/physics-gravity, capability name gravity, phase field.

What it is for#

Without this capability the kernel pulls everything down with one constant. With it, gravity is a field: one law that the cannonball, the character and every other capability read, which can change from place to place. Use it for low-gravity rooms, zero-g, a moon you can fall onto, a pit that pulls things in, a level set on another planet. It is not needed for a game where "down at 9.81" is all you want: the kernel's own gravity does that.

The model#

At a point p the gravity acceleration is

g(p) = ( field vector of the narrowest scope  +  Σ attractor pulls at p )  ×  scale of the narrowest scope

and the force on a body is m · g(p) · body.gravityScale. Three dials, all independent:

DialWhereWhat it does
vectorscoped value (world, level, zone, entity)the uniform field, m/s², default [0, -9.81, 0]
scalescoped valuemultiplies the whole field: 0 = weightless, 0.16 = the moon
gravityScalethe body (RigidBody parameter)per body, on top

The force is evaluated at every sub-step position, so a fast or orbiting body feels the field where it is, not where it started the step. While gravity is on, the kernel's own constant is set to zero and restored when you switch the capability off.

First use#

import { PhysicsHost } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';

const host = world.addService(new PhysicsHost({ capabilities: [gravity()] }));
host.enable('gravity');
host.config('gravity').set('world', 'vector', [0, -3.7, 0]);        // Mars

const rock = world.spawn('rock', { position: new BABYLON.Vector3(0, 100, 0) });
rock.add('RigidBody', { shape: 'sphere', radius: 0.2, mass: 1 });
world.run(2);

Recipes#

A low-gravity room#

A zone changes the field for bodies inside it and only them. Zones come from the ZoneService; a body is "inside" by its position.

import { PhysicsHost } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';
import { ZoneService } from '@ferret/zones';

const host = world.addService(new PhysicsHost({ capabilities: [gravity()] }));
const zones = world.addService(new ZoneService());
zones.add({ name: 'moon-room', shape: { kind: 'box', centre: [0, 50, 0], size: [10, 200, 10] } });
host.enable('gravity');
host.config('gravity').set('zone', 'vector', [0, -1.6, 0], 'moon-room');

const ball = (name: string, x: number) => {
  const e = world.spawn(name, { position: new BABYLON.Vector3(x, 100, 0) });
  e.add('RigidBody', { shape: 'sphere', radius: 0.2, mass: 1 });
  return e;
};
const inside = ball('in', 0), outside = ball('out', 30);
world.run(2);

The same from a level manifest: "gravity": { "vector": [0, -1.6, 0] } for the level, or per-zone values through the gravity.set command with scope: "zone" and the zone's name.

Weightless: one object, one room, the whole world#

  • one entity: host.config('gravity').set('entity', 'scale', 0, entity.id);
  • one body at creation: gravityScale: 0 in the RigidBody parameters;
  • a zone: set('zone', 'scale', 0, 'zero-g');
  • everything: set('world', 'scale', 0).

scale and gravityScale multiply, so 0.5 and 0.5 give a quarter.

A planet, a moon, a pit#

A point attractor pulls toward a centre, inverse square. Give it either the gravitational parameter mu = G·M (m³/s²) or the surface gravity and the radius of the body, which is easier to author: "3 m/s² at 50 m".

import { PhysicsHost } from '@ferret/physics';
import { gravity } from '@ferret/physics-gravity';

const g = gravity();
const host = world.addService(new PhysicsHost({ capabilities: [g] }));
host.enable('gravity');
host.config('gravity').set('world', 'vector', [0, 0, 0]);                       // no uniform field: only the planet
g.addAttractor({ id: 'rock', position: [0, 0, 0], surfaceGravity: 3, radius: 50 });
const onSurface = g.at(host, [50, 0, 0]);
const twiceAway = g.at(host, [100, 0, 0]);

Attractor options: position (a fixed point) or entity (the centre follows a node: a moving moon), softening (below this distance the pull stops growing; default 0.1 × radius, or 0.5 m, so nothing divides by zero at the centre), range (beyond it the attractor does nothing: keep a small pit from pulling the whole level).

A circular orbit needs speed v = √(mu / r). Orbits are good for decoration and puzzles; do not rely on one lasting for hours.

Reading gravity from your own code#

host.fields.gravity(p, ctx?) returns the acceleration at a point while the capability is on. The character controller uses it when present and keeps its own (stronger, game-feel) gravity when it is off. Your own code should do the same: ask the field, fall back to a default.

const g = host.fields.gravity?.([x, y, z]) ?? [0, -9.81, 0];

Parameters#

ValueScopeUnitDefaultSafe rangeEffect
vectorworld, level, zone, entitym/s²[0, -9.81, 0]anythe uniform field; [0,0,0] for attractors only
scaleworld, level, zone, entity–10 – 10multiplies the whole field at that point
Attractor fieldMeaning
idname, for removal
position / entitywhere the centre is
muG·M, m³/s²; or surfaceGravity and radius instead
softeningmetres; the pull is flat inside it
rangemetres; zero pull beyond

Commands#

gravity.at {position, entity?} returns the vector; gravity.set {vector, scope?, id?}; gravity.scale {scale, scope?, id?}; gravity.attractor.add {id, position | entity, mu | surfaceGravity+radius, softening?, range?}; gravity.attractor.remove {id}. An agent can build a planet with these alone.

Interplay#

  • media: buoyancy uses the field's direction and strength, so water in a zone with lower gravity floats things higher. A vessel computes its own weight and buoyancy and is marked exempt from gravity and media; see vehicles.
  • heat, liquids: convection and the liquid's surface read the field too.
  • Characters and AI agents: read fields.gravity when on.
  • The gravity capability does not move characters by itself; the character's movement integrates the vector it reads.

Budget#

One vector per body per step plus one term per attractor. Hundreds of bodies and a few attractors are far below a millisecond; do not add an attractor per rock (use one planet).

Debugging#

SymptomCause
A body does not fallscale is 0 at its scope (entity, zone or world), its gravityScale is 0, it is static, or it is exempt
Gravity is doubledthe capability is off and the kernel's own is on and you added your own force: pick one
A zone has no effectthe world has no ZoneService, the zone's box does not contain the body's position (not its extent), or an entity-scope value overrides it
attractor "x" needs "mu", or "surfaceGravity" and "radius"give one of the two forms
A body shoots away near the centresoftening is smaller than the body's distance steps: raise it
The character keeps its own gravitythe capability is off; the character only reads the field while it is on

Reference#

gravity, GravityCapability, Attractor, GRAVITY_DEFAULTS, PhysicsFields, @ferret/physics-gravity.