Skip to content

Physics ​

kiln/physics wraps Rapier 0.21: Rapier 3D by default, Rapier 2D when the app is mode: '2d'. Bodies are synced to Transform, and the simulation steps in Fixed.

ts
import { PhysicsPlugin, RigidBody, Collider } from 'kiln/physics'

app.use(CorePlugins, PhysicsPlugin)

commands.spawn(Transform({ y: 5 }), RigidBody, Collider({ shape: 'ball', radius: 0.5 }))           // falls
commands.spawn(Transform, RigidBody({ type: 'fixed' }), Collider({ hx: 10, hy: 0.5, hz: 10 }))   // the ground

The plugin loads Rapier's WASM asynchronously. Scenario tests, the CLI and the browser runtime wait for it; code that builds an app itself calls await app.ready() first.

Traits ​

RigidBody:

FieldDefault
type'dynamic''dynamic' (simulated), 'kinematic' (follows the Transform you write), 'fixed'
linearDamping, angularDamping0
gravityScale1
ccdfalseContinuous collision detection, for fast bodies
lockRotationsfalseKeep the body upright (characters)
canSleeptrue

Collider:

FieldDefault
shape'box''box', 'ball', 'capsule', 'cylinder'
hx, hy, hz0.5Box half-extents
radius0.5Ball, capsule, cylinder
halfHeight0.5Capsule, cylinder
friction0.5
restitution0Bounciness
density1
sensorfalseReport overlaps without pushing

A Collider on an entity with RigidBody attaches to that body. On a child (ChildOf a body entity) it attaches with the child's Transform as its offset, which is how compound shapes are built. On its own it is static.

LinearVelocity and AngularVelocity (x y z; 2D uses z for angular) are written back after every step. Write LinearVelocity to drive a body.

The Physics resource ​

ts
const physics = res.mut(Physics)
physics.applyImpulse(entity, { x: 0, y: 5, z: 0 })
physics.setVelocity(entity, { x: 2, y: 0, z: 0 })
const hit = physics.raycast(origin, direction, maxDist, excludeEntity)   // { entity, distance, point, normal } | null

Events ​

CollisionStarted and CollisionEnded carry { a, b, sensor }. Sensors report with sensor: true.

ts
for (const { a, b } of events.read(CollisionStarted)) {
  if (a.has(IsPlayer) && b.has(IsCoin)) commands.despawn(b)
}

Ordering ​

The plugin runs four systems in Fixed:

CreateBodies → PushState → StepPhysics → WriteBackTransforms

Systems that write Transform or LinearVelocity for bodies (movement input, teleports, kinematic targets) go .before(CreateBodies). Systems that react to the new positions go .after(WriteBackTransforms):

ts
.add(Fixed, PlayerMove.before(CreateBodies), Landing.after(WriteBackTransforms))

Options ​

physicsPlugin({ gravity }) in place of PhysicsPlugin. Gravity defaults to [0, -9.81, 0] in 3D and [0, -9.81] in 2D.

Determinism ​

Rapier is deterministic on one machine and runs the same WASM in the browser and under Bun, which usually matches. Cross-machine determinism isn't guaranteed, and long physics-heavy replays can drift. See Determinism.