Physics
Web Engine Dev provides physics simulation through an interface-first design. Abstract physics packages define the API, while adapter packages provide concrete implementations backed by physics engines like Rapier.
Architecture
The physics system follows the engine's Hexagonal Architecture pattern:
Interface packages (define the API):
@web-engine-dev/physics2d
@web-engine-dev/physics3d
Adapter packages (provide implementations):
@web-engine-dev/physics2d-rapier
@web-engine-dev/physics3d-rapierYour game code depends on the interface packages. Swap backends by changing only the adapter -- no gameplay code needs to change.
2D Physics
The 2D physics package (@web-engine-dev/physics2d) provides rigid body dynamics, collision detection, and constraints. Its API design is inspired by Box2D.
Physics World
The physics world is the top-level container for all simulation:
import { PhysicsWorld2D } from '@web-engine-dev/physics2d';
const world = new PhysicsWorld2D({
gravity: { x: 0, y: -9.81 },
allowSleep: true,
});Body Types
| Type | Description | Use Case |
|---|---|---|
static | Immovable, infinite mass | Ground, walls, static level geometry |
dynamic | Fully simulated, responds to forces and collisions | Players, projectiles, physics objects |
kinematic | User-controlled velocity, not affected by forces | Moving platforms, doors, elevators |
// Create a dynamic body
const player = world.createBody({
type: 'dynamic',
position: { x: 0, y: 10 },
linearDamping: 0.1,
bullet: true, // Enable CCD for fast-moving objects
});
// Create static ground
const ground = world.createBody({
type: 'static',
position: { x: 0, y: 0 },
});Shapes and Fixtures
Shapes define the collision geometry. Attach them to bodies as fixtures with physical properties:
// Attach a circle shape
player.createFixture({
shape: { type: 'circle', radius: 0.5 },
density: 1.0, // kg/m^2 -- affects mass
friction: 0.3, // Tangential resistance (0-1)
restitution: 0.5, // Bounciness (0-1)
});
// Attach a box shape
ground.createFixture({
shape: { type: 'box', width: 20, height: 1 },
density: 0, // Static bodies have zero density
});Available 2D shape types:
| Shape | Description |
|---|---|
circle | Simple and efficient |
polygon | Convex only, max 8 vertices |
box | Convenience for axis-aligned rectangles |
edge | Line segment (one-sided or two-sided) |
chain | Connected edges for terrain |
Forces and Impulses
| Method | Effect | Use Case |
|---|---|---|
applyForce() | Continuous acceleration (applied over time) | Thrust, wind, gravity |
applyLinearImpulseToCenter() | Instant velocity change | Explosions, jumps |
applyTorque() | Continuous rotation | Spinning |
// Continuous force (apply every frame)
body.applyForceToCenter({ x: 0, y: 100 });
// Instant velocity change
body.applyLinearImpulseToCenter({ x: 0, y: 50 });
// Force at a point (creates torque)
body.applyForce({ x: 100, y: 0 }, body.getWorldPoint({ x: 0, y: 1 }));Collision Events
React to collisions through the contact listener:
world.setContactListener({
beginContact(contact) {
const a = contact.fixtureA.body;
const b = contact.fixtureB.body;
// Collision started
},
endContact(contact) {
// Collision ended
},
preSolve(contact, oldManifold) {
// Modify contact before solving
// Example: one-way platform
const normal = contact.getWorldManifold().normal;
if (normal.y < 0.5) {
contact.setEnabled(false);
}
},
postSolve(contact, impulse) {
// React to collision intensity
const totalImpulse = impulse.normalImpulses.reduce((a, b) => a + b, 0);
if (totalImpulse > 10) {
// Play impact sound, spawn particles
}
},
});The ECS integration also provides event-based collision notifications:
import {
OnCollisionBegin,
OnCollisionEnd,
OnTriggerEnter,
OnTriggerExit,
} from '@web-engine-dev/physics2d';Collision Filtering
Control which fixtures collide using category and mask bits:
body.createFixture({
shape: { type: 'box', width: 1, height: 1 },
filter: {
categoryBits: 0x0002, // What I am
maskBits: 0x0001, // What I collide with
groupIndex: 0, // Override (positive = always collide, negative = never)
},
});Two fixtures A and B collide if: (A.categoryBits & B.maskBits) !== 0 AND (B.categoryBits & A.maskBits) !== 0.
2D Joints
Constrain two bodies together:
| Joint | Description | Common Use |
|---|---|---|
revolute | Hinge/pivot point | Doors, wheels |
prismatic | Slider along an axis | Pistons, rails |
distance | Fixed or spring distance | Ropes, chains |
weld | Rigid connection | Compound objects |
wheel | Revolute + prismatic | Vehicle suspension |
mouse | Drag body to target point | Click-to-drag |
friction | Resist relative motion | Top-down friction |
motor | Apply forces to reach target offset | Animated mechanisms |
world.createJoint({
type: 'revolute',
bodyA: wheel,
bodyB: chassis,
localAnchorA: { x: 0, y: 0 },
localAnchorB: { x: -1.5, y: -0.5 },
enableMotor: true,
motorSpeed: 10,
maxMotorTorque: 100,
});Raycasting and Queries
// Cast a ray and get the first hit
const hit = world.rayCastFirst(start, end);
if (hit) {
console.log(hit.fixture, hit.point, hit.normal, hit.fraction);
}
// Cast a ray and get all hits
const hits = world.rayCastAll(start, end);
// Query all fixtures overlapping an AABB
world.queryAABB(aabb, (fixture) => {
return true; // Return true to continue, false to stop
});
// Query all fixtures containing a point
const overlapping = world.queryPoint({ x: 5, y: 5 });Stepping the Simulation
const fixedTimeStep = 1 / 60;
function update(deltaTime: number) {
world.step(fixedTimeStep, 8, 3);
// velocityIterations: 8 (more = stable stacks)
// positionIterations: 3 (more = less penetration)
world.clearForces();
}3D Physics
The 3D physics package (@web-engine-dev/physics3d) mirrors the 2D API for 3D rigid body dynamics.
Creating a 3D Physics World
import { PhysicsWorld3D } from '@web-engine-dev/physics3d';
const world = new PhysicsWorld3D({
gravity: { x: 0, y: -9.81, z: 0 },
allowSleep: true,
solverIterations: 10,
});3D Bodies and Colliders
Bodies use the same static/dynamic/kinematic model. Shapes are attached as colliders:
// Dynamic body with a sphere collider
const ball = world.createBody({
type: 'dynamic',
position: { x: 0, y: 10, z: 0 },
continuousCollisionDetection: true,
});
ball.addCollider({
shape: { type: 'sphere', radius: 0.5 },
density: 1.0,
friction: 0.5,
restitution: 0.3,
});
// Static ground with a box collider
const ground = world.createBody({
type: 'static',
position: { x: 0, y: 0, z: 0 },
});
ground.addCollider({
shape: { type: 'box', halfExtents: { x: 50, y: 0.5, z: 50 } },
});3D Shape Types
| Shape | Description |
|---|---|
sphere | Radius-based sphere |
box | Half-extents per axis |
capsule | Radius + height, configurable axis (x/y/z) |
cylinder | Radius + height |
cone | Radius + height |
convexHull | From vertex array |
mesh | Triangle mesh (static bodies only) |
heightfield | Terrain from height data |
compound | Multiple child shapes |
3D Joints
| Joint | Description | Common Use |
|---|---|---|
fixed | Welds two bodies | Compound objects |
hinge | Single-axis rotation | Doors, wheels |
ballSocket | Free rotation with limits | Ragdoll shoulders |
slider | Linear motion along axis | Pistons, rails |
distance | Min/max distance | Ropes, chains |
spring | Distance with spring behavior | Suspension |
cone | Cone twist constraint | Ragdoll spine |
sixDof | Full 6 degrees of freedom | Complex articulations |
gear | Links rotation of two hinges | Gear trains |
motor | Driven position/velocity | Robots, mechanisms |
3D Raycasting
const hit = world.raycast(
{ x: 0, y: 10, z: 0 }, // origin
{ x: 0, y: -1, z: 0 }, // direction (normalized)
{ maxDistance: 100 }, // options
);
if (hit) {
console.log(hit.point, hit.normal, hit.distance, hit.body);
}Physics Materials
Both 2D and 3D packages provide preset materials:
// 2D
import { createDefaultMaterial, createBouncyMaterial } from '@web-engine-dev/physics2d';
// 3D
import {
createDefaultMaterial3D, // friction: 0.5, restitution: 0.0
createBouncyMaterial3D, // friction: 0.3, restitution: 0.9
createSlipperyMaterial3D, // friction: 0.02, restitution: 0.1
createRubberMaterial3D, // friction: 1.0, restitution: 0.8
} from '@web-engine-dev/physics3d';ECS Integration
Physics state is exposed as ECS resources for integration with the scheduler:
import {
PhysicsWorld2DResource,
PhysicsConfigResource,
} from '@web-engine-dev/physics2d';
// Insert the physics world as a resource
world.insertResource(PhysicsWorld2DResource, physicsWorld);Physics systems typically run in the FixedUpdate schedule to ensure deterministic behavior regardless of frame rate.
Rapier Adapters
For production use, the Rapier adapter packages provide high-performance, Rust-compiled-to-WASM physics:
@web-engine-dev/physics2d-rapier-- Rapier 2D adapter@web-engine-dev/physics3d-rapier-- Rapier 3D adapter
These adapters implement the same interfaces as the built-in physics packages, so switching is a matter of changing your import:
// Built-in (reference implementation)
import { PhysicsWorld3D } from '@web-engine-dev/physics3d';
// Rapier adapter (production)
import { PhysicsWorld3D } from '@web-engine-dev/physics3d-rapier';Units
Both physics packages use MKS (meters, kilograms, seconds) units:
- Objects should be 0.1 to 10 meters in size
- Gravity:
{ x: 0, y: -9.81 }(2D) or{ x: 0, y: -9.81, z: 0 }(3D) - Density: kg/m^2 (2D) or kg/m^3 (3D)
Using real-world scale produces the most stable simulation. Very small or very large objects can cause numerical instability.