Box2D: 2D physics

Box2D is Erin Catto's 2D physics engine: rigid bodies that fall, collide, stack and come to rest. It comes with tide as a package. A body and its shapes are components of an entity, every tick steps them, and the match stays the same on every machine, through rollbacks, joins and hot reloads, with nothing for the game to do.

For 3D, see 3D physics (Box3D), which is the same package around Box3D.

An experiment

The package works and is tested, but its names and its shape aren't settled: they may change from one version of tide to the next.

Getting it

tide add puts it in the game in the current folder:

tide add box2d

That writes the package's name in the game's tide.packages, which is all it takes:

box2d

Its code is in the namespace Box2D, so a file that uses it says using Box2D;. From then on Body, Box, Circle and Physics name the package's. A name the game declares itself still means the game's: one with a Body of its own writes the package's in full, Box2D.Body.

A game that doesn't list the package compiles and ships none of it. One that does ships Box2D, which is under the MIT license, so it ships Box2D's notice with it: the LICENSE file in the package's box2d folder, src/packages/box2d/box2d/LICENSE of the folder tide version says tide is installed in.

A first world

A floor, two walls and three stacks of crates. Tap a crate to kick it, and anywhere else to drop a ball.

Crates, balls and a floorLive Tide example, tap it to play
Open separately
tide
using Box2D;

settings { title = "Physics in 2D"; maxPlayers = 1; }

component Crate { }
component Ball { int ticks; }
singleton Tally { int touches; }

scene Main { }

event(Spawned) Build(with Main)
{
    // Bodies that never move: the floor and two walls, a box on each.
    Spawn(Body { type = BodyType.Static, position = float2(0, -0.5) }, Box { size = float2(11, 1) });
    Spawn(Body { type = BodyType.Static, position = float2(-4.8, 6) }, Box { size = float2(1, 12) });
    Spawn(Body { type = BodyType.Static, position = float2(4.8, 6) }, Box { size = float2(1, 12) });
    // Three stacks of crates. A body is dynamic unless it says otherwise.
    for (var i = 0; i < 12; i++)
        Spawn(Body { position = float2(-2.5 + 2.5 * (i / 4), 0.5 + i % 4) }, Box, Crate);
}

// What a tap asks of the match.
command Drop { float2 at; }
command Kick { }

command(Drop drop, PlayerID sender) DropBall()
{
    Spawn(Body { position = drop.at }, Circle { radius = 0.3, bounce = 0.5 }, Ball);
}

// Code moves a body by writing its component: the next step pushes it, once.
command(Kick kick, PlayerID sender) KickCrate(mut Body body, with Crate)
{
    body.impulse = float2(2, 7);
}

// A ball goes after ten seconds, and its body with it.
system Expire(mut Ball ball, Time time)
{
    ball.ticks += 1;
    if (ball.ticks > 10 * time.tickRate) this.Destroy();
}

// Sent to a body when it starts touching another.
event(Touched) Count(mut Tally tally, with Ball)
{
    tally.touches += 1;
}

view Play(Devices devices, query (Body body, with Crate) crates)
{
    Draw.Clear(Color(0.086, 0.086, 0.094));
    Draw.SetCamera(float2(0, 4.5), 5);
    if (!devices.pointer.press.down) return;
    // A tap on a crate kicks it, and a tap anywhere else drops a ball there.
    var at = Draw.ScreenToWorld(Screen.Flip(devices.pointer.position));
    mut var kicked = false;
    foreach (var crate in crates)
    {
        if (kicked || Math.Distance(at, crate.body.position) > 0.5) continue;
        crate.entity.Send(Kick { });
        kicked = true;
    }
    if (!kicked) Send(Drop { at = at });
}

// A view sees a body where the last step left it, blended between ticks.
view Walls(Body body, Box box, without Crate)
{
    Draw.Rect(body.position, box.size, Color(0.24, 0.25, 0.27));
}

view Crates(Body body, Box box, with Crate)
{
    Draw.RoundRect(body.position, box.size - float2(0.06), 0.1, body.angle, Color(0.98, 0.75, 0.14));
}

// A ball has a spoke, from its middle to its edge, to show how it's turned.
view Balls(Body body, Circle circle)
{
    Draw.Circle(body.position, circle.radius, Color(0.18, 0.83, 0.75));
    var spoke = float2(Math.Cos(body.angle), Math.Sin(body.angle)) * circle.radius;
    Draw.Line(body.position, body.position + spoke, 0.07, Color(0.086, 0.086, 0.094));
}

