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:
- Sphere - Fast, perfect for projectiles and particles
- Box - Axis-aligned bounding boxes for walls, platforms, crates
- Capsule - Ideal for character controllers (no corner catching!)
- Cylinder - For cylindrical objects like pillars or barrels
Body Types
- Dynamic - Affected by forces and collisions (enemies, objects, particles)
- Kinematic - Controlled externally, not affected by forces (player, moving platforms)
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 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 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_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.