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

Handy maths for games

A game is full of little sums. Which way is the player? How far away is the treasure? How do I make the camera glide instead of jump? You won’t need much maths for any of it, and I’ve got the pieces ready for you.

Which way is it?

atn2(y, x) gives the angle of a step x across and y down. Give it the difference between where something is and where it’s looking, and you know which way to face.

SCREEN 320, 240
turret = (160, 120)
target = (260, 220)
d = target - turret
angle = atn2(d.y, d.x)
PRINT "in radians:", fixed_str(angle, 3)
PRINT "in degrees:", fixed_str(angle * 180 / pi, 1)
in radians: 0.785
in degrees: 45.0

Taking one point from another gives the step between them, a pair you read with .x and .y. The angle comes back in radians, where a whole turn is 2 * pi. Angles start pointing right and grow clockwise, because y grows downwards on the screen. Sprites turn in degrees, so * 180 / pi converts.

Here’s a turret that follows the mouse, and fires where it’s pointing when you click:

SCREEN 320, 240
MEMORYBUFFER "turret", 32, 32
    CIRCLE (16, 16), 10, 8, 8
    LINE (16, 13) -> (31, 18), 7 !! box fill
END MEMORYBUFFER
MEMORYBUFFER "shot", 6, 6
    CIRCLE (3, 3), 2, 14, 14
END MEMORYBUFFER

// Just off each edge of the screen: a shot that reaches one is done.
BOUNDARY ADD "top", (-40, -40) -> (360, -20)
BOUNDARY ADD "bottom", (-40, 260) -> (360, 280)
BOUNDARY ADD "left", (-40, -40) -> (-20, 280)
BOUNDARY ADD "right", (340, -40) -> (360, 280)

BEHAVIOR "fly"
    VELOCITY PARAM("vx", 0), PARAM("vy", 0)
    ONCOLLISION
        IF other < 0 THEN DISABLE SPRITE me
    END ONCOLLISION
END BEHAVIOR

turret = reserve_sprite()
SET SPRITE turret !! image("turret") x(160) y(120) origin((0.5, 0.5))
SHOW SPRITE turret

angle = 0
DO
    IF COOLDOWN(1)
        angle = atn2(mousey - 120, mousex - 160)
        SET SPRITE turret !! rotate(angle * 180 / pi)
    END IF
    IF COOLDOWN(10) AND mousedown
        shot = reserve_sprite()
        IF shot >= 0
            SET SPRITE shot !! image("shot") origin((0.5, 0.5))
                !! x(160 + cos(angle) * 16) y(120 + sin(angle) * 16)
                !! behavior("fly") behavior_params({vx: cos(angle) * 4, vy: sin(angle) * 4})
            SHOW SPRITE shot
        END IF
    END IF
LOOP

The barrel is drawn pointing right, which is angle 0, so turning the sprite by the angle points it at the mouse. Going the other way, cos(angle) and sin(angle) turn an angle back into a step: how far across and how far down one pixel in that direction is. Times 4, that’s a shot’s speed, and times 16, the end of the barrel.

How far is it?

mag is how long a step is, so mag(b - a) is how far it is from a to b. It’s how a guard notices you, or how a magnet decides a coin is close enough to pull.

SCREEN 320, 240
player = (100, 100)
coins = [(130, 140), (300, 20), (90, 60)]
FOR coin = coins
    away = mag(coin - player)
    IF away < 50
        PRINT coin, "is", round(away), "away: pull it in"
    ELSE
        PRINT coin, "is", round(away), "away: leave it"
    END IF
NEXT
(130, 140) is 50 away: leave it
(300, 20) is 215 away: leave it
(90, 60) is 41 away: pull it in

distance(a, b) says the same thing in words, if you’d rather.

Smooth moves

lerp(a, b, t) is the point a share t of the way from a to b: 0 is a, 1 is b and 0.5 is halfway. Every tick, move something a small share of the way to where it’s going, and it moves fast when it’s far away and slows down as it gets there. That’s easing.

clamp(value, low, high) keeps a value between two others, so things stay where they’re allowed to be.

Here’s a camera that eases after the hero on a map bigger than the screen. Click the screen and use the arrow keys:

SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
LOADIMG "hero.png", "crashtan"

hero = reserve_sprite()
x = 288
y = 256
SET SPRITE hero !! image("crashtan") x(x) y(y) layer(2)
SHOW SPRITE hero

