Phaser AE

Box2D rigid bodies

Free packv3.0.0-52DPhysicsUpdated

Full 2D rigid-body physics over a vendored Box2D v3 core, for the games where stacking, continuous collision and Box2D's exact contact solver matter: stacks that stay stacked, dominoes, ragdolls, ropes, vehicles and a thousand settling coins.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 26, 2026

Box2D rigid bodies: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

physicsBox2d

function physicsBox2d(opts: WorldOptions = {}): Pack<{ box2d: Box2dApi }, {}, {}, { box2d?: Box2dSpec }>

The physics-box2d pack: a Box2D v3 world stepped on the kernel's fixed step, with bodies bound to ordinary roster objects. Requires a 2D world. The core starts on the first create(): a development build fetches it as a separate module then, and a standalone build embeds it in game.js.

Functions

gravityToBox

function gravityToBox(g: number | { x?: number; y?: number }, ppm: Ppm): { x: number; y: number }

Gravity as the game states it (units/s², +y down) → Box2D's (m/s², +y up).

loadBox2d

async function loadBox2d(explicit?: string): Promise<B2>

Load and instantiate the core. The first call fetches the chunk; later ones return the same instance. Rejects when explicit is not on this module's origin.

shortestArc

function shortestArc(from: number, to: number): number

The shortest-arc difference between two angles, for interpolating a pose.

Without it a body crossing ±π lerps the LONG way round and visibly spins a full turn in one frame — most often on a wheel, which is exactly the thing that crosses it constantly.

Classes

Box2dWorld

class Box2dWorld