view Hud(Tally tally)
{
    Draw.SetScreen();
    Draw.Text("Tap a crate to kick it, or anywhere else to drop a ball", float2(16, 14), 13, Color.white);
    Draw.Text($"Balls touched something {tally.touches} times", float2(16, 34), 13, Color(0.64, 0.64, 0.68));
}
  • Build makes the world out of entities: each has a Body, and a Box or a Circle for its shape. The floor and the walls are Static, and the crates are dynamic, as a body is unless it says otherwise.
  • Nothing in the game moves a crate. The package's systems step the world every tick, and write where each body is into its Body.
  • The views draw from the same components: body.position and body.angle, with the size its Box or Circle says. A ball has a spoke, so that its turning shows.
  • A tap is a command. DropBall spawns a body where the tap was. KickCrate writes the crate's impulse, and the next step pushes it.
  • Expire destroys a ball after ten seconds, and its body goes with it.
  • Count hears Touched, which the package sends to a body's entity when it starts touching another.

The Towers walkthrough shows how the package steers a falling brick as a kinematic body, turns it into a dynamic body when it lands, and casts shapes to find space for movement. It also covers absolute input, the room lobby and the views that present the physics match.

Bodies

Body is a rigid body. An entity that has one is in the physics world from the next tick on. These are the fields a game uses:

tide
component Body
{
    BodyType type = BodyType.Dynamic; // Static, Kinematic or Dynamic
    float2 position;                  // In meters
    float angle;                      // In radians, counterclockwise
    float2 velocity;                  // In meters a second
    float spin;                       // In radians a second
    float2 impulse;                   // A push at the next step, once
    float turn;                       // The same, around
    bool rewind;                      // Opt this body and its parts into collider history
    bool asleep;                      // At rest, and not stepped until something wakes it
    float gravityScale = 1;           // 0 for a body that floats
    float linearDamping;              // How fast it slows by itself
    float angularDamping;             // And how fast its spin does
    float sleepSpeed = 0.05;          // Slower than this for a while, it falls asleep
    bool fixedRotation;               // It never turns
    bool bullet;                      // For a small, fast body
}
  • A static body never moves: a floor, a wall. Its weight doesn't matter.
  • A kinematic body moves by its velocity whatever is in its way, and nothing pushes it: a platform, a door.
  • A dynamic body is moved by gravity, by what it touches and by what code pushes it with.
  • A bullet is checked against the other moving bodies all along its way, so that a small fast body doesn't pass through a thin one between two steps. It costs more, so it's for the few bodies that need it.

After each step, position, angle, velocity and spin are where Box2D left the body, with angle between -π and π, and asleep says whether it's at rest. Code reads them like any component's fields, and writing them moves the body, or puts it to sleep, or wakes it.

Views see a body between its last two ticks, as they see any component. The package says what two of the fields are, for that: position is a [Position] and angle a [Rotation] (see correction smoothing). So a view sees a body turn the short way through half a turn, where its angle goes from π to -π, and what it reads there can be a little past π. And when the server corrects where this machine had predicted a body, a view sees it get there over a few frames, where it would jump. Neither changes anything of the match.

type can be written at any time: see Changing what a body is. The fields from gravityScale down are read when the body is made, so they're set where the entity is spawned. Changing them later does nothing yet.

The rest of the component is the package's own, and not for a game to write: id is Box2D's handle to the body, 0 until its first step, and stepPosition, stepAngle, stepVelocity, stepSpin and stepAsleep are how the last step left it, which is how the package tells what code wrote since.

Box2D is tuned for meters, kilograms and seconds, and for moving things between a tenth of a meter and ten meters across. So a crate is 1 wide, not 100: the view's camera makes it as big on screen as the game wants.

Shapes

A shape is what a body touches with, and what gives it its weight. It's a component on the body's entity:

tide
Spawn(Body { position = float2(0, 4) }, Box { size = float2(2, 0.5) });
Spawn(Body { position = float2(3, 4) }, Circle { radius = 0.4, bounce = 0.5 });

These are their fields, with what each is unless a shape says otherwise:

tide
component Box
{
    float2 size = float2(1, 1); // Its full width and height
    float2 offset;              // From the body's position
    float density = 1;          // Its weight for its area
    float friction = 0.6;       // How much it grips: 0 for ice
    float bounce;               // 0 for no bounce, 1 for all of it
    bool sensor;                // Bodies pass through it, and it says which
}

