PhysicsWorld
physics/physics-world.ts:122 Extends AbstractWorldComponent
The world-scoped owner of the physics simulation: a thin, opinionated
arcade2d façade over a Rapier World. Add one to a
World and every RigidBody component in that world registers
its body and colliders here, gets integrated under gravity, and collides
against its peers.
Physics is opt-in — the engine does not auto-attach a PhysicsWorld the
way it does a Scene or the input samplers. Register one through the
world's component factory, and call initPhysics first (Rapier is
WebAssembly and must be initialised before any of its objects exist).
The frame pipeline (why bodies don't lag the render)
Getting the order of "apply input → step → read back → draw" right is the
whole game with a physics integration. The engine ticks each world in three
phases — pre-update, update, post-update — and within every phase the
world-scoped components (this one) run before the object-scoped ones
(RigidBody, graphics). This PhysicsWorld slots the simulation step
into that schedule so the result is correct regardless of the order
components were added to their objects:
PhysicsWorld.onPreUpdate(world, runs first) — drains a fixed-timestep accumulator and calls Rapier'sstep()zero or more times, advancing the sim using the forces and velocities set during the previous frame's update phase.RigidBody.onPreUpdate(object, runs right after) — copies each simulated body's transform back onto its host WorldObject (position,rotation). Because this happens in pre-update, the host transform is settled before any gameplay or rendering code reads it.- gameplay
onUpdate(object) — controllers read the fresh host position and influence bodies through RigidBody methods (applyImpulse,velocity, …). Those calls reach Rapier immediately; their effect appears after the next frame's step. - graphics
onPostUpdate(object) — the existing AbstractGraphics transform-sync copies the host position into the display object. Since step 2 already ran, the visual is never a frame behind the simulation.
This is the standard fixed-timestep ("FixedUpdate") pipeline: inputs set
this frame are integrated next frame — a single, imperceptible frame of
latency — in exchange for an ordering that has no hidden dependency on
component registration order.
Units
Everything at this surface is in the engine's pixel space. Gravity is
pixels/second², body positions are the same pixels as
WorldObject.position, and collider sizes are pixels. Internally the
PhysicsWorldOptions.pixelsPerMeter option is fed to Rapier's
lengthUnit so the solver stays numerically stable at that scale without
any coordinate conversion leaking into your game code.
Collision events
After each step this component drains Rapier's contact-event queue and fans the results out to the bodies involved. A RigidBody subscribes by supplying RigidBodyOptions.onCollisionStart / RigidBodyOptions.onCollisionEnd; those callbacks fire during the body's own update phase (so transforms are already settled) with a CollisionEvent naming the other party. Bodies without a listener generate no events and incur no per-frame cost — see RigidBodyOptions.onCollisionStart for the opt-in rules and the sensor-vs-solid distinction.
Example
import { Game, initPhysics, PhysicsWorld } from '@arcade2d/engine';
const game = await Game.bootstrap({ canvas: { fill: 'window' } });
await initPhysics();
const world = game.createWorld({
components: (world) => ({
physics: () => new PhysicsWorld(world, { gravity: { x: 0, y: 980 } }),
}),
});Constructors
constructor(host: World, options: PhysicsWorldOptions): PhysicsWorld Parameters
-
hostWorld -
optionsPhysicsWorldOptions
Throws
EngineError with code
ErrorCode.PHYSICS_NOT_INITIALISED if initPhysics has not
completed — Rapier's WASM module must be ready before its World can be
constructed.
Properties
enabled: boolean Per-component gate on the three update hooks (onPreUpdate,
onUpdate, onPostUpdate). When explicitly false, the engine skips
all three for this component during the host's tick — useful for
temporarily pausing behaviour (e.g. a freeze powerup) without removing
the component and losing its internal state.
Does not gate onAdded or onDestroy; those always fire so a host can
never end up with a half-attached component.
Accessors
game: Game The Game the host world belongs to. Always non-null — the
world's game field is a mandatory construction argument, not an
option.
gravity: Readonly<Point> The world's gravity, in pixels per second squared. Returned as a fresh
Readonly Point so writing to it (which would not change the
simulation) is a compile error — assign a new value through the setter
instead.
raw: World Direct access to the underlying Rapier World instance.
Use with care. raw is an intentional escape hatch for cases the
arcade2d API doesn't cover — joints, ray/shape casts, contact-event
queues, character controllers, debug-render buffers, anything we haven't
decided how to model yet. Code that touches raw is coupled to Rapier's
public API and may break when:
- arcade2d upgrades Rapier (including minor versions).
- Rapier itself ships a breaking change.
- arcade2d swaps Rapier for a different physics engine.
None of those will be treated as breaking changes to arcade2d's own
surface. Prefer the typed methods on this component; reach for raw only
when no equivalent exists, and isolate the access behind your own helper
so the coupling is in one place.
world: World The World this component is attached to — identical to
AbstractComponent.host at this tier, exposed under the
world name so subclass code reads the same on every tier.
Methods
onAdded(_deps: Record): void Lifecycle hook that is called when the component is added to the host object. Should not be called directly.
Parameters
-
_depsRecord
Returns
void onDestroy(): void Releases the WebAssembly memory backing the Rapier world. After this the component (and any RigidBody that depended on it) is unusable — fired automatically during world teardown.
Returns
void onPreUpdate(update: WorldUpdate): void Advances the simulation. Runs in pre-update — ahead of the per-object
readback and all gameplay code — so the rest of the tick observes a
settled physics state. Real frame time is accumulated and spent in fixed
fixedTimeStep increments (capped at maxSubSteps per frame) so the
simulation is deterministic with respect to step count and independent of
display refresh rate.
Parameters
-
updateWorldUpdate
Returns
void onUpdate(_update: WorldUpdate, _deps: Record): void Lifecycle hook for the main update phase. Called once per world
tick, after every component's onPreUpdate and before any
onPostUpdate.
Parameters
-
_updateWorldUpdate -
_depsRecord
Returns
void