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

Integer Worlds

Mar has no floating point. For general programming, The Book of Mar spends a chapter defending that; for games, the defense is shorter, because game developers already know the dirty secret: floats in a simulation are a liability you manage. They drift, they compare unreliably, and they round differently across platforms, which is why serious engines with replays or lockstep multiplayer end up rebuilding fixed-point math anyway. Mar just hands you that endpoint: all game math is Int, exact, and identical on the web, on iOS, and on the server.

(Decimal exists for money-like data and is a fine score multiplier, but simulations live on Int. This chapter is the toolbox.)

Sub-pixel motion: scale your units

The naive velocity is “pixels per tick”, and its smallest nonzero speed (1 px/tick = 60 px/s) is too fast for glide, too coarse for acceleration. The fix is the oldest trick on 8-bit hardware: store positions and velocities in a finer unit, convert only when drawing.

-- positions and velocities in millipixels (1 px = 1000 mp)

type alias Ball =
    { x : Int, y : Int, vx : Int, vy : Int }


stepBall : Ball -> Ball
stepBall b =
    { b
        | x = b.x + b.vx
        , y = b.y + b.vy
        , vy = b.vy + 40          -- gravity: 0.04 px/tick², exactly
    }


drawBall : Ball -> Shape
drawBall b =
    circle (b.x // 1000) (b.y // 1000) 6 (rgb 246 196 83)

Gravity of 40 millipixels per tick per tick gives the floaty arc a platformer wants, tuned in whole numbers. A jump is vy = 0 - 9000. Friction is a multiply-then-divide: vx = vx * 94 // 100 decays by 6% per tick with no float in sight. Everything in the examples that feels smooth (the racer’s road, the soccer ball’s rebound, the FPS raycaster) is this pattern at 1000× or some other scale that fits the game.

Choose the scale per quantity and write it down. 1000 is a good default: readable in the debugger (12500 is obviously 12.5 px) and roomy enough for gentle acceleration.

Division truncates; keep remainders on purpose

// truncates toward zero, and / is not for games (it produces an exact Decimal division, a different world). Two consequences:

  • -7 // 2 is -3, not -4. If you need flooring or wrapping behavior (tile coordinates for a camera that can go negative, animation frames), use the positive-modulo helper from chapter 2, modI, and its companion floor division when needed.
  • Repeated divide-then-accumulate loses the remainder each time. Accumulate in the fine unit and divide only at the edge (drawing, display), exactly as drawBall does.

No trig, and you will not miss it

There is no sin, cos, or sqrt, and the example games ship anyway. The workhorse substitutes:

Squared distances. Every “is it close enough” question compares squared lengths, no root required:

withinR : Int -> Int -> Int -> Int -> Int -> Bool
withinR x1 y1 x2 y2 r =
    let
        dx = x2 - x1
        dy = y2 - y1
    in
    dx * dx + dy * dy <= r * r

Octagonal distance when you need an actual length (aim assist, speed normalization), accurate to a few percent and used verbatim by the shmup to aim shots (iabs is one more two-liner for the math kit: if n < 0 then 0 - n else n):

octd : Int -> Int -> Int
octd dx dy =
    let
        ax = iabs dx
        ay = iabs dy
    in
    imax (imax ax ay) ((ax + ay) * 3 // 4)

Direction tables instead of angles. An 8-way or 16-way game does not need degrees; it needs a list of unit steps at your fixed-point scale:

-- 8 directions, millipixel unit vectors (707 ≈ 0.707 * 1000)
dirs : List { dx : Int, dy : Int }
dirs =
    [ { dx = 1000, dy = 0 }, { dx = 707, dy = 707 }
    , { dx = 0, dy = 1000 }, { dx = 0 - 707, dy = 707 }
    , { dx = 0 - 1000, dy = 0 }, { dx = 0 - 707, dy = 0 - 707 }
    , { dx = 0, dy = 0 - 1000 }, { dx = 707, dy = 0 - 707 }
    ]

The raycasting FPS goes further with a precomputed sine table at 1000× scale, ten lines of data for smooth rotation. When you find yourself wanting continuous angles, a small table at your chosen scale is always the answer, and Canvas.Rotate already covers the visual rotation of sprites in whole degrees.

Headroom

Two practical bounds keep you out of trouble. Mar’s parser rejects literals outside the safe integer range, and intermediate products are where games could reach it: with millipixel coordinates up to a million, dx * dx peaks around 10¹², comfortable everywhere. The rule of thumb: keep multiplied intermediates under about 10¹⁵ and you will never think about this again. If a formula multiplies three scaled quantities, divide once in the middle (a * b // 1000 * c // 1000) instead of once at the end.

Small numbers are a feature

The quiet benefit arrives at 2 a.m., in the time-travel debugger, when the ball is at x = 184500, vx = -2210 instead of x = 184.49999999999997. Integer worlds are legible. Tuning becomes arithmetic you can do in your head, desyncs cannot exist, tests assert exact values, and a replay of last week’s bug still reproduces to the pixel. This is the foundation the next chapter builds chance on.