component Circle
{
    float radius = 0.5;
    float2 offset;
    float density = 1;
    float friction = 0.6;
    float bounce;
    bool sensor;
}

With the default density a box of 1 by 1 weighs 1, and an impulse of 1 changes its speed by a meter a second.

An entity has one Box at most, and one Circle, and both can be on the same body, each at its offset. A shape can be added to a body that's already there: this.Add(Circle { radius = 0.3 });.

A shape is read once, when it's put on its body, the tick after it's spawned or added. Changing its component later does nothing yet.

A shape with sensor touches nothing and reports what's in it: see Sensors.

More shapes on a body

A body of more shapes than one box and one circle has the others as entities of their own. Each has its Box or its Circle, and a Part, which names the entity whose body it's on:

tide
// An L: a bar of three cells, and a fourth on its end
var brick = Spawn(Body { position = float2(0, 4) }, Box { size = float2(3, 1) }, Brick);
Spawn(Box { offset = float2(1, 1) }, Part { of = brick });
  • A part's shape is on the body from the next step, like any shape, at its offset from the body's place. The body weighs what all its shapes weigh, and turns around their middle.
  • Destroying a part's entity takes that one shape off. The body is lighter from the next step, and what was resting on the shape falls.
  • Destroying the body's entity takes its parts with it. Their entities go the tick after.
  • A part can be a sensor, on a body whose other shapes are solid: a zone beside a platform, a trigger on a door.
  • A part names an entity with a Body. One that names none, or an entity that has no body, is destroyed in the tick that finds it so.
  • The events say which shape met which: see Touching.

A part's entity has no Body of its own. An entity that has both a Body and a Part is a body, with its shapes on itself.

Moving a body

Code moves a body by writing its Body, and never by calling Box2D. After each step the package writes where every body is into its component, and before the next one it looks at each for what code changed since, and tells Box2D.

A push is an impulse. It's given once, at the next step, and the field is zero again after it:

tide
system Jump(PlayerInput input, mut Body body, with Player)
{
    if (input.jump.down) body.impulse = float2(0, 5);
}

An entity can be spawned with one, which pushes it at its first step, as soon as its shapes are on it:

tide
Spawn(Body { position = muzzle, impulse = aim * 3 }, Circle { radius = 0.1 }, Shot);

A speed that code decides is written every tick. The other half of the vector stays Box2D's:

tide
system Walk(PlayerInput input, mut Body body, with Player)
{
    body.velocity.x = input.move.x * 4;
}

A body that's somewhere else all at once has its position written. Snap has views draw it there without sliding (see Jumps):

tide
event(Fell) Restart(mut Body body, Arena arena)
{
    body.position = arena.start;
    body.velocity = float2(0, 0);
    this.Snap();
}

A kinematic body goes where its velocity takes it, and carries what stands on it:

tide
component Lift { }

event(Spawned) Build(with Main)
{
    Spawn(Body { type = BodyType.Kinematic, position = float2(0, 2), velocity = float2(1, 0) },
          Box { size = float2(3, 0.4) }, Lift);
}

system Sweep(mut Body body, with Lift)
{
    if (body.position.x > 2) body.velocity = float2(-1, 0);
    if (body.position.x < -2) body.velocity = float2(1, 0);
}

Changing what a body is

A body's type is written like its place, and the next step has it as the new kind, with the shapes it had: a door that's locked, a crate that's frozen where it stands, a piece that's carried and then let go.

tide
component Frozen { }

// Frozen where it is, and let go again: the same body all along
event(Froze) Freeze(mut Body body)
{
    body.type = BodyType.Static;
    this.Add(Frozen);
}

event(Thawed) Thaw(mut Body body)
{
    body.type = BodyType.Dynamic;
    this.Remove(Frozen);
}
  • To static: it stops where it is, and its velocity and spin read zero from the next step.
  • To kinematic: it goes on at the velocity it has, whatever is in its way: write the speed it should have with the type.
  • To dynamic: gravity and whatever it touches move it from the next step, with the weight its shapes give it.
  • What touched it is let go and found again at that step, so Parted and then Touched come for each. What rested on it is awake, and stays where it was when the body under it only turned static.

Bodies at rest

Box2D stops moving a body that has come to rest: it's asleep, and costs nothing, until something touches it or code wakes it. asleep says which it is, after each step, and code that writes it puts the body to sleep or wakes it, at the next step:

