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

# First game: complete source

The finished code of the tutorial, copied verbatim from `examples/first_game/` in the engine repository. The engine's test `tests/examples/test_first_game.mojo` compiles it and runs it headless, so it builds under the pinned toolchain (Mojo 1.2.0). Each `# Step N` comment marks where that tutorial step's code lives.

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

## `examples/first_game/main.mojo`

```mojo
"""Entry point of the tutorial game (docs/TUTORIAL.md, step 1).

    pixi run example [--headless] [--frames N] [--screenshot PATH]
"""
from std.sys import argv

from cinder.app import App, AppConfig, parse_app_args
from cinder.core.image import save_png

from first_game import FirstGame


def main() raises:
    var args = List[String]()
    var raw = argv()
    for i in range(1, len(raw)):
        args.append(String(raw[i]))
    var config = AppConfig(String("First game"), 1280, 720, False)
    config.audio = False
    var rest = parse_app_args(args, config)
    var max_frames = -1
    var screenshot = String()
    var i = 0
    while i < len(rest):
        if rest[i] == "--frames" and i + 1 < len(rest):
            max_frames = Int(rest[i + 1])
            i += 1
        elif rest[i] == "--screenshot" and i + 1 < len(rest):
            screenshot = rest[i + 1]
            i += 1
        else:
            raise Error(t"unknown argument {rest[i]}")
        i += 1
    var headless = config.headless
    var app = App(config^)
    var game = FirstGame()
    var frames = 0
    while app.run_frame(game):
        frames += 1
        if max_frames >= 0 and frames >= max_frames:
            break
    if screenshot.byte_length() > 0 and headless:
        save_png(app.lib.ptr(), screenshot, app.read_frame())
    print(
        t"frames {frames}, won {game.won}, gpu errors {app.gpu_error_count()}"
    )
    # Step 8: the game's bodies live in the app's physics world, so end the game first.
    game.shutdown(app)
    _ = game^
    _ = app^
```

## `examples/first_game/game.mojo`

```mojo
"""The game built in docs/TUTORIAL.md: a ball you push around a lit floor to knock crates off a
ledge. Every numbered step of the tutorial is a `# Step N` comment in this file."""
from std.memory import Pointer

from cinder.app import App, Game
from cinder.core.math.mat4 import Mat4
from cinder.core.math.quat import Quat
from cinder.core.math.vec import Vec3, Vec4
from cinder.ecs import Entity, LocalTransform
from cinder.ffi.cinder_native import (
    ImGuiCond_Always,
    ImGuiWindowFlags_AlwaysAutoResize,
    ImGuiWindowFlags_NoDecoration,
    ImGuiWindowFlags_NoInputs,
    ImVec2,
)
from cinder.input import Binding
from cinder.physics import BodyDesc, MotionType, RigidBody, Shape
from cinder.render import Material, cube, plane
from cinder.render.lights import LightKind
from cinder.scene.render_sync import CameraComponent, LightSource, MeshRenderer
from cinder.tools import CStrings

# SDL scancodes used by the default bindings.
comptime KEY_A = 4
comptime KEY_D = 7
comptime KEY_R = 21
comptime KEY_S = 22
comptime KEY_W = 26
comptime KEY_SPACE = 44

comptime CRATE_COUNT = 6
comptime LEDGE_HALF = 4.0
comptime FALL_Y: Float32 = -3.0
comptime PUSH_FORCE: Float32 = 300.0
"""Newtons applied to the 20 kg ball at full stick deflection."""


