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 | ||
|---|---|---|---|
int | down | 3 | -4 |
floor | down | 3 | -4 |
ceil | up | 4 | -3 |
fix | towards zero | 3 | -3 |
round | to the nearest | 4 | -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:
bitandandANDare different things.ANDasks whether both sides are true:12 AND 10is 1.bitandkeeps the bits both sides have:bitand(12, 10)is 8.