Collisions & triggers
The collision system
Section titled “The collision system”createCollisionSystem(rapierWorld, eventQueue)
(packages/engine/src/physics/collision-system.ts) drains Rapier’s event queue once per
tick, in postPhysics. Register a handler:
const collisions = createCollisionSystem(rapierWorld, eventQueue);collisions.onCollision((handleA, handleB, started) => { // started === true on contact begin, false on contact end});// each postPhysics: collisions.drain();A handler receives the two collider handles and a started boolean. Events are
buffered first, then handlers run after the drain — so it’s safe to create/destroy bodies
inside a handler without corrupting Rapier’s iteration. Resolve a handle back to its entity
with physics.getByColliderHandle(handle) (see the registry).
Triggers are sensor overlaps, handled in your own code
Section titled “Triggers are sensor overlaps, handled in your own code”A sensor collider (Rapier’s setSensor(true)) overlaps without blocking, and its
overlap events arrive through the same collision handler as everything else. There is no
engine-side callback framework in between: your world code registers one handler, resolves
the handles, and branches on whether a sensor was involved.
collisions.onCollision((a, b, started) => { const nodeA = physics.getByColliderHandle(a); const nodeB = physics.getByColliderHandle(b); if (!nodeA || !nodeB) return; if (started) onZoneEnter(nodeA, nodeB); else onZoneExit(nodeA, nodeB);});In an R3F world this registration lives in a component’s mount effect (with the unsubscribe in its cleanup), so a zone’s enter/exit logic sits beside the mesh and collider it belongs to. Collision worlds never span roots — both nodes of an overlap always belong to the same world.
See also
Section titled “See also”- Colliders —
isSensorand collision layers. - Entities & behavior — where world code lives.
- Rigid bodies — the registry that resolves handles to entities.