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:
| Dial | Where | What it does |
|---|---|---|
vector | scoped value (world, level, zone, entity) | the uniform field, m/s², default [0, -9.81, 0] |
scale | scoped value | multiplies the whole field: 0 = weightless, 0.16 = the moon |
gravityScale | the 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: 0in theRigidBodyparameters; - 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#
| Value | Scope | Unit | Default | Safe range | Effect |
|---|---|---|---|---|---|
vector | world, level, zone, entity | m/s² | [0, -9.81, 0] | any | the uniform field; [0,0,0] for attractors only |
scale | world, level, zone, entity | – | 1 | 0 – 10 | multiplies the whole field at that point |
| Attractor field | Meaning |
|---|---|
id | name, for removal |
position / entity | where the centre is |
mu | G·M, m³/s²; or surfaceGravity and radius instead |
softening | metres; the pull is flat inside it |
range | metres; 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 markedexemptfromgravityandmedia; see vehicles.heat,liquids: convection and the liquid's surface read the field too.- Characters and AI agents: read
fields.gravitywhen on. - The
gravitycapability 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#
| Symptom | Cause |
|---|---|
| A body does not fall | scale is 0 at its scope (entity, zone or world), its gravityScale is 0, it is static, or it is exempt |
| Gravity is doubled | the capability is off and the kernel's own is on and you added your own force: pick one |
| A zone has no effect | the 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 centre | softening is smaller than the body's distance steps: raise it |
| The character keeps its own gravity | the capability is off; the character only reads the field while it is on |
Reference#
gravity, GravityCapability, Attractor, GRAVITY_DEFAULTS, PhysicsFields, @ferret/physics-gravity.