The Box2D world: bodies, joints, queries and the step.

  • invalid: number — Bodies found stale during a sync. Should stay 0.
  • static async create(load: (m?: string) => Promise<B2>, opts: WorldOptions = {}): Promise<Box2dWorld> — Load the core (first call fetches the chunk) and create a world. One world per scene is the pattern.
  • get raw(): { b2: B2; world: B2Id } — The core, for anything this wrapper does not cover. Everything it hides is hidden for a reason.
  • get count(): number — Live bodies.
  • get ppmValue(): number — World units per metre.
  • bind(target: PoseTarget, opts: BindOptions = {}, handlers: ContactHandlers = {}): Body2d — Give an object a body, built from its position and size AS THEY ARE NOW — place it first.
  • reap(): void — Destroy the body of any object that has been killed. Call it before the step, not after.
  • bodyFor(target: PoseTarget): Body2d | undefined — The body bound to an object, or undefined.
  • setGravity(g: number | { x?: number; y?: number }): void — Set world gravity, units/s², +y down.
  • step(dt: number): void — Step the world by dt, then write every dynamic body's pose back onto its object. The kernel already owns the fixed step, so there is no accumulator here — v2 carried its own because the host loop was variable. Feeding a fixed dt straight through is both simpler and the only way the simulation stays in step with everything else the frame does.
  • poseAt(target: PoseTarget, alpha: number): { x: number; y: number; rot: number } | null — The interpolated render pose for a body, alpha through the current step.
  • chain(points: ReadonlyArray<{ x: number; y: number }>, opts: ChainOptions = {}, handlers: ContactHandlers = {}): Body2d — STATIC GEOMETRY FROM A POLYLINE — terrain, a cave wall, a tube rim, a ring of arcs. A chain has no thickness and no mass, and no internal corners for a fast body to snag on, which is what makes it the right primitive for ground rather than a row of thin boxes. Two things bite anyone doing this by hand, and both are handled here: An OPEN Box2D chain treats its first and last points as GHOSTS and builds only count - 3 segments, so a raw polyline silently loses two thirds of a short one. The ends are duplicated to pay for them. A chain is ONE-SIDED, and flipping y into Box2D's frame reverses which side that is — so terrain handed over in screen order comes out solid from BELOW and everything falls through it. The points are reversed to put the solid side back on top.
  • debugDraw(vec: DebugVector, opts: { width?: number; glow?: number; alpha?: number; alpha2?: number } = {}): void — Stroke every body's COLLISION SHAPE, in its live pose, into a line-art layer. One call and the whole simulation is visible — a hull that came out the wrong shape, a sensor in the wrong place, a body that has gone to sleep. None of that is visible in the art the shapes are wearing. The colour is the state, which is the half that changes while you watch: awake, asleep, static.
  • pin(a: Body2d, b: Body2d, opts: { at?: { x: number; y: number }; limits?: [number, number]; motorSpeed?: number; maxTorque?: number } = {}): Joint2d — A HINGE at a world point: doors, see-saws, ragdoll elbows, powered wheels.
  • rope(a: Body2d, b: Body2d, opts: { atA?: { x: number; y: number }; atB?: { x: number; y: number }; length?: number; range?: [number, number]; hertz?: number; damping?: number } = {}): Joint2d — A ROPE or rod: a rigid length, range for slack, or hertz for a spring.
  • grab(b: Body2d, at: { x: number; y: number }, opts: { hertz?: number; damping?: number } = {}): Grab2d — GRAB a body at a world point and drag it. The classic mouse joint, built from what this core exposes: a tiny KINEMATIC cursor body that collides with nothing, and a zero-length SPRING between it and the grabbed body. Moving the cursor moves the target; the spring pulls the body after it while every other constraint in the world still has its say, so a grabbed crate is stopped by a wall rather than dragged through it. Teleporting the body instead — the obvious shortcut — does exactly that dragging-through, and cancels the velocity so a fast release drops the object rather than throwing it. hertz is the spring's stiffness (higher is more pin-like) and damping its ratio.
  • weld(a: Body2d, b: Body2d, at?: { x: number; y: number }): Joint2d — WELD two bodies rigidly — build a compound after the fact, glue a rider to a platform.
  • slider(a: Body2d, b: Body2d, opts: { axis?: { x: number; y: number }; limits?: [number, number]; motorSpeed?: number; maxForce?: number } = {}): Joint2d — A SLIDER along an axis: elevators, pistons, sliding doors.
  • wheel(a: Body2d, b: Body2d, opts: { axis?: { x: number; y: number }; hertz?: number; damping?: number; limits?: [number, number]; motorSpeed?: number; maxTorque?: number } = {}): Joint2d — A WHEEL: it slides along its axis on a SPRING and spins freely — a car's suspension in one joint. A rigid pin axle fights every torque and judders; a wheel joint lets the chassis float above the wheels so bumps are absorbed and the body stays level. It is the way to build a car.
  • raycast(from: { x: number; y: number }, to: { x: number; y: number }): RayHit | null — The nearest body along a ray, or null.
  • explosion(at: { x: number; y: number }, radius: number, strength: number): void — A radial blast: an outward impulse on every dynamic body in range, falling off with distance.
  • destroy(): void — Tear the world down and free the core's memory.

FilterNames

class FilterNames

Names → bits, allocated on first use. At most 32 groups, because a mask is 32 bits and pretending otherwise silently drops collisions.

  • bit(name: string): number — The category bit for a group name.
  • family(name: string): number — The NEGATIVE group index for a family; the same index on two shapes means they never collide.
  • get names(): ReadonlyArray<string> — Every group name allocated so far, in allocation order.
  • resolve(group?: string, hits?: ReadonlyArray<string>, family?: string): Filter — Resolve a shape's filter. No group is category 1; no hits list means it hits everything.

Constants

flipAngle

const flipAngle = (a: number): number=> …

A screen-clockwise angle → Box2D's counter-clockwise one, and back — the same negation either way.

fromBox

const fromBox = (m: number, ppm: Ppm): number=> …

A length in metres → world units.

pointFromBox

const pointFromBox = (x: number, y: number, ppm: Ppm): { x: number; y: number }=> …

