The level's physics block
A level declares its physics in the manifest, in one physics block (PhysicsConfig). It is how a level designer, the editor and an agent author realism without writing code.
What it is for#
The block says what is on in this level and with which numbers. When the level loads, the host applies it (beginLevel); when the level goes, everything it did is undone (endLevel). A game keeps nothing of one level's physics in the next.
The fields#
"physics": {
"capabilities": ["gravity", "media", "friction", "impact", "strength"],
"gravity": { "vector": [0, -9.81, 0] },
"materials": { "ice": { "friction": 0.03, "restitution": 0.1 } },
"pairs": [{ "a": "ice", "b": "rubber", "values": { "friction": 0.9 } }],
"combine": { "friction": "min" },
"friction": { "scale": 1.2 },
"constraints": [{ "type": "rope", "a": "Beam", "b": "Weight", "length": 3 }],
"fractured": [{ "node": "Wall", "pieces": ["Wall_a", "Wall_b", "Wall_c"], "toughness": 800 }]
}
| Field | Meaning |
|---|---|
capabilities | names to switch on, in this order. Each must be registered by the game or the load fails with the list of known names |
materials | {name: material}, validated; see materials |
pairs | {a, b, values}: override the combine rule for two materials (light kernel) |
combine | {property: rule}: mean, geometric, min, max, multiply |
gravity.vector | the kernel's gravity for this level (the gravity capability has its own vector) |
<capability name> | an object of that capability's values, applied at level scope before it is switched on: "friction": {"scale": 1.2} |
constraints | links between bodies by the names of their nodes |
fractured | bodies authored in pieces, see below |
Constraints#
type is rod, rope, spring, ball, hinge or fixed (the last three need Havok). Fields: a, b (node names), length, stiffness, damping, maxForce (breaks above it), anchor (world point), axis, limits (radians, [min, max]), id. The result is the same as addConstraint in code; it is removed when the level unloads. A constraint whose node has no body is not an error: beginLevel returns it as a warning, because a level may be loaded before its streamed parts.
import { PhysicsHost, LightKernel } from '@ferret/physics';
const host = world.addService(new PhysicsHost({ kernel: new LightKernel({ gravity: [0, -9.81, 0] }) }));
const beam = world.spawn('Beam', { position: new BABYLON.Vector3(0, 5, 0) });
beam.add('RigidBody', { shape: 'sphere', radius: 0.2, kind: 'static' });
const weight = world.spawn('Weight', { position: new BABYLON.Vector3(0, 3, 0) });
weight.add('RigidBody', { shape: 'sphere', radius: 0.3, mass: 5 });
const warnings = host.beginLevel({ constraints: [
{ type: 'rope', a: 'Beam', b: 'Weight', length: 2 },
{ type: 'rope', a: 'Beam', b: 'Missing', length: 2 },
] });
const linked = host.kernel.constraints.length;
host.endLevel();
Pre-fractured bodies#
For a wall, a pillar or a statue that breaks in designed pieces (rather than split by a rule), author the pieces as nodes next to the whole. fractured: [{ node, pieces, material?, toughness? }] says: when the body on node breaks, show these pieces instead. The pieces wait hidden until then. toughness sets the whole body's toughness for the strength capability at entity scope. At least two pieces are needed. When the whole breaks, each piece becomes a box body of its own size (the bounds of its node), the whole's mass is shared by volume, each piece keeps the motion of its own point of the whole (velocity plus spin) and a push outward from the centre, sized by the capability's burst. The pieces use the fracture's material when given, otherwise the whole's.
Scope: what the level changes and gives back#
beginLevel applies at level scope and endLevel clears it, so nothing leaks:
- the capabilities it switched on go off again (unless another capability still needs them: those stay on);
- level and zone values of every capability are cleared;
- its constraints are removed and its fractures forgotten.
What the block does not undo: materials it added to the library stay (they are only data), and anything a game set at world scope.
Validating before you ship#
import { validatePhysicsConfig } from '@ferret/physics';
const issues = validatePhysicsConfig({
capabilities: ['gravity', 'warp'],
gravity: { vector: [0, -9.81] },
constraints: [{ type: 'chain', a: 'x', b: 'y' }],
}, ['gravity', 'friction']);
The editor session runs this check on the draft level, and beginLevel runs it before applying; call it yourself in tools. Messages carry the path (physics.constraints[0]) so a designer finds the line.
Debugging#
| Symptom | Cause |
|---|---|
invalid physics block: … | the list says which paths; fix them, nothing was applied |
| A constraint is missing | beginLevel returned a warning: the node name has no RigidBody; check the spelling and that the body exists before the level's physics begins |
| Physics of the previous level lingers | the game applied it at world scope: use beginLevel, not configure(…, 'world') |
| A capability is not on after loading | it was not registered, or a requires capability is missing from the list: put the required one first |
Reference#
PhysicsConfig, LevelConstraint, LevelFracture, validatePhysicsConfig, PhysicsHost (beginLevel, endLevel, configure), level manifests.