tide
component Boulder { }

// The boulder hangs over the path, asleep from the tick it's spawned
event(Spawned) Build(with Main)
{
    Spawn(Body { position = float2(4, 9), asleep = true }, Circle { radius = 1 }, Boulder);
}

// ...until the game's own event says the trap was sprung
event(Sprung) Drop(mut Body body, with Boulder)
{
    body.asleep = false;
}

// The round is over: everything stops where it is
system Freeze(mut Body body, Round round)
{
    if (round.over && !body.asleep) body.asleep = true;
}
  • A body that's put to sleep stops where it is, in the air too: its velocity and its spin are zero from then on, and none of its speed comes back when it wakes.
  • It takes what it touches with it. Box2D sleeps and wakes bodies an island at a time: the bodies that touch each other, and the ones those touch, as the last step found them. So the bottom crate of a stack that's put to sleep takes the stack with it, and a crate that's sliding along a plank stops where it is when the plank is put to sleep. A kinematic body is one of them: a lift goes to sleep with what stands on it. A static body isn't: two crates on the same floor are two islands.
  • What hasn't touched it yet stays awake. A crate that's still on its way down to a sleeping stack goes on falling, and wakes the stack when it lands.
  • Waking is the same: asleep = false wakes the body and all of what it fell asleep with.
  • A static body is never asleep. Its asleep reads false, whatever is written into it, and so does a sleeping body's once it's made Static.
  • It works where bodies don't fall asleep by themselves (sleeping off): one that code puts to sleep sleeps all the same, until something wakes it.

A sleeping body wakes by itself, with its island, when something awake touches it. And whatever else code writes into it wakes it too:

  • A velocity or a spin that isn't zero, an impulse or a turn.
  • A position or an angle: the body is moved and wakes, so a crate at rest that's put in the air falls. What was touching it wakes too: a platform that's moved away, a static one as well, drops what stood on it.
  • A type: the body wakes, and what was touching it.

So does a body beside it that's destroyed, and a new gravity. After the step, asleep reads false.

How readily a body falls asleep by itself is its sleepSpeed: once it has been slower than that for half a second, with everything it touches, it sleeps. 0.05 meters a second is Box2D's own, and a body that should rest sooner, like the bricks of a tall stack, says more. Physics.sleeping turns that off for the whole world (see The world).

What code puts to sleep in a tick is asleep as that tick's step starts, whatever else was written that tick, into it or into a body it touches. A push or a speed written with it is lost. A position written with it is where it sleeps: a body that's put somewhere else and to sleep at once hangs there. A new gravity in that tick doesn't wake it. What does is another body running into it in that very step.

That's for a body that was awake. Writing asleep = true into one that's asleep already writes nothing, so a sleeping body that code moves wakes like any other: asleep is false after that tick, and code that wants it asleep where it put it says so then.

A body that's spawned asleep starts where it's spawned, with none of the speed or the push it was spawned with. A gravity change or automatic sleeping being turned off in its creation tick does not override that sleep. Bodies spawned asleep on each other are each asleep by themselves, as no step has seen them touch: one that's hit wakes, and the ones resting on it a step after each other. That's fine for a few. For a great many stacked on each other it's better to spawn them awake and put them to sleep a tick later, once they have had a step: they then sleep and wake a stack at a time.

A world where everything is asleep and nothing was written isn't stepped at all (see What it costs), and a body that's put to sleep costs what one that fell asleep does: nothing.

When things happen in a tick

The package's systems run before the game's, as every package's do, in this order:

  1. Bodies and shapes that are new are made, and what code wrote into a Body goes to Box2D.
  2. The world steps, by the tick's length, Time.dt.
  3. Each body's Body gets where the step left it.
  4. Touched and Parted are sent.

So a system of the game's reads this tick's positions, and what it writes goes into the next tick's step. The same goes for what an event's or a command's handler writes, at the end of the tick. An entity spawned in a tick has its body made at the start of the next, and is in that tick's step.

Before the step

A system that has to write into this tick's step says so. The package's systems are one named phase, PhysicsStages.Step, and a system that's Before it runs before everything in the list above:

tide
[Before(PhysicsStages.Step)]
system Walk(PlayerInput input, mut Body body, with Player)
{
    body.velocity.x = input.move.x * 4;
}

What Walk writes is the speed this tick steps with, a tick sooner than without the attribute. What it reads of a body is where the last tick's step left it.