A Box2D point → the world's, flipping y back.

pointToBox

const pointToBox = (x: number, y: number, ppm: Ppm): { x: number; y: number }=> …

A world point → Box2D's, flipping y.

toBox

const toBox = (px: number, ppm: Ppm): number=> …

A length in world units → metres.

Types

B2

type B2 = Record<string, never> & Record<string, unknown>

The core module's surface is the vendored embind build's own; this wrapper is the contract, not the types.

B2Id

interface B2Id

A Box2D id — a plain value object, cheap to copy, nothing to free.

  • index1: number — Slot of the object in its world's table, counted from 1 (0 means none).
  • world0?: number — Index of the world that owns it, counted from 0; Box2D fills it in and the wrapper never reads it.
  • generation: number — Reuse counter of the slot: an id whose generation no longer matches refers to a destroyed object.

BindOptions

interface BindOptions

How an object becomes a body.

  • type?: BodyType — Default 'dynamic'.
  • shape?: ShapeKind — Default 'auto' — a box from the object's width and height.
  • cornerRadius?: number — Rounded box corners in world units, kept inside the specified width/height.
  • radius?: number — Circle or capsule radius, world units. Default half the smaller side.
  • points?: ReadonlyArray<{ x: number; y: number }> — With shape: 'polygon' or 'decompose', the outline in the object's LOCAL frame, world units. polygon runs it through convexHull and simplifyHull first, because Box2D polygons are convex and capped at eight vertices — an eleven-point asteroid handed over raw comes back a null shape with no error at all. That approximation is right for a rock and wrong for anything whose concavity is the point: decompose splits the outline into convex pieces and attaches each as its own polygon on the same body, so a ship, a plus, a letter or a shape a player drew keeps its notch.
  • density?: number — Mass per unit area. Default 1.
  • mass?: number — Exact body mass in kilograms. When set, inertia is scaled with it after all shapes are attached.
  • friction?: number — Surface friction, 0..1-ish. Default 0.6.
  • bounce?: number — Restitution: 0 is a beanbag, 1 a superball. Default 0.
  • damping?: number — Linear damping — air resistance. Default 0.
  • angularDamping?: number — Angular damping. Default 0.05.
  • gravityScale?: number — Multiply gravity for this body alone: 0 floats, 2 falls hard. Default 1.
  • fixedRotation?: boolean — Lock the rotation — what a platformer character almost always wants. Default false.
  • bullet?: boolean — CONTINUOUS collision for this body. A fast small body otherwise passes THROUGH a thin wall between two steps and nothing about the frame looks wrong. Costs more; use it on bullets, not crates.
  • sensor?: boolean — A sensor reports overlaps and stops nothing — a trigger, a pickup, a goal.
  • group?: string — Collision group name.
  • hits?: ReadonlyArray<string> — The group names this shape may hit; omitted, it hits everything.
  • family?: string — A family: every shape sharing one ignores every other, whatever the masks say. A ragdoll's own limbs.
  • balance?: { x: number; y: number } — Shift the centre of mass off the shape's centre, world units — bottom-heavy things do not tip.

Body2d

interface Body2d

