> Cinder 0.1.0 documentation: Build your first game
> Canonical page: https://cinder.ash10.dev/docs/tutorial/ · Source: `docs/TUTORIAL.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# Build your first game

This tutorial builds a small game on Cinder from nothing: a red ball you push around a lit
ledge to knock six crates off the edge. It takes about an hour. The finished code is in
`examples/first_game/` and is compiled and run headless by `tests/examples/test_first_game.mojo`,
so every snippet below is code that builds under the pinned toolchain. Each `# Step N` comment in
`examples/first_game/game.mojo` marks where that step's code lives.

What you will use: the `App` runtime and `Game` trait (`cinder/app`), the ECS world and its
built-in components (`cinder/ecs`, `cinder/scene`), the physics world (`cinder/physics`), the
renderer's meshes, materials and lights (`cinder/render`), input actions (`cinder/input`) and
an ImGui HUD through the raw bindings. Deeper material on each lives in `docs/guides/`.

## 0. Prerequisites

Follow the README quick start once: the toolchain container, `pixi run native`, and
`pixi run test` green. Every command below runs inside the container
(`tools/in_container.sh ...`), on Linux x86-64.

```bash
tools/in_container.sh pixi run example               # the finished game, windowed
tools/in_container.sh pixi run example -- --headless --frames 120 --screenshot .cache/first.png
```

## 1. An App and a Game

A Cinder program is an `App` that owns every subsystem (window or offscreen target, GPU device,
renderer, ECS world, physics, animation, audio, input, debug UI) and a `Game`, a struct you write
that implements four methods. `App.run_frame` calls them in a fixed order every frame:

| Method | When | Use it for |
|---|---|---|
| `start(mut self, mut app)` | once, on the first frame | loading content, spawning entities, binding input |
| `fixed_update(mut self, mut app, dt)` | zero or more times per frame, at `AppConfig.fixed_hz` (60 Hz) | anything that reads or drives physics |
| `update(mut self, mut app, dt)` | once per frame, variable `dt` | timers, camera, UI state |
| `draw_ui(mut self, mut app)` | once per frame, inside an ImGui frame | HUD, menus |

`examples/first_game/main.mojo` is the whole entry point. It parses the engine options
(`--headless`, `--width`, `--height`, ...) with `parse_app_args`, which hands back the arguments
it did not recognise, builds the `App`, and loops:

```mojo
var config = AppConfig(String("First game"), 1280, 720, False)
config.audio = False
var rest = parse_app_args(args, config)
var app = App(config^)
var game = FirstGame()
while app.run_frame(game):
    frames += 1
    if max_frames >= 0 and frames >= max_frames:
        break
game.shutdown(app)
_ = game^
_ = app^
```

The last three lines matter. Mojo destroys a value at its last use, so without the explicit
order the `App` could be destroyed while the game still holds physics bodies that live in it.
The game is ended first, then the app, always.

The game struct carries only what it needs to remember between frames:

```mojo
struct FirstGame(Game):
    var shapes: List[Shape]
    var bodies: List[UInt32]
    var ball: UInt32
    var crates: List[UInt32]
    var fallen: Int
    var won: Bool
    var elapsed: Float32
    var ui: CStrings
```

## 2. A ledge, crates and a ball

Content is entities in `app.world`. Visible things carry a `LocalTransform` (or a `RigidBody`,
which writes the transform for you) and a `MeshRenderer` that names a mesh and a material
registered with the renderer. Physics objects live in `app.physics[]` and are linked to entities
by the `RigidBody` component.

Register the meshes and materials once. `cube` and `plane` are built-in primitives; the second
`add_mesh` argument says whether to build LODs with meshoptimizer:

```mojo
var floor_mesh = app.renderer[].add_mesh(String("ledge"), plane(LEDGE_HALF, 1), False)
var box_mesh = app.renderer[].add_mesh(String("box"), cube(0.5), True)
var grey = app.renderer[].add_material(Material(String("grey")))
var red = Material(String("red"))
red.base_color = Vec4(0.9, 0.15, 0.1, 1.0)
red.metallic = 0.2
red.roughness = 0.35
var red_id = app.renderer[].add_material(red)
```

The ledge is a static box in physics and a plane in the renderer. Static bodies are created
inactive (the second `create_body` argument), on layer 0; the default `LayerConfig` makes layer 0
the static layer that collides with everything but itself:

```mojo
var ledge_shape = app.physics[].box(Vec3(LEDGE_HALF, 0.5, LEDGE_HALF))
self.shapes.append(ledge_shape)
self.bodies.append(app.physics[].create_body(
    BodyDesc(ledge_shape, Vec3(0.0, -0.5, 0.0), MotionType.static(), 0), False))
var ledge = app.world.spawn()
app.world.add[LocalTransform](ledge, LocalTransform(Mat4.identity()))
app.world.add[MeshRenderer](ledge, MeshRenderer(floor_mesh, grey, False))
```

Dynamic crates share one shape (shapes are reference counted by Jolt; the game keeps the handle
so it can release it at the end). A `RigidBody` component makes the engine copy the body's
position and rotation into the entity's transform after every fixed step, interpolated to the
render time:

```mojo
var crate_shape = app.physics[].box(Vec3(0.5, 0.5, 0.5))
for i in range(CRATE_COUNT):
    var p = Vec3(Float32(i) * 1.2 - Float32(CRATE_COUNT - 1) * 0.6, 0.5, -2.5)
    var crate_desc = BodyDesc(crate_shape, p, MotionType.dynamic(), 1)
    crate_desc.mass = 10.0
    var body = app.physics[].create_body(crate_desc)
    var e = app.world.spawn()
    app.world.add[RigidBody](e, RigidBody(body, MotionType.dynamic(), p, Quat.identity()))
    app.world.add[MeshRenderer](e, MeshRenderer(box_mesh, wood_id, True))
```

The ball is the same recipe with a sphere shape. Masses are set explicitly: left at zero, Jolt
derives mass from the shape's volume at the density of water, which makes a one-metre crate
weigh a tonne. The crates are 10 kg (`crate_desc.mass = 10.0`) and the ball 20 kg. Two more
`BodyDesc` fields tune the ball: damping so it stops rolling when you let go, and
`allow_sleeping = False` so Jolt never puts the player's body to sleep while a force is about to
be applied.

## 3. A sun and a camera

Lights and cameras are entities too. A `LightSource` shines down its entity's -Z axis, so a
directional light is placed with a look-at matrix; the last argument asks for cascaded shadow
maps. The camera is the entity with an active `CameraComponent`; its transform is the camera's
world matrix (the inverse of a view matrix):

```mojo
var sun = app.world.spawn()
app.world.add[LocalTransform](sun, LocalTransform(
    Mat4.look_at(Vec3(6.0, 10.0, 4.0), Vec3.zero(), Vec3.unit_y()).inverse()))
app.world.add[LightSource](sun, LightSource(
    LightKind.directional(), Vec3(1.0, 0.95, 0.9), 3.0, 0.0, 0.0, 0.0, True))

var camera = app.world.spawn()
app.world.add[LocalTransform](camera, LocalTransform(
    Mat4.look_at(Vec3(0.0, 7.0, 9.0), Vec3(0.0, 0.0, -0.5), Vec3.unit_y()).inverse()))
app.world.add[CameraComponent](camera, CameraComponent(0.9, 0.05, True))
```

`CameraComponent` takes the vertical field of view in radians and the near plane; the renderer
uses reverse-Z with an infinite far plane, so there is no far distance to pick. Image-based
lighting comes from the renderer's default environment, so the scene is lit from all directions
even with a single sun.

Run it now (`pixi run example`): the crates settle on the ledge, the ball sits in front of them,
the sun casts shadows. Nothing moves yet.

## 4. Input as actions and axes

Gameplay never reads keys directly. It binds named axes and actions in `start`, and reads them
by name; the same names can be rebound at runtime (`InputMap.listen_action`) and saved as text
(`InputMap.save`). Keys are SDL scancodes:

```mojo
app.input.bind_axis(String("move_x"), Binding.key(KEY_D, 1.0))
app.input.bind_axis(String("move_x"), Binding.key(KEY_A, -1.0))
app.input.bind_axis(String("move_z"), Binding.key(KEY_S, 1.0))
app.input.bind_axis(String("move_z"), Binding.key(KEY_W, -1.0))
app.input.bind_axis(String("move_x"), Binding.gamepad_axis(0, 1.0))
app.input.bind_action(String("jump"), Binding.key(KEY_SPACE))
app.input.bind_action(String("restart"), Binding.key(KEY_R))
```

Several bindings on one axis add up, clamped to [-1, 1] for digital bindings (mouse deltas are
left in pixels); a gamepad stick on the same axis just works.

## 5. Pushing the ball: the fixed step

Everything that drives physics goes in `fixed_update`, which runs at exactly 60 Hz however fast
the screen refreshes, so the game behaves the same on every machine (and the headless test can
replay it). Forces are per step; impulses are instantaneous:

