arcade2d
Class

PhysicsWorld

physics/physics-world.ts:122

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:

  1. PhysicsWorld.onPreUpdate (world, runs first) — drains a fixed-timestep accumulator and calls Rapier's step() zero or more times, advancing the sim using the forces and velocities set during the previous frame's update phase.
  2. 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.
  3. 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.
  4. 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

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.

readonly #
host: World

The host this component is attached to. Stored read-only; subclasses access it as this.host directly, or through the tier-appropriate aliases (this.world, this.game) on the per-tier abstract bases.

Accessors

readonly #
fixedTimeStep: number

The fixed timestep the simulation advances by, in seconds.

readonly #
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.

readonly #
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.

readonly #
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

  • _deps Record

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

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

Returns

void
ESC