A live body.

  • readonly handle: unknown — The object it drives.
  • readonly type: BodyType — What it is made of.
  • readonly alive: boolean — False once destroyed.
  • velocity: { x: number; y: number } — Velocity in world units/s.
  • spin: number — Angular velocity, radians/s, screen-clockwise.
  • readonly mass: number — Mass in kg, from the shape and density.
  • readonly inertia: number — Rotational inertia — what torque scales by, as mass is what force scales by.
  • impulse(v: { x?: number; y?: number }, at?: { x: number; y: number }): void — An instant kick, units/s × mass, optionally at a world point (which also spins it).
  • force(v: { x?: number; y?: number }): void — A continuous push while you keep calling it: an acceleration, in world units/s², not raw newtons. Scaled by the body's own mass, so force({ y: -600 }) lifts a pebble and a chassis at the same rate and the number keeps its meaning when you change a density.
  • torque(t: number): void — A twist: an angular acceleration, in rad/s², screen-clockwise positive. Scaled by the body's own rotational inertia, exactly as force is scaled by its mass — so torque(4) reaches 4 rad/s in a second (less whatever angularDamping takes back) on anything. Raw Box2D torque is in kg·m², and a small sprite's inertia is a fraction of one, so an unscaled value that looks modest spins a light body into a blur.
  • teleport(x: number, y: number, angle?: number): void — Move it outright, cancelling velocity. The ONLY correct hard move.
  • wake(): void — Wake it, so a settled body responds to a change.
  • readonly awake: boolean — Is the solver still thinking about it? A settled pile goes to sleep, and that is the solver doing its job.
  • destroy(): void — Remove it from the world.

BodyType

type BodyType = 'dynamic' | 'static' | 'kinematic'

What a body is made of.

Box2dApi

interface Box2dApi

game.box2d.

  • create(opts?: WorldOptions): Promise<Box2dWorld> — Load the core and create a world. One per scene is the pattern; the pack steps whatever is current and drops it when the scene exits.
  • readonly world: Box2dWorld | null — The current world, or null before create().
  • bind(h: Handle, opts?: BindOptions, handlers?: ContactHandlers): Body2d — Give an object a body in the current world. Throws before create().
  • bodyFor(h: Handle): Body2d | undefined — The body bound to an object, or undefined.
  • gravity(g: number | { x?: number; y?: number }): void — Set world gravity, units/s², +y down.
  • pin(a: Body2d, b: Body2d, opts?: Parameters<Box2dWorld['pin']>[2]): ReturnType<Box2dWorld['pin']> — A HINGE between two bodies.
  • rope(a: Body2d, b: Body2d, opts?: Parameters<Box2dWorld['rope']>[2]): ReturnType<Box2dWorld['rope']> — A ROPE, rod or spring.
  • weld(a: Body2d, b: Body2d, at?: { x: number; y: number }): ReturnType<Box2dWorld['weld']> — WELD two bodies rigidly.
  • slider(a: Body2d, b: Body2d, opts?: Parameters<Box2dWorld['slider']>[2]): ReturnType<Box2dWorld['slider']> — A SLIDER along an axis.
  • wheel(a: Body2d, b: Body2d, opts?: Parameters<Box2dWorld['wheel']>[2]): ReturnType<Box2dWorld['wheel']> — A sprung WHEEL — the way to build a car.
  • raycast(from: { x: number; y: number }, to: { x: number; y: number }): ReturnType<Box2dWorld['raycast']> — The nearest body along a ray, or null.
  • explosion(at: { x: number; y: number }, radius: number, strength: number): void — A radial blast on every dynamic body in range.
  • grab(b: Body2d, at: { x: number; y: number }, opts?: Parameters<Box2dWorld['grab']>[2]): ReturnType<Box2dWorld['grab']> — GRAB a body and drag it — the mouse joint. A spring to a moving point, not a teleport.

Box2dSpec

type Box2dSpec = BindOptions & ContactHandlers

What a spawn spec's box2d key takes: the bind options plus the contact handlers.

ChainOptions

interface ChainOptions

How a chain is built.

  • loop?: boolean — Close it, so there are no ends. Default false.
  • friction?: number — Surface friction. Default 0.6.
  • bounce?: number — Surface restitution. Default 0.
  • group?: string — Its collision group.
  • hits?: readonly string[] — The groups it collides with.

ContactHandlers

interface ContactHandlers

Contact callbacks a bound object may carry.

  • onHit?: (other: Body2d, speed: number) => void — A hard hit, with the approach speed in world units/s.
  • onTouch?: (other: Body2d) => void — Touch began.
  • onRelease?: (other: Body2d) => void — Touch ended.
  • onEnter?: (other: Body2d) => void — Something entered this sensor.
  • onExit?: (other: Body2d) => void — Something left this sensor.

