API reference

Every public class, method, and event in @codexo/exojs. Generated from source.

C

classPhysicsWorld

@codexo/exojs-physics / physics / stable

The collision/query world: owns bodies, colliders, the detection backend, bindings, the query engine and the fixed-step accumulator. Stepped by the caller (commonly from a `Scene.update`), each fixed sub-step it integrates body velocities, runs broad- and narrow-phase detection, solves contacts and integrates positions, then fires immutable contact/sensor events and writes bound node transforms. It holds **no module-level state**, so any number of worlds run in isolation. The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft step"): each fixed step runs detection once, then several sub-steps, each integrating gravity over the sub-step and solving contacts with a soft position bias plus a bias-free relax pass; a 2-point block normal solve propagates stack loads, and restitution is a separate final pass. Decoupling stiffness from the iteration count keeps tall towers stable. The detection backend sits behind an internal seam, so the solver is swappable without touching this public surface. **Operating envelope.** The soft solver trades a little accuracy for robustness, so it has a few documented limits - each stays finite and stable, and each is pinned by a gate in `dynamics.test.ts` or `contact-push-out.test.ts`: - **Resting contacts** settle within a small, fixed tolerance. A face contact settles at exactly that tolerance; a single-point contact settles slightly deeper, and that gap grows with acceleration. Very high accelerations eventually exceed what the push-out can resolve within a step, at which point the contact holds a slightly deeper resting depth instead of the tolerance - keep gravity in the range ordinary 2D games use. - **Mass ratio** - contacts between bodies of very different mass degrade gradually rather than at a fixed ratio. What sets the practical limit is how thick the supporting geometry is relative to the lighter body, not the ratio alone: a light body squeezed against a boundary thinner than itself is the case that fails first, and it fails by sinking through. - **Scale-relative behaviour** - the solver's tolerances are absolute lengths, so a very small shape sees them as a large fraction of itself, and a very large one as a negligible one. Keep the shapes of one world within a couple of orders of magnitude of each other. - **CCD is opt-in and translation-only** - detection runs once per fixed step, so an ordinary body that crosses more than roughly half a barrier's total thickness within one step may end up on the wrong side of it, either passing through or being resolved out of the far face. Flag fast projectiles with PhysicsBody.isBullet: each of the body's colliders is then shape-cast along the step's motion (an exact translation-only sweep of the full shape, not just the centre) and clamped at the first impact. Rotation over the step is not swept - a long body spinning fast enough to sweep past an obstacle within one step can still miss it. - **PhysicsWorldOptions.subStepCount** - the default `4` is load-bearing for tall-stack stability; lowering it below `2` visibly degrades stacking, so do not reduce it for performance. - **Broad phase is a dynamic AABB tree** (`AabbTreeBroadPhase`), stateful across fixed steps: a collider whose tight AABB stays inside its stored fat AABB costs nothing to re-sync, and only colliders that actually move outside their margin trigger a tree update and a local re-query for new neighbours. Detection still walks every live collider once per step (a cheap containment check for each), so there is a small linear floor, but the dominant cost - reinsertion and neighbour discovery - is driven by how much actually moved, not by the total live collider count; sleeping bodies skip that dominant cost entirely. Scales to tens of thousands of simultaneously-live colliders, including dense clusters that would degrade a sort-and-sweep broad phase; very large or highly dynamic worlds may still benefit from splitting into several smaller `PhysicsWorld` instances (e.g. per room/chunk).

16
props
18
methods
4
events
Import
import { PhysicsWorld } from '@codexo/exojs-physics'

The collision/query world: owns bodies, colliders, the detection backend, bindings, the query engine and the fixed-step accumulator. Stepped by the caller (commonly from a `Scene.update`), each fixed sub-step it integrates body velocities, runs broad- and narrow-phase detection, solves contacts and integrates positions, then fires immutable contact/sensor events and writes bound node transforms. It holds **no module-level state**, so any number of worlds run in isolation.

