tiramisu/physics

Physics module using Rapier physics engine

Provides declarative physics simulation following the same immutable, diff/patch pattern as the rest of Tiramisu.

Physics bodies are declared alongside scene nodes, and the physics world is managed as part of the game’s Model state.

Types

Axis locks for restricting body movement/rotation

pub type AxisLock {
  AxisLock(
    lock_translation_x: Bool,
    lock_translation_y: Bool,
    lock_translation_z: Bool,
    lock_rotation_x: Bool,
    lock_rotation_y: Bool,
    lock_rotation_z: Bool,
  )
}

Constructors

  • AxisLock(
      lock_translation_x: Bool,
      lock_translation_y: Bool,
      lock_translation_z: Bool,
      lock_rotation_x: Bool,
      lock_rotation_y: Bool,
      lock_rotation_z: Bool,
    )

    Arguments

    lock_translation_x

    Lock translation on X axis

    lock_translation_y

    Lock translation on Y axis

    lock_translation_z

    Lock translation on Z axis

    lock_rotation_x

    Lock rotation on X axis

    lock_rotation_y

    Lock rotation on Y axis

    lock_rotation_z

    Lock rotation on Z axis

Physics body type

pub type Body {
  Dynamic
  Kinematic
  Fixed
}

Constructors

  • Dynamic

    Dynamic bodies are affected by forces and gravity

  • Kinematic

    Kinematic bodies can be moved programmatically but don’t respond to forces

  • Fixed

    Fixed (static) bodies don’t move

Collider shape

pub type ColliderShape {
  Box(
    offset: transform.Transform,
    width: Float,
    height: Float,
    depth: Float,
  )
  Sphere(offset: transform.Transform, radius: Float)
  Capsule(
    offset: transform.Transform,
    half_height: Float,
    radius: Float,
  )
  Cylinder(
    offset: transform.Transform,
    half_height: Float,
    radius: Float,
  )
}

Constructors

  • Box(
      offset: transform.Transform,
      width: Float,
      height: Float,
      depth: Float,
    )

    Box collider with half-extents

  • Sphere(offset: transform.Transform, radius: Float)

    Sphere collider with radius

  • Capsule(
      offset: transform.Transform,
      half_height: Float,
      radius: Float,
    )

    Capsule collider (cylinder with rounded caps)

  • Cylinder(
      offset: transform.Transform,
      half_height: Float,
      radius: Float,
    )

    Cylinder collider

Collision events that occurred during the physics step

pub type CollisionEvent {
  CollisionStarted(body_a: String, body_b: String)
  CollisionEnded(body_a: String, body_b: String)
}

Constructors

  • CollisionStarted(body_a: String, body_b: String)

    Two bodies started colliding

  • CollisionEnded(body_a: String, body_b: String)

    Two bodies stopped colliding

Collision groups for filtering which objects can collide with each other.

Uses Rapier’s collision groups system based on 32-bit bitmasks:

  • membership: What collision layers this body belongs to (0-15)
  • filter: What collision layers this body can interact with (0-15)

Two bodies can collide only if:

  • Body A’s membership overlaps with Body B’s filter, AND
  • Body B’s membership overlaps with Body A’s filter

Example

// Player belongs to layer 0, collides with layers 1 (enemies) and 2 (ground)
let player_groups = physics.CollisionGroups(
  membership: [0],
  filter: [1, 2]
)

// Enemy belongs to layer 1, collides with layers 0 (player) and 3 (projectiles)
let enemy_groups = physics.CollisionGroups(
  membership: [1],
  filter: [0, 3]
)

// Ground belongs to layer 2, collides with everything
let ground_groups = physics.CollisionGroups(
  membership: [2],
  filter: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
)
pub type CollisionGroups {
  CollisionGroups(membership: List(Int), filter: List(Int))
}

Constructors

  • CollisionGroups(membership: List(Int), filter: List(Int))

    Arguments

    membership

    List of collision layers this body belongs to (0-15)

    filter

    List of collision layers this body can collide with (0-15)

Opaque handle to the Rapier physics world This is part of your Model and gets updated each frame