camera_x = 160
camera_y = 168
DO
    IF COOLDOWN(1)
        IF keydown("Left") THEN x = x - 3
        IF keydown("Right") THEN x = x + 3
        IF keydown("Up") THEN y = y - 3
        IF keydown("Down") THEN y = y + 3
        // The map is 640 x 736 pixels and the hero is 40 x 40.
        x = clamp(x, 0, 600)
        y = clamp(y, 0, 696)
        SET SPRITE hero !! x(x) y(y)

        // Where the camera would like to be: the hero in the middle,
        // but never past the edges of the map.
        camera_x = lerp(camera_x, clamp(x + 20 - 160, 0, 640 - 320), 0.08)
        camera_y = lerp(camera_y, clamp(y + 20 - 120, 0, 736 - 240), 0.08)
        FOR layer = 1 ... 2
            SET LAYER layer
            SET SCROLL camera_x, camera_y
        NEXT
    END IF
LOOP

The map’s two layers are on screen layers 1 and 2, so both scroll together. The hero is a sprite on layer 2, and scrolling that layer takes it along. Try 0.3 in place of 0.08 for a camera that keeps up better, or 0.02 for a lazy one. lerp works on pairs too: lerp((0, 0), (100, 50), 0.5) is (50, 25).

Whole numbers

Positions and speeds are rarely whole, but tiles, lives and scores are. There are a few ways to lose the fraction:

3.7-3.7
intdown3-4
floordown3-4
ceilup4-3
fixtowards zero3-3
roundto the nearest4-4
SCREEN 320, 240
x = 75.5
y = 140.2
PRINT "standing on tile", int(x / 32), int(y / 32)
hp = 7
PRINT "hearts to draw:", ceil(hp / 2)
PRINT "score:", round(1234.56)
PRINT int(3.7), floor(3.7), ceil(3.7), fix(3.7), round(3.7)
PRINT int(-3.7), floor(-3.7), ceil(-3.7), fix(-3.7), round(-3.7)
standing on tile 2 4
hearts to draw: 4
score: 1235
3 3 4 3 4
-4 -4 -3 -3 -4

Random, but the same every time

rnd is a random number from 0 up to (not quite) 1. int(rnd * 6) + 1 rolls a die. My random numbers come from a seed, and RANDOMIZE sets it: with no seed it uses the clock, so every game is different.

With a seed, it’s the same numbers every time. That’s how a game makes a huge level from one number, and makes it again just the same:

SCREEN 320, 240
FUNCTION rocks(level)
    RANDOMIZE level
    placed = []
    FOR n = 1 ... 3
        PUSH placed, (int(rnd * 320), int(rnd * 240))
    NEXT
    RETURN placed
END FUNCTION

PRINT "level 7:", rocks(7)
PRINT "level 8:", rocks(8)
PRINT "level 7:", rocks(7)
RANDOMIZE
level 7: [(9, 73), (45, 130), (87, 228)]
level 8: [(113, 183), (313, 108), (193, 47)]
level 7: [(9, 73), (45, 130), (87, 228)]

Level 7 has its rocks in the same places both times. The RANDOMIZE at the end goes back to the clock, so what comes after isn’t the same every game.

Lots of yes-or-no in one number

A number is made of bits, each one a yes or a no. That makes one number a tidy place to keep lots of flags: which keys you’ve found, which levels are done, which doors are open. Number the flags from 0.

bitset(n, b) turns flag b on, bittest(n, b) asks whether it’s on, and bitclear(n, b) turns it off. shl(1, b) is the number with only flag b on, and bitor and bitand put flags together and pick them out. shl and shr slide every bit up or down a number of places.

SCREEN 320, 240
RED_KEY = 0
BLUE_KEY = 1
GOLD_KEY = 2

keys = 0
keys = bitset(keys, RED_KEY)
keys = bitset(keys, GOLD_KEY)
PRINT "the keys, as one number:", keys
PRINT "got the blue key?", bittest(keys, BLUE_KEY)
PRINT "got the gold key?", bittest(keys, GOLD_KEY)

// The big door needs red and gold.
door = bitor(shl(1, RED_KEY), shl(1, GOLD_KEY))
PRINT "the big door opens?", bitand(keys, door) = door
PRINT "every flag moved down one:", shr(keys, 1)
the keys, as one number: 5
got the blue key? 0
got the gold key? 1
the big door opens? 1
every flag moved down one: 2

The keys are 5: flag 0 is worth 1 and flag 2 is worth 4. Moved down one place, the red key falls off the end and the gold key’s flag becomes flag 1, worth 2. Five keys, or fifty levels, still save as one number with SAVETEXT.

Crash-tan’s Tip: bitand and AND are different things. AND asks whether both sides are true: 12 AND 10 is 1. bitand keeps the bits both sides have: bitand(12, 10) is 8.