expresso/body

Rigid body physics with flexible collider shapes.

This module provides the Body type and functions for creating and managing rigid bodies in a 2D physics simulation.

Collider Shapes

Bodies can use any of the collider shapes from the spatial library:

Body Types

Quick Start

import expresso/body
import vec/vec2

// Create a dynamic circle (common case)
let bullet = body.new_circle("bullet", vec2.Vec2(0.0, 0.0), radius: 0.25)
  |> body.with_velocity(vec2.Vec2(10.0, 0.0))

// Create a kinematic character with capsule collider
let player = body.new_capsule("player", vec2.Vec2(0.0, 0.0), height: 2.0, radius: 0.5)
  |> body.kinematic()

// Create a static wall with box collider
let wall = body.new_box("wall", vec2.Vec2(5.0, 0.0), half_width: 5.0, half_height: 1.0)
  |> body.with_mass(0.0)  // Infinite mass = static

Advanced Usage

For full control over the collider, use body.new() with a custom collider:

import spatial/collider
import vec/vec3

let custom_collider = collider.cylinder(
  center: vec3.Vec3(0.0, 0.0, 0.0),
  radius: 1.0,
  height: 3.0,
)
let tower = body.new("tower", vec2.Vec2(5.0, 0.0), custom_collider)

Types

A 3D rigid body with configurable collider

pub type Body(id) {
  Body(
    id: id,
    position: vec3.Vec3(Float),
    velocity: vec3.Vec3(Float),
    rotation: quaternion.Quaternion,
    angular_velocity: vec3.Vec3(Float),
    collider: collider.Collider,
    mass: Float,
    is_kinematic: Bool,
    friction: Float,
    static_friction: Float,
    dynamic_friction: Float,
    use_ccd: Bool,
    layer: Int,
    collision_mask: Int,
    is_trigger: Bool,
  )
}

Constructors

  • Body(
      id: id,
      position: vec3.Vec3(Float),
      velocity: vec3.Vec3(Float),
      rotation: quaternion.Quaternion,
      angular_velocity: vec3.Vec3(Float),
      collider: collider.Collider,
      mass: Float,
      is_kinematic: Bool,
      friction: Float,
      static_friction: Float,
      dynamic_friction: Float,
      use_ccd: Bool,
      layer: Int,
      collision_mask: Int,
      is_trigger: Bool,
    )

    Arguments

    id

    Unique identifier for this body

    position

    Current position in 3D space

    velocity

    Current velocity (units per second)

    rotation

    Current rotation (quaternion)

    angular_velocity

    Current angular velocity (radians per second, axis-angle form)

    collider

    Collision shape (Sphere, Box, Capsule, or Cylinder)

    mass

    Mass (0.0 = infinite mass)

    is_kinematic

    If true, body is not affected by physics (player-controlled)

    friction

    Velocity damping (0.0 = no damping, 1.0 = full damping)

    static_friction

    Static friction coefficient (prevents sliding, typical: 0.3-0.6)

    dynamic_friction

    Dynamic friction coefficient (opposes sliding, typical: 0.2-0.4)

    use_ccd

    Enable continuous collision detection for fast-moving objects

    layer

    Collision layer this body belongs to (bitmask)

    collision_mask

    Layers this body can collide with (bitmask)

    is_trigger

    If true, body is a trigger (detects collisions but doesn’t physically interact)

Collision layer (bitmask for flexible layer management)

Use powers of 2 for layers: 1, 2, 4, 8, 16, etc. This allows combining multiple layers with bitwise OR.

pub type CollisionLayer =
  Int

Values

pub fn bounding_radius(body: Body(id)) -> Float

Get the bounding radius of the collider (for spatial queries)

This returns the radius of the smallest sphere that contains the collider.

pub fn combine_layers(layers: List(Int)) -> Int

Combine multiple collision layers into a single mask using bitwise OR

Example

let mask = combine_layers([layer_2, layer_3])
pub fn dynamic(body: Body(id)) -> Body(id)

Make a body dynamic (affected by physics)

pub fn inverse_mass(body: Body(id)) -> Float

Get the inverse mass (used in physics calculations)

Returns 0.0 for kinematic bodies or bodies with 0 mass.

pub fn kinematic(body: Body(id)) -> Body(id)

Make a body kinematic (not affected by physics)

Kinematic bodies can push dynamic bodies but are not pushed themselves. Useful for player-controlled entities.

pub const layer_0: Int
pub const layer_1: Int
pub const layer_2: Int
pub const layer_3: Int
pub const layer_4: Int
pub const layer_5: Int
pub const layer_all: Int
pub fn new(
  id id: id,
  position position: vec3.Vec3(Float),
  collider collider: collider.Collider,
) -> Body(id)