struct FirstGame(Game):
    """A ball pushed by the player knocks crates off a ledge; knock them all off to win.
    """

    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

    def __init__(out self):
        self.shapes = List[Shape]()
        self.bodies = List[UInt32]()
        self.ball = 0
        self.crates = List[UInt32]()
        self.fallen = 0
        self.won = False
        self.elapsed = 0.0
        self.ui = CStrings()

    # Step 2: content lives in the ECS world; physics bodies are created in the physics world and
    # linked to entities through the RigidBody component.
    def start(mut self, mut app: App) raises:
        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 ball_mesh = app.renderer[].add_mesh(
            String("ball"), cube(0.35), True
        )
        var grey = app.renderer[].add_material(Material(String("grey")))
        var wood = Material(String("wood"))
        wood.base_color = Vec4(0.7, 0.45, 0.2, 1.0)
        var wood_id = app.renderer[].add_material(wood)
        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: a static box the size of the visible plane.
        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)
        )

        # Crates along the far edge.
        var crate_shape = app.physics[].box(Vec3(0.5, 0.5, 0.5))
        self.shapes.append(crate_shape)
        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)
            self.crates.append(body)
            self.bodies.append(body)
            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 the player pushes.
        var ball_shape = app.physics[].sphere(0.35)
        self.shapes.append(ball_shape)
        var start = Vec3(0.0, 0.35, 2.5)
        var ball_desc = BodyDesc(ball_shape, start, MotionType.dynamic(), 1)
        ball_desc.mass = 20.0
        ball_desc.linear_damping = 0.6
        ball_desc.allow_sleeping = False
        self.ball = app.physics[].create_body(ball_desc)
        self.bodies.append(self.ball)
        var ball = app.world.spawn()
        app.world.add[RigidBody](
            ball,
            RigidBody(self.ball, MotionType.dynamic(), start, Quat.identity()),
        )
        app.world.add[MeshRenderer](ball, MeshRenderer(ball_mesh, red_id, True))

        # Step 3: a shadowed sun and a camera. Lights and cameras are entities too.
        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))

        # Step 4: input is named actions and axes, bound to keys (rebindable at runtime).
        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_axis(String("move_z"), Binding.gamepad_axis(1, 1.0))
        app.input.bind_action(String("jump"), Binding.key(KEY_SPACE))
        app.input.bind_action(String("restart"), Binding.key(KEY_R))
        app.physics[].optimize_broad_phase()

    # Step 5: gameplay that touches physics runs in the fixed step, at a constant rate.
    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)

    def ball_grounded(self, app: App) -> Bool:
        return app.physics[].position(self.ball).y() < 0.4

    def reset_ball(mut self, mut app: App):
        app.physics[].set_position_rotation(
            self.ball, Vec3(0.0, 0.35, 2.5), Quat.identity(), True
        )
        app.physics[].set_linear_velocity(self.ball, Vec3.zero())
        app.physics[].set_angular_velocity(self.ball, Vec3.zero())

    # Step 6: per-frame logic (timers, camera, UI state) runs once per rendered frame.
    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)

    def restart(mut self, mut app: App):
        for i in range(len(self.crates)):
            var p = Vec3(
                Float32(i) * 1.2 - Float32(CRATE_COUNT - 1) * 0.6, 0.5, -2.5
            )
            app.physics[].set_position_rotation(
                self.crates[i], p, Quat.identity(), True
            )
            app.physics[].set_linear_velocity(self.crates[i], Vec3.zero())
            app.physics[].set_angular_velocity(self.crates[i], Vec3.zero())
        self.reset_ball(app)
        self.fallen = 0
        self.won = False
        self.elapsed = 0.0

    # Step 7: the HUD is immediate-mode ImGui through the bindings; CStrings keeps the C strings
    # alive for the frame.
    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
            ),
        ):
            var line = String(
                t"crates knocked off {self.fallen}/{CRATE_COUNT}   time"
                t" {Int(self.elapsed)} s"
            )
            if self.won:
                line = String(t"you win in {Int(self.elapsed)} s   R restarts")
            lib[].ImGui_TextUnformatted(self.ui.c(line))
            lib[].ImGui_TextUnformatted(
                self.ui.c(String("WASD push, Space hop, R restart"))
            )
        lib[].ImGui_End()

    # Step 8: physics objects belong to the game; release them before the App goes away.
    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)
        self.bodies.clear()
        self.shapes.clear()
```

## `examples/first_game/__init__.mojo`

```mojo
"""The tutorial game from docs/TUTORIAL.md."""
from .game import CRATE_COUNT, PUSH_FORCE, FirstGame
```
