box2d
Simulate rigid bodies falling, colliding and coming to rest with Box2D, and read their positions back as numbers.
import "std:box2d"std:box2d is a 2D physics engine. You build a world, put bodies in it, give each body a shape, and call advance once per frame. The engine moves the bodies, resolves collisions and tells you where everything ended up. Use it for games, toys and anything where things should stack, bounce and slide.
The engine draws nothing. It works in plain numbers, so it runs, and can be tested, without a window. Pair it with window or raylib to show the bodies.
import "std:box2d"A world and a body#
box2d.new() makes an empty world. By default gravity is {x: 0, y: 9.8}, and y points down, like a screen. createBody adds a body and addCircle gives it a shape:
import "std:box2d"
auto world = box2d.new()
log world.gravity()
auto ball = world.createBody({type: "dynamic", position: {x: 0, y: 0}})
ball.addCircle(0.5).unwrap()
log world.bodyCount()
log ball.bodyType()
log ball.position()Positions are in world units, not pixels. Pick a scale (say one unit is one metre, or 50 pixels) and convert when you draw. Shape functions return a result, so call .unwrap() or handle the failure.
Body types#
A body is one of three types:
"static"(the default) never moves: floors, walls, platforms."dynamic"is pushed around by gravity, forces and collisions."kinematic"moves at the velocity you give it and ignores forces: moving platforms.
Shapes are addBox(width, height), addCircle(radius) and addPolygon(points), a convex polygon of 3 to 8 points. A body with no shape has no mass and collides with nothing.
Advancing time#
world.advance(ms) runs the simulation forward by that many milliseconds. Call it once per frame with the frame's duration. Here a ball falls onto a floor, 60 frames a second for two seconds:
import "std:box2d"
auto world = box2d.new()
auto floor = world.createBody({position: {x: 0, y: 10}})
floor.addBox(40, 1).unwrap()
auto ball = world.createBody({type: "dynamic"})
ball.addCircle(0.5, {restitution: 0.3}).unwrap()
for frame in 1 to 120 (
world.advance(1000 / 60).unwrap()
if frame % 30 == 0 (
log "frame " ++ frame ++ ": y = " ++ round(ball.position().y, 2)
)
)The floor is a box 1 unit tall centred on y = 10, so its top is at 9.5, and the ball touches it when its centre is at about 9. It lands a little after frame 80, bounces up a short way (restitution 0.3) and settles back. Use a fixed step like this, not the real elapsed time, if you want the same result on every run.
Shape options#
Every shape takes the same options:
density(default 1) decides the body's mass together with the shape's area.friction(default 0.2) is how much surfaces grip.restitutionis bounciness. 0 does not bounce, 1 keeps all its speed.sensor: truedetects touching without colliding.
import "std:box2d"
auto world = box2d.new()
auto light = world.createBody({type: "dynamic"})
light.addBox(1, 1, {density: 1}).unwrap()
auto heavy = world.createBody({type: "dynamic"})
heavy.addBox(1, 1, {density: 5}).unwrap()
log light.mass()
log heavy.mass()Resting and sleeping#
A body that stops moving falls asleep, so the engine spends no time on it. isAwake() tells you, and contactCount() says how many shape pairs are touching:
import "std:box2d"
auto world = box2d.new({gravity: box2d.vec(0, 10)})
auto floor = world.createBody({position: {x: 0, y: 10}})
floor.addBox(40, 1).unwrap()
auto crate = world.createBody({type: "dynamic"})
crate.addBox(1, 1, {friction: 0.5}).unwrap()
for _ in 1 to 240 ( world.advance(1000 / 60).unwrap() )
log "y:" round(crate.position().y, 2)
log "awake:" crate.isAwake()
log "contacts:" world.contactCount()The crate came to rest with its centre at about 9: the floor's top is at 9.5 and the crate is 0.5 from its centre to its bottom. box2d.vec(x, y) builds the {x, y} vector that gravity, forces and velocities use. Pass allowSleep: false to createBody to keep a body awake.
Pushing bodies#
applyImpulse changes a body's velocity at once (an impulse is mass times velocity). applyForce pushes for the next advance, so call it every frame for a steady push. Both wake a sleeping body. setLinearVelocity sets the speed directly:
import "std:box2d"
auto world = box2d.new({gravity: box2d.vec(0, 0)})
auto puck = world.createBody({type: "dynamic"})
puck.addCircle(0.5).unwrap()
puck.applyImpulse({x: 3, y: 0}).unwrap()
log puck.linearVelocity()
puck.setLinearVelocity({x: 0, y: -2}).unwrap()
for _ in 1 to 60 ( world.advance(1000 / 60).unwrap() )
log round(puck.position().y, 2)With zero gravity and nothing in the way, the puck keeps its velocity: it moved 2 units up in one second. The puck has a mass of about 0.785 (density 1 times the circle's area), which is why an impulse of 3 gave a velocity of about 3.8.
Placing bodies#
Set where a body starts with position, angle, velocity and angularVelocity in createBody, and move it later with setTransform(position, angle):
import "std:box2d"
auto world = box2d.new()
auto crate = world.createBody({
type: "dynamic",
position: {x: 4, y: 1},
angle: 0.5
})
crate.addBox(1, 1).unwrap()
log crate.position() round(crate.angle(), 2)
crate.setTransform({x: 0, y: 0}, 0).unwrap()
log crate.position() crate.angle()Destroying things#
body.destroy() removes one body, and world.destroy() frees the whole world and every body in it. Using a destroyed body is an error, but you can check first:
import "std:box2d"
auto world = box2d.new()
auto ball = world.createBody({type: "dynamic"})
ball.addCircle(1).unwrap()
world.destroy().unwrap()
log world.isDestroyed() ball.isDestroyed()
auto added = ball.addBox(1, 1)
log added.isErr()
log added.unwrapErr().kindBad arguments, like a box with no width, are not results: they throw a ValueError.
A body made with createBody({type: "static"}) is hit by a falling ball. What does the static body do?
One second of free fall
Drop a circle from y = 0 with no floor, advance 60 frames of 1000 / 60 milliseconds, and print its y rounded to a whole number. Gravity is the default, 9.8.
5
API reference#
29 members, generated from the std:box2d source of OSL 9.3.0.
Functions
box2d.new(object? options) *box2d.Worldnew creates an empty world. Options: gravity ({x, y}, default {x: 0, y: 9.8}, so with y pointing down things fall down the screen).
box2d.newWorld(any gravity) *box2d.WorldDeprecated. Deprecated: use box2d.new({gravity: {x, y}}) instead.
newWorld creates a world with the given gravity; anything that isn't a vector means {x: 0, y: 9.8}.
box2d.vec(number x, number y) {x: number, y: number}vec builds a vector {x, y}.
*box2d.World
world.advance(number ms, object? options) result<unit, error>advance runs the simulation forward by ms milliseconds; call it once a frame with the frame's duration. Options: velocityIterations (default 8) and positionIterations (default 3), the solver's accuracy per step. Fails with StateError when the world has been destroyed; throws ValueError for a negative duration.
world.bodyCount() intbodyCount returns how many bodies the world holds. Throws StateError when the world has been destroyed.
world.contactCount() intcontactCount returns how many shape pairs are touching or close to it. Throws StateError when the world has been destroyed.
world.createBody(object? options) *box2d.BodycreateBody adds a body to the world and returns it. Options: type ("static" (default), "dynamic" or "kinematic"), position ({x, y}; or x and y), angle (radians), velocity ({x, y} units per second), angularVelocity (radians per second), linearDamping, angularDamping, gravityScale (default 1), allowSleep (default true), awake (default true), fixedRotation, bullet and active (default true). A body has no shape until addBox, addCircle or addPolygon gives it one. Throws StateError when the world has been destroyed.
world.destroy() result<unit, error>destroy frees the world and all its bodies. Destroying it again does nothing; any other use of the world or its bodies afterwards is a StateError.
world.destroyBody(*box2d.Body body) booleanDeprecated. Deprecated: use body.destroy() instead.
destroyBody removes a body from the world and returns false when it was already removed, belongs to another world, or the world is destroyed.
world.gravity() {x: number, y: number}gravity returns the world's gravity {x, y}. Throws StateError when the world has been destroyed.
world.isDestroyed() booleanisDestroyed reports whether destroy has been called on the world.
world.setGravity(any gravity) result<unit, error>setGravity changes the world's gravity to a vector {x, y}. Fails with StateError when the world has been destroyed.
world.step(any dt, any ...iterations) booleanDeprecated. Deprecated: use world.advance(ms, {velocityIterations, positionIterations}) instead; it takes milliseconds.
step runs the simulation forward by dt seconds, with optional velocity and position iteration counts, and returns false when the world has been destroyed.
*box2d.Body
body.addBox(number width, number height, object? options) result<unit, error>addBox attaches a width by height rectangle centred on the body. Options: density (default 1), friction (default 0.2), restitution (bounciness, default 0) and sensor (detects contacts without colliding). Fails with StateError when the body or its world has been destroyed; throws ValueError for a size that isn't positive.
body.addCircle(number radius, object? options) result<unit, error>addCircle attaches a circle. Options: center ({x, y} relative to the body, default {x: 0, y: 0}) and the shape options of addBox. Fails with StateError when the body or its world has been destroyed; throws ValueError for a radius that isn't positive.
body.addPolygon(array points, object? options) result<unit, error>addPolygon attaches a convex polygon with 3 to 8 points {x, y} relative to the body. Takes the shape options of addBox. Fails with StateError when the body or its world has been destroyed; throws ValueError for too few or too many points.
body.angle() numberangle returns the body's rotation in radians. Throws StateError when the body or its world has been destroyed.
body.applyForce(any force) result<unit, error>applyForce pushes the body's centre with a force {x, y} for the next advance, waking it. Fails with StateError when the body or its world has been destroyed.
body.applyImpulse(any impulse) result<unit, error>applyImpulse changes the body's velocity at once by an impulse {x, y} (mass times velocity), waking it. Fails with StateError when the body or its world has been destroyed.
body.awake() booleanDeprecated. Deprecated: use body.isAwake() instead.
awake reports whether the body is awake, or false when it was destroyed.
body.bodyType() stringbodyType returns "static", "dynamic" or "kinematic". Throws StateError when the body or its world has been destroyed.
body.destroy()destroy removes the body from its world. Destroying it again, or after the world was destroyed, does nothing.
body.isAwake() booleanisAwake reports whether the body is being simulated rather than resting asleep. A destroyed body is not awake.
body.isDestroyed() booleanisDestroyed reports whether the body, or the world it belongs to, has been destroyed.
body.linearVelocity() {x: number, y: number}linearVelocity returns the body's velocity {x, y} in units per second. Throws StateError when the body or its world has been destroyed.
body.mass() numbermass returns the body's mass, which its shapes' density and area decide. Throws StateError when the body or its world has been destroyed.
body.position() {x: number, y: number}position returns the body's position {x, y} in world units. Throws StateError when the body or its world has been destroyed.
body.setLinearVelocity(any velocity) result<unit, error>setLinearVelocity sets the body's velocity {x, y} in units per second. Fails with StateError when the body or its world has been destroyed.
body.setTransform(any position, number angle) result<unit, error>setTransform moves the body to position {x, y} with rotation angle in radians. Fails with StateError when the body or its world has been destroyed.