```mojo
def fixed_update(mut self, mut app: App, dt: Float32) raises:
    if self.won:
        return
    var push = Vec3(app.input.axis(String("move_x")), 0.0, app.input.axis(String("move_z")))
    if push.length() > 1.0:
        push = push.normalized()
    app.physics[].add_force(self.ball, push * PUSH_FORCE)
    if app.input.just_pressed(String("jump")) and self.ball_grounded(app):
        app.physics[].add_impulse(self.ball, Vec3(0.0, 80.0, 0.0))
    self.fallen = 0
    for body in self.crates:
        if app.physics[].position(body).y() < FALL_Y:
            self.fallen += 1
    if self.fallen == CRATE_COUNT:
        self.won = True
    if app.physics[].position(self.ball).y() < FALL_Y:
        self.reset_ball(app)
```

`just_pressed` is true for exactly one frame's worth of fixed steps, so a held key fires once.
Teleporting a body is `set_position_rotation` with `activate = True`, followed by zeroing its
velocities, which is what `reset_ball` does.

## 6. Per-frame logic

`update` runs once per rendered frame with the real elapsed time. It is the place for timers,
camera smoothing and anything cosmetic:

```mojo
def update(mut self, mut app: App, dt: Float32) raises:
    if not self.won:
        self.elapsed += dt
    if app.input.just_pressed(String("restart")):
        self.restart(app)
```

## 7. A HUD

`draw_ui` is called inside an ImGui frame. The bindings are the raw dcimgui C API on
`app.lib.ptr()`, and ImGui wants NUL-terminated C strings that stay alive for the frame, which is
what `CStrings` does (clear it at the start of each `draw_ui`):

```mojo
def draw_ui(mut self, mut app: App) raises:
    self.ui.clear()
    var lib = app.lib.ptr()
    lib[].ImGui_SetNextWindowPos(ImVec2(12.0, 12.0), Int32(ImGuiCond_Always))
    if lib[].ImGui_Begin(self.ui.c(String("hud")),
                         Optional[Pointer[Bool, MutUntrackedOrigin]](),
                         Int32(ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize
                               | ImGuiWindowFlags_NoInputs)):
        lib[].ImGui_TextUnformatted(self.ui.c(
            String(t"crates knocked off {self.fallen}/{CRATE_COUNT}   time {Int(self.elapsed)} s")))
    lib[].ImGui_End()
```

The engine's own overlay is on by default in windowed builds (`--no-debug-ui` hides it) and
appears next to yours, with the frame-time graph, the entity inspector (your entities are there),
the render graph and the log.

## 8. Cleaning up

Shapes and bodies are owned by whoever created them. `shutdown` destroys the bodies and then
the shapes, and `main` calls it before the game and the app go out of scope. The headless test
checks `app.physics[].body_count() == 0` afterwards, and `App` itself asserts that no GPU
resource leaked when it is destroyed.

```mojo
def shutdown(mut self, mut app: App):
    for b in self.bodies:
        app.physics[].destroy_body(b)
    for s in self.shapes:
        app.physics[].destroy_shape(s)
```

## 9. Running it headless, and testing it

`--headless` renders to an offscreen texture and reads it back; `--frames N` stops after N
frames; `--screenshot PATH` writes the last frame. That is how the test drives the game without
a keyboard: it lines the ball up behind each crate and applies the same force the move axis would,
checks that every crate drops off and the game reports `won`, restarts, reads a frame and checks
that the GPU reported no validation errors.

```bash
tools/in_container.sh pixi run mojo run -I . -I examples tests/examples/test_first_game.mojo
```

## Where to go next

- Load real content: `cinder/assets` imports glTF (`load_gltf`, `upload_gltf`) and
  `cinder/scene.spawn_gltf` turns a file into entities; `docs/guides/assets.md`.
- A walking character instead of a ball: `CharacterController` in `cinder/physics` and an
  `Animator` with a blend tree in `cinder/anim`; the sample game (`sample/`) does both.
- Sound: `AudioEngine` buses, one-shots and ECS `AudioSource`s; `docs/guides/audio.md`.
- Saving: make your components `SceneComponent`s and write the world with `SceneRegistry`
  (`docs/guides/scene.md`); the sample's F5/F9 save and load are forty lines.
- Performance: `docs/BENCHMARKS.md`, and `tests/perf/test_zero_alloc.mojo` for the rule that a
  steady-state frame allocates nothing.
