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

The Game Loop Is MVU

Every game engine ever written is organized around the same heartbeat:

loop forever:
    read input
    advance the world one step
    draw the world

The classical implementation mutates: an update() method walks an array of entities and changes their fields in place, a renderer walks the same objects and paints them, and input handlers poke flags from the outside. It works, and it is also where game bugs come from: the frame where something read a position mid-change, the entity deleted while another entity’s closure still points at it, the pause menu that forgot to stop one of the seven timers.

Mar runs the same heartbeat with the mutation removed. The loop is the Model-View-Update cycle you know from apps, driven fast:

The game loop

  • The Model is the entire world in one immutable value: every enemy, every particle, every score digit, the camera, the phase.
  • update is the rules. Given one message and the current world, it returns the next world. A Tick advances physics; a key press flips an input flag; nothing else can touch anything.
  • view is the camera crew. Given a world, it returns a description of one frame (a list of shapes). It changes nothing and decides nothing.
  • The runtime is the projector and the mail room: it schedules ticks, delivers input as messages, and blits the draw list to a real screen, on the web or on iOS.

Chapter 2 already showed the whole pattern in the flesh. This part of the book examines each arrow of the diagram: the tick that drives it (next chapter), the draw list (after that), and the two disciplines that make Mar games deterministic, integer math and seeded randomness.

There is no engine object

The thing to internalize is what is missing. There is no Game class, no scene graph, no entity registry, no spawn/despawn lifecycle. “Spawning” an enemy means the next Model’s list has one more record in it. “Despawning” means the next list does not. An enemy cannot dangle, leak, or be updated after death, because it is not an object with an address; it is a value in exactly one place.

This also means the dreaded engine-integration layer does not exist. Your game logic is not called by an engine through callbacks and virtual methods; your functions are the whole program, and the runtime’s only job is to keep calling them.

Why immutability is not slow here

The reflex objection: “you rebuild the whole world 60 times a second?” Two facts dissolve it.

First, rebuilding is mostly reusing. When one enemy moves, the new Model shares every unchanged part of the old one; a record update copies a handful of fields, not the world. Values that did not change are not copied, they are pointed at. This is how every immutable language works and Mar is no exception.

Second, a canvas frame was always going to be redrawn from scratch. UI apps diff trees to avoid touching the DOM; a game blits every frame regardless, so describing the frame fresh from the Model is not extra work, it is the same work with a name. The real performance boundaries are elsewhere (how many shapes, how much logic per tick) and chapter 14 measures them properly.

One doorway

Everything that happens to your game arrives as a Msg through update. The tick, the keyboard, the gamepad, the touch, the resize, a finished sound, a network reply in a multiplayer game: one union type lists them all, and the compiler checks you answered each one in every phase. When a bug does occur, it is reproducible by construction: the same starting Model and the same sequence of messages produce the same wrong world, every time, on every platform. The dev tools exploit this ruthlessly, letting you scrub time backward through your own bug.

That is the machine. Now, the clock that drives it.