That's the one way to get in ahead of the step, and a few things follow from how systems are ordered:

  • A system that doesn't say runs after the step, in a game where none says Before. Once one does, the systems written above it in the game's files run before the step too, as the package now waits for a system that comes after them. [After(PhysicsStages.Step)] on a system holds it after the step whatever the others say: write it on one that has to read where this tick's step left things.
  • [Before(Step)] is an error, as no system has that name: the phase is what to name.
  • The package's own systems aren't for a game to name. tide schedule lists them, Box2D.Enter, Box2D.Simulate, Box2D.Leave and the rest, and they keep their order whatever a game orders its systems by. But a system that's put among them, with [Before(Box2D.Simulate)] say, runs after what code wrote has gone to Box2D and before the bodies come back: where it writes a body's place or its speed, the step's own is written over it.

A tick is one step, so a match at the default 60 ticks a second steps a 60th of a second at a time, which is what Box2D is tuned for. A lower tickRate makes each step longer and the world less steady.

Touching

When two bodies start touching, the package sends Touched to each one's entity, and Parted when they stop. Each has the other entity, and which shapes met:

tide
event Touched
{
    Entity other;     // Whose body it met
    Entity part;      // Which shape of this body's
    Entity otherPart; // ...and which of the other's
}

part and otherPart are entities too: the body's own entity for a shape that's on it, and the part's entity for a part. Parted has the same three.

tide
component Fragile { int hits; }

// Runs for the entity that touched something: `this` is that entity.
event(Touched) Crack(mut Fragile fragile)
{
    fragile.hits += 1;
    if (fragile.hits == 3) this.Destroy();
}

// What it touched is `touched.other`: the World has its components.
event(Touched touched) Land(World world, with Ball)
{
    if (world.Has<Floor>(touched.other)) this.Destroy();
}

They're events like any other: a handler runs for the entity the event was sent to, when that entity has the components the handler takes, at the end of the tick whose step found the touch. A local handler hears them too, on each machine, for a sound or a spark.

They're sent once for each pair of shapes that touch, so a body whose Box and Circle are both on the floor hears Touched twice, each time with its own entity for part: which of an entity's own two shapes it was isn't said. A part says, as it's an entity.

When the other entity was destroyed while they touched, Parted still comes, and its other is no entity: parted.other == default. Which entity it was isn't said. When only a part was destroyed, its body is still there to say: Parted comes to both bodies with the right other, and default for the part that went.

Hits

Touched says that two bodies met. Hit says how hard: it's sent to each body's entity when they meet faster than Physics.hitSpeed, which is a meter a second unless the game says otherwise.

tide
event Hit
{
    Entity other;
    Entity part;      // Which shapes met, as in Touched
    Entity otherPart;
    float2 point;     // Where
    float2 normal;    // The way from this body into the other
    float speed;      // How fast they closed, in meters a second
}

// Dust where a crate lands hard, on each machine
local event(Hit hit) Thud(mut Dust dust, with Crate)
{
    Puff(dust, hit.point, hit.speed);
}
  • It comes in the tick whose step they met in, once for each pair of shapes, to both entities: each with the normal pointing away from itself.
  • A crate that settles onto another sends none, nor does anything that goes into a sensor.
  • It's for what shows and sounds of a collision, so its handler is often a local one. An event that nothing in the match handles costs the match nothing.

Sensors

A shape with sensor = true stops nothing: bodies pass through it. It sends the same two events, Touched when a body's shape comes into it and Parted when it leaves, to its own entity and to the other's:

tide
component Goal { int score; }

event(Spawned) Build(with Main)
{
    Spawn(Body { type = BodyType.Static, position = float2(8, 1) }, Box { size = float2(1, 2), sensor = true }, Goal);
}

// Runs for the goal: what came in is `touched.other`.
event(Touched touched) Score(mut Goal goal, World world)
{
    if (world.Has<Ball>(touched.other)) goal.score += 1;
}
  • A sensor reports every body's shape that's in it: one on a body that never moves too, so a sensor under a character's feet says when it stands on the ground. A body that's in it from the start is reported at its first step.
  • It can be on a body that moves, alone or beside the shapes the body touches with. It reports nothing of its own body's shapes.
  • A sensor doesn't report another sensor.
  • It has a weight like any shape, by its density: give it 0 where it's beside the shapes its body touches with, and shouldn't add to them.
  • What's in a sensor is looked at once a step, where things are after it. A small body that's fast enough to cross a thin sensor between two steps may come and go unreported: make such a sensor thick.