The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft step"): each fixed step runs detection once, then several sub-steps, each integrating gravity over the sub-step and solving contacts with a soft position bias plus a bias-free relax pass; a 2-point block normal solve propagates stack loads, and restitution is a separate final pass. Decoupling stiffness from the iteration count keeps tall towers stable. The detection backend sits behind an internal seam, so the solver is swappable without touching this public surface.

**Operating envelope.** The soft solver trades a little accuracy for robustness, so it has a few documented limits - each stays finite and stable, and each is pinned by a gate in `dynamics.test.ts` or `contact-push-out.test.ts`: - **Resting contacts** settle within a small, fixed tolerance. A face contact settles at exactly that tolerance; a single-point contact settles slightly deeper, and that gap grows with acceleration. Very high accelerations eventually exceed what the push-out can resolve within a step, at which point the contact holds a slightly deeper resting depth instead of the tolerance - keep gravity in the range ordinary 2D games use. - **Mass ratio** - contacts between bodies of very different mass degrade gradually rather than at a fixed ratio. What sets the practical limit is how thick the supporting geometry is relative to the lighter body, not the ratio alone: a light body squeezed against a boundary thinner than itself is the case that fails first, and it fails by sinking through. - **Scale-relative behaviour** - the solver's tolerances are absolute lengths, so a very small shape sees them as a large fraction of itself, and a very large one as a negligible one. Keep the shapes of one world within a couple of orders of magnitude of each other. - **CCD is opt-in and translation-only** - detection runs once per fixed step, so an ordinary body that crosses more than roughly half a barrier's total thickness within one step may end up on the wrong side of it, either passing through or being resolved out of the far face. Flag fast projectiles with PhysicsBody.isBullet: each of the body's colliders is then shape-cast along the step's motion (an exact translation-only sweep of the full shape, not just the centre) and clamped at the first impact. Rotation over the step is not swept - a long body spinning fast enough to sweep past an obstacle within one step can still miss it. - **PhysicsWorldOptions.subStepCount** - the default `4` is load-bearing for tall-stack stability; lowering it below `2` visibly degrades stacking, so do not reduce it for performance. - **Broad phase is a dynamic AABB tree** (`AabbTreeBroadPhase`), stateful across fixed steps: a collider whose tight AABB stays inside its stored fat AABB costs nothing to re-sync, and only colliders that actually move outside their margin trigger a tree update and a local re-query for new neighbours. Detection still walks every live collider once per step (a cheap containment check for each), so there is a small linear floor, but the dominant cost - reinsertion and neighbour discovery - is driven by how much actually moved, not by the total live collider count; sleeping bodies skip that dominant cost entirely. Scales to tens of thousands of simultaneously-live colliders, including dense clusters that would degrade a sort-and-sweep broad phase; very large or highly dynamic worlds may still benefit from splitting into several smaller `PhysicsWorld` instances (e.g. per room/chunk).

