Enemies that think
A bad guy that walks straight at you gets stuck on the first wall. One that finds its way round, notices when it can see you, and wanders about when it can’t, feels alive. I have a set of functions for exactly that, and none of them needs more than a line.
Thinking on a grid
Most of them think about the level as a grid: a list of rows, where 0 is a
square you can walk on and anything else is in the way. That’s the same
shape as the grid of a tile map, and they read it the same way, grid(y, x), a row and then a column. A place on the grid is a coordinate (x, y):
a column and then a row.
maze makes one for you, walled all round. Give it a seed and you get the
same maze every time:
SCREEN 320, 240
grid = maze(15, 7, 42)
FOR y = 0 ... 6
row = ""
FOR x = 0 ... 14
IF grid(y, x) = 0 THEN row = row + "." ELSE row = row + "#"
END FOR
PRINT row
END FOR
###############
#.............#
#.#.#########.#
#.#.#...#.#...#
#.###.###.#.###
#.....#.......#
###############
Without a seed it’s a different maze each time. After the seed it can take a
style: "BACKTRACKER", long winding corridors and the one it starts with;
"PRIM", lots of short dead ends; "DIVISION", straight walls like rooms;
or "CAVE", a cave. And a last number from 0 to 1 knocks out that share of
the dead ends, so there’s more than one way round: maze(21, 15, 7, "PRIM", 0.3).
Finding the way
astar finds the shortest path across a grid from one square to another,
round anything in the way. It gives back the squares to walk through, from
the start to the end, or [] when there’s no way there at all.
SCREEN 320, 240
// A wall down column 3, with a gap at the bottom.
grid = [[0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0]]
path = astar(grid, (0, 0), (5, 0))
PRINT path
PRINT "the first step is to", path(1)
PRINT "into the wall:", astar(grid, (0, 0), (3, 0))
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 2), (5, 1), (5, 0)]
the first step is to (1, 1)
into the wall: []
It goes diagonally as well as straight, so the path cuts across to the gap
and back up. An enemy only needs path(1), the next square, and asks again
later, when you’ve moved.
Crash-tan’s Tip:
astaris quick, but there’s no need to ask it every tick. An enemy that looks again each time it takes a step is every bit as clever, and leaves your loop more time for everything else.
Two more look at the grid around a square. neighbors gives the squares
next to one that you can step on (up, down, left and right), and floodfill
counts every square you can reach from one, which tells you whether a level
you’ve made has bits nobody can get to:
SCREEN 320, 240
grid = [[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]]
PRINT "next to (1, 1):", neighbors(grid, (1, 1))
PRINT "reachable from the left:", floodfill(grid, (0, 0), 0)
PRINT "reachable from the right:", floodfill(grid, (4, 0), 0)
next to (1, 1): [(1, 0), (1, 2), (0, 1)]
reachable from the left: 6
reachable from the right: 6
Can it see you?
raycast draws a straight line between two squares and says whether
anything is in the way: 1 if the line is clear, 0 if it isn’t. That’s line
of sight. A guard that can see you comes running, and one that can’t keeps
to its rounds.
SCREEN 320, 240
grid = [[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]]
PRINT "along the top:", raycast(grid, (0, 0), (4, 0))
PRINT "through the pillar:", raycast(grid, (0, 1), (4, 1))
along the top: 1
through the pillar: 0
Moving about
Not everything lives on a grid. For things that move freely, these work in pixels, and give back a velocity: how far to move this tick, across and down.
seek(from, to, speed)heads straight for something.flee(from, away_from, speed)heads straight away from it.wander(place, heading, radius, distance)gives a new place a little way ahead, wobbling at random: a sheep that’s grazing.distance(a, b)is how far apart two places are, andnearest(place, places)is which of a list of places is closest, as its index.
SCREEN 320, 240
v = seek((0, 0), (30, 40), 5)
PRINT "toward:", v, " away:", flee((0, 0), (30, 40), 5)
PRINT "how far:", distance((0, 0), (30, 40))
PRINT "nearest:", nearest((0, 0), [(50, 50), (5, 5), (90, 0)])
toward: (3, 4) away: (-3, -4)
how far: 50.0
nearest: 1
Here are three of them at once, around the mouse: a chick that follows it, a mouse-shy sheep that runs off when it comes close, and one more sheep that just wanders.
SCREEN 320, 240
MEMORYBUFFER "chick", 10, 10
CIRCLE (5, 5), 4, 14, 14
END MEMORYBUFFER
MEMORYBUFFER "sheep", 14, 10
CIRCLE (7, 5), 6, 15, 15
END MEMORYBUFFER
chick = (60, 120)
shy = (160, 120)
grazer = (260, 120)
heading = 0
FOR n = 0 ... 2
ENABLE SPRITE n
NEXT
SET SPRITE 0 !! image("chick") x(chick.x) y(chick.y) origin((0.5, 0.5))
SET SPRITE 1 !! image("sheep") x(shy.x) y(shy.y) origin((0.5, 0.5))
SET SPRITE 2 !! image("sheep") x(grazer.x) y(grazer.y) origin((0.5, 0.5))
FOR n = 0 ... 2
SHOW SPRITE n
NEXT
DO
IF COOLDOWN(1)
mouse = (mousex, mousey)
IF distance(chick, mouse) > 12 THEN chick = chick + seek(chick, mouse, 1.5)
IF distance(shy, mouse) < 60 THEN shy = shy + flee(shy, mouse, 2)
heading = heading + (rnd - 0.5) * 20
grazer = wander(grazer, heading, 0.3, 0.5)
shy = (clamp(shy.x, 0, 320), clamp(shy.y, 0, 240))
grazer = (clamp(grazer.x, 0, 320), clamp(grazer.y, 0, 240))
SET SPRITE 0 !! x(chick.x) y(chick.y)
SET SPRITE 1 !! x(shy.x) y(shy.y)
SET SPRITE 2 !! x(grazer.x) y(grazer.y)
END IF
LOOP
A little luck
An enemy that does the same thing every time is easy to beat. choose
picks one thing from a list, each as likely as the next, and weighted
picks with odds: the weights [6, 3, 1] make the first thing six times as
likely as the last. Press Space to open a chest:
SCREEN 320, 240
loot = ["a coin", "a potion", "a sword"]
held = FALSE
DO
IF COOLDOWN(1)
IF keydown("Space") AND NOT held
found = weighted(loot, [6, 3, 1])
mood = choose(["Ooh,", "Hey,", "Nice,"])
PRINT mood, found + "!"
END IF
held = keydown("Space")
END IF
LOOP
The chaser
Here’s an enemy that thinks. The level is a maze with a few loops in it, so
there’s somewhere to run. The hero moves a square at a time with the arrow
keys. The chaser asks astar for the way to the hero and takes the next step.
When raycast says it can see the hero, it goes red and speeds up.
SCREEN 320, 240
RANDOMIZE
LOADIMG "hero.png", "hero"
// The level: a maze, 20 squares by 15, drawn as a tile map.
grid = maze(20, 15, 0, "BACKTRACKER", 0.4)
MEMORYBUFFER "tiles", 16, 16
LINE (0, 0) -> (15, 15), 8 !! box fill
LINE (1, 1) -> (14, 7), 7 !! box
END MEMORYBUFFER
TILEMAP "maze"
TILESET "tiles"
TILESIZE 16
SIZE 20, 15
TILES 0 !! from(grid)
END TILEMAP
CREATE MAP "maze" !! layer_to(0)
MEMORYBUFFER "calm", 16, 16
CIRCLE (8, 8), 6, 9, 9
END MEMORYBUFFER
MEMORYBUFFER "angry", 16, 16
CIRCLE (8, 8), 7, 12, 12
END MEMORYBUFFER
TOSPRITESHEET "chaser", ["calm", "angry"]
// The hero starts at the top left, the chaser at the bottom right.
her = (1, 1)
chaser = (17, 13)
hero = reserve_sprite()
SET SPRITE hero !! image("hero") x(her.x * 16 + 8) y(her.y * 16 + 8)
!! origin((0.5, 0.5)) scale((0.5, 0.5))
SHOW SPRITE hero
enemy = reserve_sprite()
SET SPRITE enemy !! image("chaser") frame(0) x(chaser.x * 16) y(chaser.y * 16)
SHOW SPRITE enemy
FUNCTION walkable(place)
RETURN grid(place.y, place.x) = 0
END FUNCTION
DO WHILE her <> chaser
// The hero: a square every 8 ticks while an arrow is held.
IF COOLDOWN(8)
target = her
IF keydown("Left") THEN target = (her.x - 1, her.y)
IF keydown("Right") THEN target = (her.x + 1, her.y)
IF keydown("Up") THEN target = (her.x, her.y - 1)
IF keydown("Down") THEN target = (her.x, her.y + 1)
IF walkable(target) THEN her = target
SET SPRITE hero !! x(her.x * 16 + 8) y(her.y * 16 + 8)
END IF
// The chaser: a square every 6 ticks if it can see the hero, every 14 if not.
sees = raycast(grid, chaser, her)
IF sees
SET SPRITE enemy !! frame(1)
ELSE
SET SPRITE enemy !! frame(0)
END IF
IF (sees AND COOLDOWN(6)) OR (NOT sees AND COOLDOWN(14))
path = astar(grid, chaser, her)
IF len(path) > 1 THEN chaser = path(1)
SET SPRITE enemy !! x(chaser.x * 16) y(chaser.y * 16)
END IF
LOOP
SOUND 110, 0.5
Each COOLDOWN keeps its own time, so the two speeds don’t trip over each
other. Make the chaser smarter or slower, give it a friend, or make it
wander until it first sees you.