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

Sixty Frames on a Budget

Mar’s runtime is a tree-walking interpreter. There is no JIT, no compilation to native code; your step and view are evaluated structure by structure, sixty times a second. This chapter is honest about what that budget buys, how to measure it, and the seven techniques that kept the heaviest example games at sixty.

What the budget actually is

The proof points, all shipping in the repository and all holding 60 fps on ordinary hardware:

  • A real-time strategy game stepping hundreds of units with flow-field pathfinding.
  • A raycasting first-person dungeon crawler (a screen-wide column render, per frame).
  • A pseudo-3D racer projecting a curving, hilly road.
  • A ten-stage shmup with layered parallax, dozens of actors and shots, and per-beat music.

So the budget is generous for 2D-game logic. What it is not is infinite in the direction people expect: the cost is rarely your game rules; it is almost always the size of the frame description. A thousand game-logic decisions are cheap; a thousand shapes in the draw list, rebuilt and serialized every frame, are the actual bill.

Measure before touching anything

Dev mode surfaces frame cost (real milliseconds per frame, not just the frame rate, which quantizes to 60/30 and hides everything). Watch it while playing the busiest moment of your game: boss plus bullets plus particles. The reading tells you which world you are in:

  • Under ~8 ms: ship it. Do not optimize.
  • 8 to 16 ms: you are spending headroom; fine on desktop, watch a phone.
  • Over 16 ms: dropped frames; time for this chapter.

Then find out which half: temporarily return a minimal frame from view (one rect). If the cost collapses, it is the draw list; if it does not, it is step. That one experiment aims all the work below.

Shrinking the draw list

In rough order of payoff, as measured on the racer during its optimization rounds:

1. Draw less scenery. The single biggest wins were density cuts: fewer sky bands, fewer snowflakes, fewer roadside trees. Decorative counts are tuning constants; halve them and look. Nobody missed the fourth layer of stars.

2. Cull off-screen early. Skip actors outside the viewport before building their shapes, with a cheap bounds test. One if per entity beats four shapes per entity.

3. Level-of-detail by distance. Far things are small: draw the 1-rect version beyond a threshold and the 6-shape version near. The racer’s roadside sprites and the FPS’s far walls both do this.

4. Precompute static geometry. Anything that does not depend on the frame (the road’s segment table, a level’s wall rects, a pixel-font’s glyph rects) belongs in a top-level value, computed once, not rebuilt in view. (Mind the file-order rule from the previous chapter.)

5. Hoist the colors. rgb calls inside a per-entity loop allocate every frame. Name colors at the top level (you did this for the palette anyway) and inner loops just reference them.

Taming step

6. Stagger the brains. AI does not need to think at 60 Hz. The RTS re-plans a slice of its units per tick (unit index modulo N equals tick modulo N); every unit still moves each tick, but pathfinding amortizes 10× with no visible change.

7. Cap the populations. Particles, shots, corpses, floating numbers: give each list a maximum and drop the oldest. Bounded lists turn worst-case frames into average frames, and no player has ever counted your particles.

Beyond the seven: prefer a single fold that does three things over three traversals of the same list; keep squared-distance checks before expensive logic, not after; and leave update’s message dispatch alone; it is never the problem.

Phones, and the 1x dividend

The same code runs on iOS with its own natively optimized runtime; the interpreter fast paths were tuned specifically so game workloads (arithmetic and record updates in hot loops) run lean. The letterboxed 1x canvas from chapter 5 is the other half of mobile performance: the fill-rate story that would sink a retina-resolution canvas simply never starts.

Two mobile-specific habits: test the busiest scene on the oldest device you care about, not your dev machine, and remember that a warm phone throttles: a game at 15 ms per frame on a cool phone is a game at 19 ms twenty minutes later. Target 12 or better where you can. The tick’s catch-up machinery (chapter 4) keeps the game correct under throttling, but frames you never drop feel best.

The order of operations

When a game is slow, do these in order and stop when fast: measure; determine draw versus step; cut scenery density; cull and LOD; precompute and hoist; stagger AI; cap lists. Resist rewriting game rules for speed; in every measured case so far, the rules were innocent, and clarity you trade away during a panic never comes back.