pub opaque type PhysicsWorld(id)

Result of a raycast hit

pub type RaycastHit(id) {
  RaycastHit(
    id: id,
    point: vec3.Vec3(Float),
    normal: vec3.Vec3(Float),
    distance: Float,
  )
}

Constructors

  • RaycastHit(
      id: id,
      point: vec3.Vec3(Float),
      normal: vec3.Vec3(Float),
      distance: Float,
    )

    Arguments

    id

    ID of the body that was hit

    point

    Point where the ray intersected the body

    normal

    Normal vector at the hit point

    distance

    Distance from ray origin to hit point

Physics body configuration

pub opaque type RigidBody

Builder for creating rigid bodies with a fluent API

pub opaque type RigidBodyBuilder(a)
pub type WithCollider
pub type WithoutCollider

Physics world configuration

pub type WorldConfig(id) {
  WorldConfig(gravity: vec3.Vec3(Float))
}

Constructors

  • WorldConfig(gravity: vec3.Vec3(Float))

    Arguments

    gravity

    Gravity vector (typically Vec3(0.0, -9.81, 0.0))

Values

pub fn apply_force(
  world: PhysicsWorld(body),
  id: body,
  force: vec3.Vec3(Float),
) -> PhysicsWorld(body)

Queue a force to be applied to a rigid body during the next physics step. Returns updated world with the command queued.

Example

let world = physics.apply_force(world, "player", vec3.Vec3(0.0, 100.0, 0.0))
let world = physics.step(world, ctx.delta_time)  // Force is applied here
pub fn apply_impulse(
  world: PhysicsWorld(body),
  id: body,
  impulse: vec3.Vec3(Float),
) -> PhysicsWorld(body)

Queue an impulse to be applied to a rigid body during the next physics step. Returns updated world with the command queued.

Example

// Jump
let world = physics.apply_impulse(world, "player", vec3.Vec3(0.0, 10.0, 0.0))
pub fn apply_torque(
  world: PhysicsWorld(body),
  id: body,
  torque: vec3.Vec3(Float),
) -> PhysicsWorld(body)

Queue a torque to be applied to a rigid body during the next physics step. Returns updated world with the command queued.

pub fn apply_torque_impulse(
  world: PhysicsWorld(body),
  id: body,
  impulse: vec3.Vec3(Float),
) -> PhysicsWorld(body)

Queue a torque impulse to be applied to a rigid body during the next physics step. Returns updated world with the command queued.

pub fn build(
  builder: RigidBodyBuilder(WithCollider),
) -> RigidBody

Build the final rigid body from the builder

Returns an error if no collider was set.

pub fn get_angular_velocity(
  world: PhysicsWorld(body),
  id: body,
) -> Result(vec3.Vec3(Float), Nil)

Get the current angular velocity of a rigid body

pub fn get_collision_events(
  world: PhysicsWorld(id),
) -> List(CollisionEvent)

Get all collision events that occurred during the last physics step.

Events are automatically collected when step() is called and stored in the world.

Example

let physics_world = physics.step(physics_world)
let collision_events = physics.get_collision_events(physics_world)

list.each(collision_events, fn(event) {
  case event {
    physics.CollisionStarted(a, b) ->
      io.println(a <> " started colliding with " <> b)
    physics.CollisionEnded(a, b) ->
      io.println(a <> " ended colliding with " <> b)
  }
})
pub fn get_transform(
  physics_world: PhysicsWorld(id),
  id: id,
) -> Result(transform.Transform, Nil)

Get the current transform of a rigid body.

Queries the physics simulation directly, so it always returns the latest position even for bodies that were just created in the current frame.

Example

let cube_transform = case physics.get_transform(physics_world, Cube1) {
  Ok(t) -> t
  Error(_) -> transform.at(position: vec3.Vec3(0.0, 10.0, 0.0))
}
pub fn get_velocity(
  world: PhysicsWorld(body),
  id: body,
) -> Result(vec3.Vec3(Float), Nil)

Get the current velocity of a rigid body

pub fn new_rigid_body(
  body_type: Body,
) -> RigidBodyBuilder(WithoutCollider)

