Physics
Physics is Jolt (jolt-physics 1.1, wasm) behind an EngineModule. It provides rigid bodies, CharacterVirtual controllers, raycast / raycast-batch / overlap-sphere queries, contact events and a wireframe debug view. Queries are the only synchronous imports the guest gets; everything that mutates the world is a command in frame-output (add-body, apply-impulse, move-character, ...). Physics steps inside fixedUpdate, so results are deterministic for a given command stream.
The shape of a frame
fixedUpdate(dt) host: world.step(dt)
-> readBodies(buffer) host: stride-14 rows into frame-input.bodies
-> drainContacts(pool) host: only bodies flagged report-contacts
-> guest tick() game: reads bodies, emits commands
-> apply commands host: add-body, move-character, apply-impulse, ...The body buffer is a packed Float32Array, stride 14, one row per enabled non-static body, sorted ascending by body id:
| lane | meaning |
|---|---|
| 0 | body id |
| 1–3 | position x, y, z |
| 4–7 | rotation x, y, z, w |
| 8–10 | linear velocity |
| 11–13 | angular velocity |
Static bodies are never written — they never move, and the guest already knows where it put them. The buffer is preallocated and reused; readBodies returns the row count it wanted, so a guest that outgrows its buffer grows it once and carries on rather than allocating every frame.
Layers and masks
A body declares what it is (layer) and what it collides with (mask), both as bitsets. Two bodies interact only when a.mask & b.layer and b.mask & a.layer are both non-zero, which makes one-way relationships impossible by construction — a pickup that ignores the player is a pickup the player also walks through.
Jolt itself wants a symmetric table of object layers, so the module mints one object layer per distinct (layer, mask, moving) triple a game actually uses, lazily, and enables the pairs that the masks imply. Sixty-four slots — thirty-two static, thirty-two moving — cover any reasonable game; layers.maxObjectLayers raises the ceiling.
Characters
A character body is a Jolt CharacterVirtual, not a rigid body: it has no inertia, does not bounce, and is moved by setting a desired velocity rather than by forces. It carries an inner kinematic body so that other bodies collide with it and raycasts hit it, which is why one body id addresses both halves.
move-character takes the horizontal components literally. A positive y while grounded is a jump; after that gravity integrates until the character lands. The controller sticks to the floor over small drops, walks up stairs, and refuses slopes steeper than 45°.
Queries
raycast, raycast-batch and overlap-sphere are the only blocking calls the guest may make during tick, because each one is a full canonical-ABI round trip. Prefer the batch form: it packs N rays into one buffer (stride 7) and returns N results (stride 9) for a single crossing.
Mesh colliders respect triangle winding — Jolt ignores back faces — so a collider baked with the wrong winding is invisible to hitscan weapons while still stopping the player.
Debug view
engine.get('physics').debugWireframe(scene) adds a LineSegments drawing each body's oriented bounds. The line buffer is rebuilt only when the set of bodies changes; every other frame just rewrites the existing positions.
See also
- The engine loop
- Modules
- Add a physics body
packages/physics-jolt/README.md— @aosengine/physics-jolt