Physics · interface

WorldOptions

Options a consumer sets once. Every one of them is a quality or feel dial, not a contract.

Explained in Your first game, Rigid bodies.

interface WorldOptions
import type { WorldOptions } from '@driftengine/physics';

Properties

NameTypeDescription
substepsoptionalnumberHow many integrate-and-solve passes per tick. A quality dial: raising it changes nothing.
iterationsoptionalnumberIterations per substep.
gravityXoptionalnumber
gravityYoptionalnumber
gravityZoptionalnumber
contactHertzoptionalnumberContact stiffness as a frequency, which is what makes substeps a dial.
contactDampingoptionalnumber
jointHertzoptionalnumberJoint stiffness, higher than a contact's because a joint is a structural relationship.
jointDampingoptionalnumber
linearDampingoptionalnumber
angularDampingoptionalnumber
speculativeMarginoptionalnumberHow far apart two shapes may be and still get a constraint.
allowSleepoptionalbooleanWhether settled islands stop being solved. On by default; off for a test isolating something.
frictionModeloptionalFrictionModelHow the two friction axes are bounded against each other. box by default. See solver.ts.
More

elliptical is the correct one and is not the default, because every stacking result moves under it — fingerprintBodies, both baselines, and any golden fingerprint or stored replay a consumer has kept. A production engine does not silently invalidate those. What a consumer gets by asking for it is friction that is the same in every direction, which is what friction physically is; what a box gives instead is up to sqrt(2) more grip along the diagonal of a tangent basis built from the contact normal, which is an artefact rather than a fact about the world.

workersoptionalnumberHow many workers solve islands. 0, the default, is the serial runner and changes nothing.
More

It needs a cross-origin isolated page. Without one SharedArrayBuffer is undefined and the pool cannot start; physics stays correct and serial and parallelism.reason says so. See workerPool.ts for what the headers cost a consumer, which is why this is a switch and not a default. Clamped to hardwareConcurrency - 1 where the runtime reports one.

spawnoptional() => PoolWorkerBuild one worker yourself.
More

The default is new Worker(new URL('./islandWorker.ts', import.meta.url), { type: 'module' }), which Vite, webpack 5 and Parcel resolve. Supply this when your build does not, or to point at an entry of your own that imports @driftengine/physics/src/islandWorker.ts.

pooloptional(request: PoolRequest) => PoolOutcomeWhat builds the pool. createIslandPool is the one this package ships.
More

Passed in rather than imported here, and the reason is a measurement made twice. A PhysicsWorld that reaches the pool on its own puts it in every consumer's bundle: 2,629 bytes gzipped charged to everyone who never asks for a worker, in a package whose promise is that you pay only for what you import.

That was half the problem and the other half cost 4.7 times as much. Naming the factory at the call site let tree-shaking drop the pool, and a bundler had already emitted the worker — new Worker(new URL(...)) is rewritten at transform time, before anything decides what is reachable. Measured on two consumers on 2026-09-05: 12,259 bytes gzipped of worker chunk that nothing in either bundle could fetch. So the factory left the barrel too; see workers.ts.

import { PhysicsWorld } from '@driftengine/physics';
import { createIslandPool } from '@driftengine/physics/workers';

const world = new PhysicsWorld({ workers: 4, pool: createIslandPool });

Asking for workers without it is not silent: parallelism.reason says exactly this.