Create a new body with a custom collider

Use this for full control over the collider shape.

Example

let custom_collider = collider.sphere(
  center: vec3.Vec3(0.0, 0.0, 0.0),
  radius: 2.0,
)
let body = body.new(
  id: "custom",
  position: vec3.Vec3(0.0, 0.0, 0.0),
  collider: custom_collider,
)
pub fn new_box(
  id id: id,
  position position: vec3.Vec3(Float),
  half_extents half_extents: vec3.Vec3(Float),
) -> Body(id)

Create a new dynamic body with a box collider

Example

let wall = body.new_box(
  id: "wall_1",
  position: vec3.Vec3(10.0, 5.0, 0.0),
  half_extents: vec3.Vec3(5.0, 0.5, 0.5),
)
pub fn new_capsule(
  id id: id,
  position position: vec3.Vec3(Float),
  height height: Float,
  radius radius: Float,
) -> Body(id)

Create a new dynamic body with a capsule collider

Perfect for character controllers!

Example

let player = body.new_capsule(
  id: "player",
  position: vec3.Vec3(0.0, 0.0, 0.0),
  height: 2.0,
  radius: 0.5,
)
pub fn new_sphere(
  id id: id,
  position position: vec3.Vec3(Float),
  radius radius: Float,
) -> Body(id)

Create a new dynamic body with a sphere collider

This is a convenience constructor for spherical bodies.

Example

let enemy = body.new_sphere(
  id: "enemy_1",
  position: vec3.Vec3(10.0, 5.0, 0.0),
  radius: 0.5,
)
pub fn should_collide(body_a: Body(id), body_b: Body(id)) -> Bool

Check if two bodies should collide based on their layers

Bodies collide if:

  • Body A’s layer is in Body B’s collision mask, AND
  • Body B’s layer is in Body A’s collision mask
pub fn trigger(body: Body(id)) -> Body(id)

Make a body a trigger (sensor)

Triggers detect collisions but don’t physically interact. Perfect for damage zones, checkpoints, pickups, etc.

Example

let checkpoint = body.new_circle("checkpoint", vec2.Vec2(10.0, 0.0), 1.0)
  |> body.trigger()
pub fn with_angular_velocity(
  body: Body(id),
  angular_velocity: vec3.Vec3(Float),
) -> Body(id)

Set the angular velocity of a body (axis-angle form, radians per second)

pub fn with_ccd(body: Body(id), enabled: Bool) -> Body(id)

Enable continuous collision detection for fast-moving objects

CCD prevents fast objects from tunneling through thin obstacles. Use for bullets, fast projectiles, or critical objects. Has a small performance cost.

pub fn with_collision_mask(body: Body(id), mask: Int) -> Body(id)

Set the collision mask of a body

The collision mask determines which layers this body can collide with. Use bitwise OR to combine multiple layers.

Example

// Player bullet only collides with enemies and environment
let bullet = body.new_circle("bullet", vec2.Vec2(0.0, 0.0), 0.2)
  |> body.with_layer(body.layer_1)
  |> body.with_collision_mask(combine_layers([
       body.layer_2,
       body.layer_3,
     ]))
pub fn with_dynamic_friction(
  body: Body(id),
  friction: Float,
) -> Body(id)

Set the dynamic friction coefficient (opposes sliding, typical: 0.2-0.4)

Dynamic friction opposes motion while sliding. Should usually be less than static friction.

pub fn with_friction(body: Body(id), friction: Float) -> Body(id)

Set the velocity damping of a body (0.0 = no damping, 1.0 = full damping)

pub fn with_layer(body: Body(id), layer: Int) -> Body(id)

Set the collision layer of a body

The layer determines which “group” this body belongs to. Use the predefined constants or custom powers of 2.

Example

let bullet = body.new_circle("bullet", vec2.Vec2(0.0, 0.0), 0.2)
  |> body.with_layer(body.layer_1)
pub fn with_mass(body: Body(id), mass: Float) -> Body(id)

Set the mass of a body

pub fn with_rotation(
  body: Body(id),
  rotation: quaternion.Quaternion,
) -> Body(id)

Set the rotation of a body

pub fn with_static_friction(
  body: Body(id),
  friction: Float,
) -> Body(id)

Set the static friction coefficient (prevents sliding, typical: 0.3-0.6)

Static friction must be overcome to start sliding. Higher values make objects harder to push.

pub fn with_velocity(
  body: Body(id),
  velocity: vec3.Vec3(Float),
) -> Body(id)

Set the velocity of a body

pub fn world_collider(body: Body(id)) -> collider.Collider

Get the collider in world space (updated with body position and rotation)

This is useful for collision detection.

Search Document