Touched and Parted say who comes and who goes. Inside says who's there: the entities whose bodies have a shape in an entity's sensors, as the last step found them.

tide
// How many are standing on the plate
system Weigh(mut Plate plate, Physics physics)
{
    physics.Inside(this, out var found);
    plate.load = found.count;
}
  • physics.Inside(sensor, out found) takes the entity the sensor shape is a component of: a body's, or a part's. An entity with no sensor has nobody in it.
  • It returns whether anybody is there. found has each entity once, a static body's too, in an order that's the same on every machine.
  • It's one of the functions that ask the world something: see Asking the world for what they see and when.

Asking the world

A ray, a shape that's moved along a way, a shape that's looked into: functions that ask the world something and change nothing of it. Each takes the Physics singleton of the code that asks, returns whether it found anything, and gives what it found through out.

tide
struct RayHit
{
    Entity entity;   // Whose body was met
    Entity part;     // ...and which shape: that entity, or a part's
    float2 point;    // Where
    float2 normal;   // The way out of what was met, there
    float fraction;  // How far along the way: 0 at its start, 1 at its end
}

bool Raycast(Physics physics, float2 from, float2 to, out RayHit hit)
bool Cast(Physics physics, Box shape, float2 from, float angle, float2 to, out RayHit hit)
bool Cast(Physics physics, Circle shape, float2 from, float angle, float2 to, out RayHit hit)
bool Overlap(Physics physics, Box shape, float2 at, float angle, out List<Entity> found)
bool Overlap(Physics physics, Circle shape, float2 at, float angle, out List<Entity> found)

Raycast is the first thing a ray meets on its way from one point to another:

tide
// A shot that's a ray: what's in its way this tick
system Fly(mut Shot shot, Physics physics, Time time)
{
    var to = shot.at + shot.velocity * time.dt;
    if (physics.Raycast(shot.at, to, out var hit))
    {
        hit.entity.Send(Struck { at = hit.point });
        this.Destroy();
        return;
    }
    shot.at = to;
}

Cast moves a shape along a way, and says where it first touches something: fraction is how much of the way it went before it did. The shape is a Box or a Circle as it would be on a body at from, turned by angle: its size or its radius, around its offset. So a body's own box, cast from the body's position at its angle, is the box where it is.

tide
// Whether there's ground just under the player
system Stand(Body body, mut Player player, Physics physics)
{
    var feet = Circle { radius = 0.3 };
    var under = body.position + float2(0, -0.4);
    player.grounded = physics.Cast(feet, body.position, 0, under, out _, ignore: body);
}

Overlap is the entities whose bodies have a shape in a shape at a place, or touching it, each once:

tide
// Everything within three meters is thrown outwards
event(Exploded) Blast(Body body, Physics physics)
{
    physics.Overlap(Circle { radius = 3 }, body.position, 0, out var found);
    foreach (var caught in found) caught.Send(Thrown { from = body.position });
}
  • One body can be left out. Raycast and Cast take a last argument, the Body they don't meet: physics.Raycast(body.position, target, out var hit, ignore: body). A ray from a body's own middle meets that body at once otherwise.
  • Sensors aren't met, nor found by Overlap: they stop nothing. Inside is what asks a sensor.
  • A ray or a shape that starts in something meets it there, at fraction 0, with no normal. A shape that only touches something where it starts is in it, for this: a crate at rest touches the floor, so its own box cast along the floor meets the floor at once. Cast a slightly smaller one to slide it along what it rests on.
  • Of two things as near as each other, the one that was made first is the answer, and Overlap and Inside give their entities in the order their bodies were made in: the same on every machine, whatever order Box2D comes to them in.

What they see

A question is asked of the world as its last step left it:

  • A system that's before the step sees where the last tick's step left things. That's what the system reads in body.position too, so the two agree.
  • A system after the step, and a handler, see where this tick's step left things.
  • What code wrote since isn't in the answer: a body whose position was written this tick is still where the step left it, to a ray, until the next step. Neither is a body that was spawned since.
  • An entity that was destroyed isn't met any more, from the moment it's gone.
  • In a world nothing has stepped yet, the first tick's, there's nothing to find.

A view asks too, with the Physics it takes: where the mouse points, where a piece would land. It's asked of the world the view draws, as the last tick left it, not between two ticks as the view sees bodies' places.

