Packages

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.

osl
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:

main.osl
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()
Click the code to edit it

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:

main.osl
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)
  )
)
Click the code to edit it

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.
  • restitution is bounciness. 0 does not bounce, 1 keeps all its speed.
  • sensor: true detects touching without colliding.
main.osl
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()
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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:

main.osl
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)
Click the code to edit it

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):

main.osl
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()
Click the code to edit it

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:

main.osl
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().kind
Click the code to edit it

Bad arguments, like a box with no width, are not results: they throw a ValueError.

Quick check

A body made with createBody({type: "static"}) is hit by a falling ball. What does the static body do?

Your turn

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.

main.osl
Ctrl Enter runs
Expected output
5

API reference#

29 members, generated from the std:box2d source of OSL 9.3.0.

Functions

#box2d.new(object? options) *box2d.World

new 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.World

Deprecated. 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() int

bodyCount returns how many bodies the world holds. Throws StateError when the world has been destroyed.

#world.contactCount() int

contactCount returns how many shape pairs are touching or close to it. Throws StateError when the world has been destroyed.

#world.createBody(object? options) *box2d.Body

createBody 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) boolean

Deprecated. 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() boolean

isDestroyed 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) boolean

Deprecated. 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() number

angle 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() boolean

Deprecated. Deprecated: use body.isAwake() instead.

awake reports whether the body is awake, or false when it was destroyed.

#body.bodyType() string

bodyType 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() boolean

isAwake reports whether the body is being simulated rather than resting asleep. A destroyed body is not awake.

#body.isDestroyed() boolean

isDestroyed 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() number

mass 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.