Reference

Physics

Reference for @irt/physics, Rapier 2D and 3D as a stepper shared by rooms and client prediction.

@irt/physics has one entry per engine, each needing its Rapier package installed beside it:

Entry Needs
@irt/physics/rapier2d @dimforge/rapier2d-compat 0.21.0
@irt/physics/rapier3d @dimforge/rapier3d-compat 0.21.0

Both export the same names. Rapier is loaded by the time the import resolves. The physics guide explains how a tick runs, prediction and saving.

import { physics, body, castRay, RAPIER, type BodyOptions, type BodySpec, type BodyType, type Control, type PhysicsOptions, type PhysicsStepper, type Pose } from "@irt/physics/rapier2d";

physics

function physics<I>(options: {
  gravity: { x: number; y: number };            // 3D: { x, y, z }
  bodies: readonly BodySpec<I>[];
  parseInput?(raw: unknown): I;                 // as a stepper's parseInput, on the room only
}): PhysicsStepper<I>;

interface PhysicsStepper<I> extends Stepper<I> {
  rapierWorld(world: World): RAPIER.World | undefined;
  bodyOf(world: World, entity: Entity | number): RAPIER.RigidBody | undefined;
  entityOf(world: World, of: RAPIER.RigidBody | RAPIER.Collider): Entity | undefined;
}

The result is a stepper: pass it as the room’s stepper and to room.predict. It keeps one Rapier world per room or prediction, keyed by the world it’s stepped with, so one stepper object serves every room in a room host.

  • rapierWorld(world) is the Rapier world for room.entities, for ray casts and other queries. It’s undefined until the room’s first tick or wake, and after the room has left the room host. Forces and impulses can be applied to its bodies, but set poses through the entities’ fields.
  • bodyOf(world, entity) is an entity’s body, by entity or ID, and entityOf(world, bodyOrCollider) the entity a body belongs to, or the entity of the body a collider is attached to, such as a ray cast’s hit. Both are undefined for what has no body, including an entity in the tick it’s created, before the stepper has run. A body can be made again, so look it up rather than keep it across ticks.
  • save returns bytes when the world has a dynamic body: the Rapier world and which entity each body belongs to. load restores them and reconciles with the entities. Without dynamic bodies, save returns undefined and load makes every body from its entity.
  • release frees a room’s Rapier world; the room host calls it when the room leaves, and a prediction when it stops.

physics throws when a kind is listed twice, when a kind lacks a position field or has only part of a group (vx without vy), when a pose field isn’t f32 or f64, and when a pose field is writable.

body

function body<K extends Kind, I>(kind: K, options: BodyOptions<K, I>): BodySpec<I>;

type BodyType = "dynamic" | "fixed" | "kinematic" | "character";

interface BodyOptions<K, I> {
  type: BodyType;
  colliders(entity: Entity<K>): RAPIER.ColliderDesc | RAPIER.ColliderDesc[];
  shapeKey?(entity: Entity<K>): unknown;
  reshape?(colliders: RAPIER.Collider[], entity: Entity<K>, before: Entity<K>): void;
  body?(desc: RAPIER.RigidBodyDesc, entity: Entity<K>): void;
  player?(entity: Entity<K>): string | undefined;
  control?(control: Control<K>, input: I | undefined, dt: number): void;
  character?(controller: RAPIER.KinematicCharacterController): void;
}
Option Meaning
type dynamic is simulated; fixed never moves; kinematic moves where control sets it with body.setNextKinematicTranslation; character is kinematic and moves with control.move
colliders The shapes of an entity’s body, made with the body and again when shapeKey changes. A fixed body’s colliders are made with density 0, since its mass is never used; the description itself is left as it was
shapeKey A value compared with !== each tick, on the room and in prediction, that changes whenever colliders would return different shapes, such as an edit count. When it changes, the colliders are updated by reshape, or made again. Bodies in contact with them are woken
reshape Updates the colliders in place after shapeKey changed, such as with collider.setVoxel, given the entity and before, a copy of the entity’s values when the colliders were last made or reshaped. It mustn’t add or remove colliders. Colliders restored from a saved world are made again instead
body Adjusts the body description before the body is made: damping, CCD, gravity scale, initial velocity
player The player whose input control gets for this entity
control Runs each tick before the world steps: on the room for every body of the kind, on a client only for the predicted entity’s. input is undefined when the player sent none for the tick, or there’s no player
character Configures the kind’s character controller once per world: slopes, steps, snapping to the ground, pushing dynamic bodies

Pose fields, by name:

2D 3D
Position, required x, y x, y, z
Rotation; without it the body doesn’t rotate angle qx, qy, qz, qw
Velocity, dynamic only vx, vy vx, vy, vz
Angular velocity, dynamic only spin wx, wy, wz

Control

interface Control<K> {
  readonly tick: number;          // the room tick being stepped, the same on the room and in a replay
  readonly entity: Entity<K>;     // write the kind's other fields as game state
  readonly body: RAPIER.RigidBody;
  readonly world: RAPIER.World;
  move(dx: number, dy: number): boolean;   // 3D: move(dx, dy, dz)
}

move asks the character controller to move a character body by up to the given distance this tick, sliding along what it hits, and returns whether the body ends on the ground. Call it once per tick; a second call replaces the first. It throws for other body types. A movement no longer than twice the controller’s normal nudge factor, 1e-4 by default, is dropped: the nudge and snapping to the ground otherwise move a character standing across an edge up and down every tick.

castRay

function castRay<T extends Pose>(ray: RAPIER.Ray, maxToi: number, targets: Iterable<T>, shape: (target: T) => RAPIER.Shape): { target: T; toi: number } | undefined;

interface Pose { readonly x: number; readonly y: number; readonly angle?: number }   // 3D: x, y, z and optional qx, qy, qz, qw

The nearest of targets that the ray hits within maxToi, each placed at its pose with the shape shape returns for it, such as new RAPIER.Capsule(0.5, 0.3), and the time of impact in units of ray.dir; undefined when it hits none. A target without rotation fields isn’t rotated. A ray that starts inside a shape hits it at 0.

It needs no Rapier world, so it checks a shot against poses from room.rewind, which are where the shooter saw the targets; see Lag compensation. It visits every target, so pass only the ones in range when there are many.

RAPIER

The Rapier module the entry loaded, for ColliderDesc, RigidBodyDesc, ray casts and the rest of Rapier’s API. Importing @dimforge/rapier2d-compat directly gives the same module.

View this page as Markdown