Constructors1
Methods18
Add a body to the world: allocates the body and its collider ids, registers the colliders, computes the mass model and tracks the body for stepping. Construct the body freely first (new PhysicsBody({ ... })), then add it. Safe to call inside an event callback - the body push is deferred to the end of the step, exactly like collider registration. Returns the body.
addJoint(joint: T): T
Add a constraint joint. Construct it first (new DistanceJoint({ ... })), then add it. Wakes both bodies; safe inside a callback (registration is deferred). Returns the joint.
Convenience: create a body carrying a single collider, add it to the world and bind it to node in one call. The node tracks body.position after each step. Returns the body. Equivalent to new PhysicsBody(...) + add + bind. When options.position/options.angle are omitted, the body starts at node's current WORLD position/rotation (via getWorldTransform(), composed through any transform-group boundary) rather than (0, 0) - otherwise a body attached to an already-placed node would visibly "teleport" to the origin on the next step. Pass position/angle explicitly to override.
destroy(): void
Release every body, collider, binding and backend resource.
Destroy a body and its colliders. Every dynamic body touching it is woken, so anything that was resting on the destroyed body falls once its support is gone. Deferred when called inside a callback.
Destroy a single collider, recomputing its body's mass. Wakes every dynamic body touching it, the same way destroyBody does. Deferred when called inside a callback.
Advance by exactly one fixed step, driven directly by the engine's own fixed-timestep scheduler when this world is registered as a System (app.systems.add(world, { order: SystemOrder.Physics }) or the scene equivalent) - bypasses timeStepper's variable-delta accumulator entirely, since the caller has already decided exactly when a fixed step occurs. Prefer this over manual step once the world is system-registered; step remains available for advanced manual driving. Always advances by exactly timeStepper's configured fixedDelta, regardless of the caller's actual fixed-step interval - if the engine's Application.fixedTimeStep doesn't match this world's fixedDelta, the simulation stays deterministic but runs at the wrong wall-clock speed relative to real time.
rayCast(origin: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, direction: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, filter?: Partial<{ category: number; group: number; mask: number }>, maxDistance?: number): RayHitA single ray-cast intersection. | null
Nearest collider hit by the ray, or null.
rayCastAll(origin: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, direction: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, filter?: Partial<{ category: number; group: number; mask: number }>, out?: RayHitA single ray-cast intersection.[], maxDistance?: number): RayHitA single ray-cast intersection.[]
All collider hits along the ray, sorted by distance. Writes into out (cleared) if given.
Remove a joint, waking both bodies so they respond to the lost constraint. Deferred when called inside a callback.
step(frameDeltaSeconds: number): void
Advance the world by frameDeltaSeconds. Accumulates into fixed steps; each fixed step runs detection once, then a TGS-Soft sub-step loop (integrate gravity, solve contacts with a soft bias, integrate positions, relax) and a restitution pass, then writes the accumulated motion into each body. Finally dispatches events and writes bound node transforms. **Prefer registering the world as a system instead** (app.systems.add(world, { order: SystemOrder.Physics }) or the scene equivalent) so fixedUpdate drives stepping directly from the engine's own fixed-timestep scheduler - one call per fixed step, no accumulator duplication. step remains available here for manual/advanced driving (e.g. a fixed-but-non-standard rate, or stepping outside the normal frame loop entirely): pass any delta and this accumulator converts it into the right number of fixed sub-steps (0, 1, or several, clamped by PhysicsWorldOptions.maxSubSteps). Either way the simulation is frame-rate independent and deterministic - the same sequence of deltas replays identically. timeStepper.alpha ([0, 1)) is the leftover sub-step fraction after this call, for callers that want to interpolate a bound node's rendered position between the last two fixed states instead of snapping to the latest one (bindings do not do this automatically - bind always writes the latest fixed-step transform verbatim).
Variable-rate System phase: places bound nodes for the frame that is about to be drawn. Only does work while interpolation is on - otherwise fixedUpdate has already snapped them to the latest fixed state.
Properties16
contactHertz: number
Soft-contact stiffness in Hz.
Runs once per solid contact per fixed step, after contact generation and before island building and the solver, so a contact it disables applies no impulse and does not couple its two bodies into one sleeping island. At most one modifier per world - it mutates simulation state, so a multi-listener signal would make the outcome depend on registration order. Set to null to remove it; the per-contact values then stay at the defaults derived from the two colliders.
dampingRatio: number
Soft-contact damping ratio.
enableSleeping: boolean
Whether resting bodies are put to sleep.
frameAlphaSource: () => number
Supplies the [0, 1) blend factor for interpolated bindings. Defaults to this world's own accumulator; point it at the host's own fraction (() => app.frameAlpha) when the world runs as a System, because fixedUpdate never advances the local accumulator.
interpolation: boolean
Whether bound nodes are placed between the last two fixed states rather than snapped to the latest one. Toggleable at runtime; switching it off snaps every bound node back to the current fixed state on the next sync.
sleepAngularVelocity: number
Angular sleep threshold (rad/s).
sleepLinearVelocity: number
Linear sleep threshold (px/s).
subStepCount: number
TGS-Soft sub-steps per fixed step.
timeToSleep: number
Seconds below the thresholds before a body sleeps.
backend: PhysicsBackend
The detection backend (internal; consumed by the debug draw layer).
Events4
Source