Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Canvas, Sound, and Games

A language whose values never change sounds like the last place to write a 60-frames-per-second game. Mar’s examples folder disagrees: a pseudo-3D racer, a raycasting dungeon crawler, a vertical shoot-em-up, a rhythm runner, a real-time strategy game with flow-field pathfinding. All pure Mar, all immutable, all shipping. This chapter is not a game-dev tutorial; it is about why the architecture holds at 60fps, which is the strongest possible stress test of everything this book has claimed.

The canvas is a value too

Canvas is the second drawing surface, for worlds instead of widgets. Where the UI vocabulary is semantic, the canvas is geometric: rectangles, circles, triangles, text, grouped and transformed. A game’s view returns a draw list, an ordinary immutable value describing one frame:

view : Model -> View Msg
view model =
    Canvas.canvas [ Canvas.onTap Tapped, Canvas.onResize Resized ]
        (List.concat
            [ [ Canvas.rect 0 0 model.w model.h (Canvas.rgb 13 9 12) ]
            , stars model
            , List.map drawShip model.ships
            , hud model
            ])

No retained scene graph, no sprite objects with positions to mutate. Every frame, the world (the Model) is rendered to a fresh description, and the runtime blits it. Groups apply transforms (Canvas.Translate, Canvas.Rotate, degrees as integers) and blend modes to whole sub-lists, which is how a windmill spins or a glow adds light without any element ever being “moved”.

A game is just a fast page

Recall the subscriptions chapter: a game is an MVU page whose subscriptions requests a fast tick and the input streams:

subscriptions model =
    Sub.batch
        [ Time.every (Time.millis 16) Tick
        , Keyboard.onKeyDown KeyDown
        , Keyboard.onKeyUp KeyUp
        ]

update receives Tick and advances the simulation one step: new positions, collisions, spawns, all computed as the next immutable world from the previous one, in fixed-point integer math (positions in sixteenths of a pixel, the racer’s road projection, the crawler’s raycaster: integers throughout, as the numbers chapter promised). The runtime keeps game time honest against wall-clock time even when a device drops frames, so a slow phone sees fewer frames of the same-speed game rather than a slow-motion one.

The remarkable part is what did not need adding: no entity-component system, no special game loop API, no mutable particle pools. The RTS holds hundreds of units; its Model is one record holding lists of units, buildings, and shots; its update is the rules of the game as a pure function. Purity here is not a tax paid for elegance. It is the reason a desync between the game’s server and client replays is structurally impossible in the card game example, whose one rules engine, written in shared Mar, is executed independently by the Go backend and the JS frontend and must agree, and does, because integer math plus pure functions equals bit-identical replay.

Sound as data

The same philosophy reaches audio. Sound is a chip-tune synthesizer: notes, chords, sweeps, vibrato, sequences, described as values and played, looped, or attached to the world by the runtime. The rhythm runner drives its entire soundtrack from the game world itself, one groove step per beat line, so the music cannot drift from the action: they are the same data.

Input rounds out the kit: Keyboard and Gamepad mirror each other as subscriptions, Canvas.onTap / onDrag / onAltTap / onWheel cover pointers and touch, and Device reports capabilities (touch-only, can-hover) so games choose control schemes by facts rather than user-agent guessing.

Why this chapter is in a theory book

Because games are the adversarial audit of the language’s claims. Immutability at 60fps, integers for physics and 3D projection, effects-as-data for real-time audio, one codebase running identical logic on two runtimes: if any of Part I were marketing, this is where it would collapse. Instead the games get the same dividends the CRUD apps get, plus one nobody expects until they feel it: the time-travel debugger works on games. Scrub backward through a boss fight; every frame is just a Model, and Models keep.

The remaining question for any real project is the least romantic one: how does all this get to users?