DebugShape

interface DebugShape

A body's collision shape in WORLD units, kept so a debug view can draw what is really being simulated.

  • kind: 'box' | 'circle' | 'capsule' | 'chain' — What to draw; a chain is world geometry with no pose of its own.
  • w: number — Full width in world units (0 for a chain).
  • h: number — Full height in world units (0 for a chain).
  • r: number — Circle or capsule radius in world units (0 for a chain).
  • loop?: boolean — For a chain: whether the last point joins back to the first.
  • points?: ReadonlyArray<{ x: number; y: number }> — An outline in the body's local frame (a polygon or decomposed shape), or a chain's points in world units.

DebugVector

interface DebugVector

What debugDraw needs of a line-art layer. Structural on purpose: this pack does not depend on one.

  • seg(x1: number, y1: number, x2: number, y2: number, style?: object): void — Stroke a line segment from (x1, y1) to (x2, y2) in world units.
  • poly(points: ReadonlyArray<{ x: number; y: number }>, style?: object): void — Stroke a closed or open polyline through the points, in world units.
  • arc(cx: number, cy: number, r: number, from: number, to: number, style?: object): void — Stroke an arc of radius r about (cx, cy), from angle from to angle to in radians.

Filter

interface Filter

The resolved filter for a shape.

  • category: number — Category bits: the one group bit this shape belongs to (1 when it names no group).
  • mask: number — Category bits this shape collides with; all 32 bits when it names no hits list.
  • group: number — Box2D group index: negative for a family (its members never collide with each other), 0 for none.

Grab2d

interface Grab2d

A live GRAB — a soft constraint dragging a body toward a point you keep moving. It is what a mouse-drag on a physics object is: not a teleport, which would put the body wherever the cursor is and let it pass through walls, but a SPRING to the cursor that the rest of the world still argues with.

  • moveTo(p: { x: number; y: number }): void — Move the target point.
  • release(): void — Let go. The body keeps the velocity it had, so a fast release throws it.
  • readonly alive: boolean — False once released.

Joint2d

interface Joint2d

A joint between two bodies.

  • readonly alive: boolean — False once destroyed.
  • motor(speed: number, effort?: number): void — Drive it: an angular speed for a hinge or wheel, a linear one for a slider. effort caps the torque or force.
  • destroy(): void — Remove it.

PoseTarget

interface PoseTarget

What a bound object must look like: a centred pose the wrapper reads and writes.

  • x: number — Centre x in world units.
  • y: number — Centre y in world units, +y down.
  • rot: number — Rotation in radians, clockwise on screen.
  • width: number — Width in world units; sizes the default box shape.
  • height: number — Height in world units; sizes the default box shape.
  • alive?: boolean — Set false by the game to retire the object: its body is destroyed before the next step. Default true.

Ppm

type Ppm = number

World units per simulated metre. 32 is the usual sprite-scale default.

RayHit

interface RayHit

What a raycast found.

  • body: Body2d — The body hit.
  • point: { x: number; y: number } — Where, in world units.
  • normal: { x: number; y: number } — The surface normal there, in world (y-down) space.
  • distance: number — Distance from the ray's start, world units.

ShapeKind

type ShapeKind = 'auto' | 'box' | 'circle' | 'capsule' | 'polygon' | 'decompose'

The collision shape. auto is a box from the object's size.

WorldOptions

interface WorldOptions

How a world is created.

  • gravity?: number | { x?: number; y?: number } — Gravity in world units/s², +y down. Default 900.
  • pixelsPerMeter?: number — World units per simulated metre. Default 32.
  • substeps?: number — Solver sub-steps a step. Default 4.
  • module?: string — Override the core module URL in a development build, on the game's own origin; another origin throws.