Create a new rigid body builder

Example

let body = physics.new_rigid_body(physics.Dynamic)
  |> physics.body_collider(physics.Box(2.0, 2.0, 2.0))
  |> physics.body_mass(5.0)
  |> physics.build_body()
pub fn new_world(config: WorldConfig(body)) -> PhysicsWorld(body)

Create a new physics world (call this in your init function)

pub fn raycast(
  world: PhysicsWorld(id),
  origin origin: vec3.Vec3(Float),
  direction direction: vec3.Vec3(Float),
  max_distance max_distance: Float,
) -> Result(RaycastHit(id), Nil)

Cast a ray and return the first hit

Useful for shooting mechanics, line-of-sight checks, and ground detection.

Example

// Cast ray downward from player position
let origin = player_position
let direction = vec3.Vec3(0.0, -1.0, 0.0)

case physics.raycast(world, origin, direction, max_distance: 10.0) {
  Ok(hit) -> {
    // Found ground at hit.distance units below player
    io.println("Hit body with ID")
  }
  Error(Nil) -> {
    // No ground found within 10 units
  }
}
pub fn set_angular_velocity(
  world: PhysicsWorld(body),
  id: body,
  velocity: vec3.Vec3(Float),
) -> PhysicsWorld(body)

Queue an angular velocity change for a rigid body during the next physics step. Returns updated world with the command queued.

pub fn set_velocity(
  world: PhysicsWorld(body),
  id: body,
  velocity: vec3.Vec3(Float),
) -> PhysicsWorld(body)

Queue a velocity change for a rigid body during the next physics step. Returns updated world with the command queued.

pub fn step(world: PhysicsWorld(id)) -> PhysicsWorld(id)

Step the physics simulation forward This should be called in your update function each frame Returns updated world with new transforms for all bodies

pub fn with_angular_damping(
  builder: RigidBodyBuilder(a),
  damping: Float,
) -> RigidBodyBuilder(b)

Set the angular damping for the rigid body

pub fn with_body_ccd_enabled(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Enable continuous collision detection for the rigid body

pub fn with_collider(
  builder: RigidBodyBuilder(a),
  collider: ColliderShape,
) -> RigidBodyBuilder(WithCollider)

Set the collider shape for the rigid body

pub fn with_collision_groups(
  builder: RigidBodyBuilder(a),
  membership membership: List(Int),
  can_collide_with filter: List(Int),
) -> RigidBodyBuilder(b)

Set collision groups for filtering which objects can collide

Example

// Player belongs to layer 0, collides with enemies (1) and ground (2)
let body = physics.new_rigid_body(physics.Dynamic)
  |> physics.body_collider(physics.Capsule(1.0, 0.5))
  |> physics.body_collision_groups(
    membership: [0],
    filter: [1, 2]
  )
  |> physics.build_body()
pub fn with_friction(
  builder: RigidBodyBuilder(a),
  friction: Float,
) -> RigidBodyBuilder(b)

Set the friction for the rigid body

pub fn with_linear_damping(
  builder: RigidBodyBuilder(a),
  damping: Float,
) -> RigidBodyBuilder(b)

Set the linear damping for the rigid body

pub fn with_lock_rotation_x(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Lock rotation on the X axis (pitch)

pub fn with_lock_rotation_y(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Lock rotation on the Y axis (yaw)

pub fn with_lock_rotation_z(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Lock rotation on the Z axis (roll)

pub fn with_lock_translation_x(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Lock translation on the X axis

pub fn with_lock_translation_y(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Lock translation on the Y axis

pub fn with_lock_translation_z(
  builder: RigidBodyBuilder(a),
) -> RigidBodyBuilder(b)

Lock translation on the Z axis

pub fn with_mass(
  builder: RigidBodyBuilder(a),
  mass: Float,
) -> RigidBodyBuilder(b)

Set the mass for the rigid body

pub fn with_restitution(
  builder: RigidBodyBuilder(a),
  restitution: Float,
) -> RigidBodyBuilder(b)

Set the restitution (bounciness) for the rigid body

Search Document