tiramisu
Tiramisu game engine main module - immutable game loop with effect system.
This module provides the core game loop following the Model-View-Update (MVU) architecture, inspired by Lustre. Your game state is immutable, and updates return new state along with effects.
Quick Example
import gleam/option
import tiramisu
import tiramisu/camera
import tiramisu/effect
import tiramisu/geometry
import tiramisu/material
import tiramisu/scene
import tiramisu/transform
import vec/vec3
type Model {
Model(rotation: Float)
}
type Msg {
Tick
}
type Ids {
Cube
MainCamera
}
pub fn main() {
tiramisu.run(
dimensions: option.None,
background: tiramisu.Color(0x111111),
init: init,
update: update,
view: view,
)
}
fn init(_ctx: tiramisu.Context(Ids)) {
#(Model(rotation: 0.0), effect.tick(Tick), option.None)
}
fn update(model: Model, msg: Msg, ctx: tiramisu.Context(Ids)) {
case msg {
Tick -> {
let new_rotation = model.rotation +. ctx.delta_time
#(Model(rotation: new_rotation), effect.tick(Tick), option.None)
}
}
}
fn view(model: Model, _ctx: tiramisu.Context(Ids)) {
let assert Ok(cam) = camera.perspective(field_of_view: 75.0, near: 0.1, far: 1000.0)
let assert Ok(cube_geo) = geometry.box(width: 1.0, height: 1.0, depth: 1.0)
let assert Ok(cube_mat) =
material.new()
|> material.with_color(0xff0000)
|> material.build()
[
scene.Camera(
id: MainCamera,
camera: cam,
transform: transform.at(position: vec3.Vec3(0.0, 0.0, 5.0)),
look_at: option.None,
active: True,
viewport: option.None,
),
scene.Mesh(
id: Cube,
geometry: cube_geo,
material: cube_mat,
transform: transform.identity
|> transform.rotate_y(model.rotation),
physics: option.None,
),
]
}
Types
Game context passed to init and update functions.
Contains timing information, input state, canvas dimensions, and physics world for the current frame.
Example
fn update(model: Model, msg: Msg, ctx: Context) {
// Move player based on delta time for smooth motion
let speed = 5.0
let new_x = model.x +. speed *. ctx.delta_time
// Convert screen coordinates to world space
let world_x = { screen_x -. ctx.canvas_width /. 2.0 } /. 100.0
let world_y = { ctx.canvas_height /. 2.0 -. screen_y } /. 100.0
// Check if space key is pressed
case input.is_key_down(ctx.input, "Space") {
True -> jump(model)
False -> Model(..model, x: new_x)
}
}
pub type Context(id) {
Context(
delta_time: Float,
input: input.InputState,
canvas_width: Float,
canvas_height: Float,
physics_world: option.Option(physics.PhysicsWorld(id)),
input_manager: @internal InputManager,
)
}
Constructors
-
Context( delta_time: Float, input: input.InputState, canvas_width: Float, canvas_height: Float, physics_world: option.Option(physics.PhysicsWorld(id)), input_manager: @internal InputManager, )
Canvas dimensions for the game window.
Used with tiramisu.run()
to specify the size of the game canvas.
If not provided (None), the game will run in fullscreen mode.
Example
import gleam/option.{None, Some}
import tiramisu
// Fullscreen mode
tiramisu.run(
dimensions: None,
background: tiramisu.Color(0x111111),
// ...
)
// Fixed size
tiramisu.run(
dimensions: Some(tiramisu.Dimensions(width: 800.0, height: 600.0)),
background: tiramisu.Color(0x111111),
// ...
)
pub type Dimensions {
Dimensions(width: Float, height: Float)
}
Constructors
-
Dimensions(width: Float, height: Float)
Values
pub fn run(
dimensions dimensions: option.Option(Dimensions),
background background: background.Background,
init init: fn(Context(id)) -> #(
state,
effect.Effect(msg),
option.Option(physics.PhysicsWorld(id)),
),
update update: fn(state, msg, Context(id)) -> #(
state,
effect.Effect(msg),
option.Option(physics.PhysicsWorld(id)),
),
view view: fn(state, Context(id)) -> List(scene.Node(id)),
) -> Nil
Initialize and run the game loop.
This is the main entry point for your game. It sets up the renderer, initializes your game state,
and starts the game loop. The loop will call your update
and view
functions each frame.
Parameters
dimensions
: Canvas dimensions (width and height). UseNone
for fullscreen mode.background
: Background as Color, Texture, or CubeTexture (seeBackground
type)init
: Function to create initial game state and effectupdate
: Function to update state based on messagesview
: Function to render your game state as scene nodes
Camera Setup
You must include a Camera
scene node with active: True
in your initial scene.
Use effect.set_active_camera(id)
to switch between cameras at runtime.
Example
import gleam/option.{None, Some}
import tiramisu
import tiramisu/camera
import tiramisu/effect
import tiramisu/scene
import tiramisu/transform
import vec/vec3
type Model {
Model(rotation: Float)
}
type Msg {
Tick
}
pub fn main() {
// Fullscreen mode with color background
tiramisu.run(
dimensions: None,
background: tiramisu.Color(0x111111),
init: fn(_ctx) {
#(Model(rotation: 0.0), effect.tick(Tick), option.None)
},
update: fn(model, msg, ctx) {
case msg {
Tick -> #(
Model(rotation: model.rotation +. ctx.delta_time),
effect.tick(Tick),
option.None,
)
}
},
view: fn(model, _ctx) {
let assert Ok(cam) = camera.perspective(field_of_view: 75.0, near: 0.1, far: 1000.0)
let assert Ok(geo) = geometry.box(width: 1.0, height: 1.0, depth: 1.0)
let assert Ok(mat) = material.new() |> material.with_color(0xff0000) |> material.build()
[
scene.Camera(
id: "main-camera",
camera: cam,
transform: transform.at(position: vec3.Vec3(0.0, 0.0, 5.0)),
look_at: option.None,
active: True,
viewport: option.None,
),
scene.Mesh(
id: "cube",
geometry: geo,
material: mat,
transform: transform.identity
|> transform.rotate_y(model.rotation),
physics: option.None,
),
]
},
)
}