tide
// What's under the pointer
view Hover(Devices devices, Physics physics, mut Pointed pointed)
{
    var at = Draw.ScreenToWorld(Screen.Flip(devices.pointer.position));
    pointed.over = default;
    if (physics.Overlap(Circle { radius = 0.1 }, at, 0, out var found)) pointed.over = found[0];
}

The answers are the same on every machine, like everything of the match: match code can act on them. They cost what Box2D's own queries do, about half a microsecond each in a small world. The one cost to know about: where this machine has no Box2D world at the match's state, the first question makes it from the state the match keeps, as a rolled-back tick does (see What it costs).

A system that asks runs on the tick's threads like any other.

The world

Physics is a singleton, the world's. A game sets these fields of it:

tide
singleton Physics
{
    float2 gravity = float2(0, -10);
    int substeps = 4;            // The smaller steps Box2D solves each step in
    float contactStiffness = 30; // How stiff contacts are, in cycles a second
    float contactDamping = 10;   // ...and how little they spring back
    float contactSpeed = 3;      // The fastest bodies that overlap are pushed apart, in meters a second
    float bounceSpeed = 1;       // Shapes with `bounce` only bounce when they meet faster than this
    float hitSpeed = 1;          // Bodies that meet faster than this send Hit
    bool sleeping = true;        // Bodies at rest fall asleep
}

Each is what Box2D comes with, so a game that sets none gets the same world as before there were any.

  • substeps: more make stacks and fast bodies steadier, and a step slower.
  • contactStiffness is how hard two bodies that touch push back as they sink into each other. Stiffer contacts hold a tall stack: a tower of thirty crates that sheds half of them when a heavy crate lands on it stands whole at twice the stiffness. It's no more than an eighth of the substeps a second, which at four substeps and 60 ticks a second is 30, the default: to go stiffer, raise substeps with it, 8 for 60.
  • contactDamping is how gently they come back out. Under 1 they spring; the higher, the less a pile that's been squeezed pops.
  • contactSpeed is the fastest two bodies that are in each other are moved apart: lower, and something that's spawned inside something else eases out of it.
  • bounceSpeed and hitSpeed are the speeds two bodies have to meet at for a bounce to count and for Hit to be sent.
  • sleeping, off, keeps bodies from falling asleep by themselves. Code can still put a body to sleep with asleep, and a world with every body asleep waits until something wakes it.

Any match code that takes it mut writes them, and the next step uses them:

tide
event(Spawned) Setup(mut Physics physics, with Main)
{
    physics.gravity = float2(0, -3);
}

A new gravity wakes every body that's asleep, so that what was at rest feels it. A game that changes it every tick has nothing at rest. A change to any of the others is the next step's too, and that tick steps even when everything is asleep.

The other fields are the package's own. moved says whether the last tick stepped the world, and bodies and shapes how many were made so far; stamp, size, image, gone, shed, dropped, owners, parts and sensors are how the package keeps Box2D's state in the match.

Destroying

An entity that's destroyed takes its body with it, at the next step: this.Destroy() is all there is to it. What it was touching gets Parted. Its parts go with it, and a part that's destroyed by itself takes its one shape off the body.

Removing Body from an entity that stays doesn't take its body out of the world yet. Destroy the entity.

Multiplayer

There's nothing to add for a match with several machines. Box2D keeps its world in C's memory, which a rollback or a joining player wouldn't reach, so after every step the package has Box2D's whole state in the match, as bytes in Physics: it writes what changed of them. A client that predicted wrong and runs ticks again, a player who joins, a build that's reloaded and a machine that takes the match over all go on from that state.

That state is in the match's hash, and it's the same bytes on every machine: the same match gives the same result on desktop and on the web, so their players share one. See Networking and Determinism.

It's also why code never calls Box2D: everything that changes the physics world goes through the match, as components. Asking changes nothing, so it's a function. The package itself talks to Box2D through C functions, and reads the bodies back with systems written in C.

What it costs

Measured on a release build, with 2,400 crates in stacks, on one thread, with the world kept and hashed every tick as a client's is:

