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 forroom.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, andentityOf(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.savereturns bytes when the world has a dynamic body: the Rapier world and which entity each body belongs to.loadrestores them and reconciles with the entities. Without dynamic bodies,savereturns undefined andloadmakes every body from its entity.releasefrees 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.