A tick, in milliseconds Everything moving One crate pushed At rest
The tick 0.51 0.06 0.02
of it, Box2D's step 0.30 0.01 0
Keeping and hashing the world, after it 0.26 0.04 0.00
  • A world at rest costs next to nothing. It isn't stepped, and nothing of the match is written. What's left is looking at each body for what code wrote.
  • The step goes by what's moving. With one crate pushed among 2,400 it takes a hundredth of a millisecond.
  • Keeping Box2D's state in the match goes by what's moving too, while no more than half of the world is awake. It's about a kilobyte a body, 2.5 MB here, and Box2D says which bodies, shapes and contacts may have changed since the last tick: only those are written, which with one crate pushed takes 0.01 ms of a tick's 0.06.
  • With more than half of the world awake, all of that state is written and compared with the last tick's, which goes by how big the world is: 0.06 ms of the 0.51 here, before what changed of it is put into the match. A stack that wakes or falls asleep can have one tick's written whole too.
  • Only what changed of it is copied and hashed, either way, and a player who joins downloads it with the rest of the world.
  • A rollback makes Box2D's world again from that state, once: about half a millisecond for this world, before the ticks it runs again. So does the first question a view or a system asks when this machine has no Box2D world at the match's state, which is the tick after a rollback, or a view of a world other than the last one that ticked. In a world of a few hundred bodies that's a few hundredths of a millisecond.
  • The step runs on the tick's threads once 2,048 bodies are awake, 4,096 on the web, and gives the same world on any number of them.
  • On the web the step takes about 0.46 ms for this world with everything moving. Box2D solves four contacts at a time there, on WebAssembly's SIMD, where a desktop with AVX2 solves eight, and gives the same world to the bit.
  • Building: Box2D compiles in about 6 seconds, once, and adds about 350 KB to a game.

Not there yet

  • Joints, chains, polygons other than boxes, and capsules.
  • Rays and casts give the first thing met, not everything along the way, and leave out one body at the most. Nothing asks by a kind of thing (layers), and a sensor is never met.
  • Touched and Parted don't say which of an entity's own Box and Circle it was, and a parting from an entity that was destroyed doesn't say which entity.
  • The fields of a body that are read when it's made don't follow later changes, and a shape's fields don't either: a sensor stays a sensor.
  • Removing Body from an entity that stays leaves its body in the world, and removing Box or Circle leaves the shape on it.

Rewinding queries

physics.Raycast(...), physics.Cast(...), physics.Overlap(...) and physics.Inside(...) query current geometry. To validate a shot against confirmed presentation, prepare a separate context once:

tide
settings { maxRewind = 0.25; } // Seconds; 0 (the default) disables history

// On players or moving geometry that should rewind:
// Spawn(Body { rewind = true }, ...);

system Fire(Controls controls, Body body, Physics physics)
{
    if (!controls.fire.down) return;
    // Validate weapon, origin, aim and firing cadence from match state first.
    if (!physics.Rewind(controls.viewTime, out var query)) return;
    if (query.Raycast(body.position, controls.aim, out var hit, body))
    {
        // Validate the hit entity is still alive, then apply the game's damage.
        ...
    }
}

The context has every current query form, including all shape casts, overlaps, sensor Inside queries and existing ignore-body overloads. It is detached geometry: preparing and querying it moves no live body, changes no contacts and sends no events. Query output is temporary unless the game saves it.

Body.rewind opts the entire body and its parts in. Their poses interpolate between retained physics steps, with Snap() preventing interpolation across a teleport. Shape geometry and membership use the upper endpoint. Non-opted bodies remain current, so a wall still blocks a rewound player. A moving door can opt in too. Despawned historical shapes can be hit; their entity handles retain their generations, and the game must check that a damage target is still alive. A newly spawned opted-in body has no shape in an older frame. An existing body that opts out uses its current geometry.

Rewind returns false for disabled history, invalid, stale or future times, or an unavailable endpoint. The output then contains no queryable geometry. No time is clamped and no current-world fallback is substituted. History begins after the first completed physics step and follows the same snapshot, rollback and serialization path as the match. Geometry is that of the physics step, as with current queries; write movement before PhysicsStages.Step when presentation and collider poses should agree.

The history window covers the full age of the frame when the server executes the shot: confirmation delivery, time since the last drawn frame, input lead and delivery of the input all contribute. Low frame rates, stalls, jitter and packet loss can push a shot outside maxRewind even when its ping seems small. The engine uses the actual presented tick and fraction, rather than estimating the shot time by subtracting ping.

The server validates the input timestamp, but the game remains responsible for authoritative shot validation. It must never accept a client-supplied collider pose, origin or hit just because the timestamp passed validation.