This week, enemies progressed to toddler stage: they now chase players!
And we've got a new type of enemy too.
Pathfinding
Making enemies figure out how to move to an objective is called "pathfinding".
One straightforward approach is:
- When you make a level, also place some "waypoints" (invisible markers) in your level.
- For every waypoint, connect waypoint A with waypoint B if an enemy can move straight from A to B.
- When an enemy wants to move to some destination:
- It finds the origin waypoint: the closest waypoint to the start.
- Likewise, it finds the best destination waypoint.
- Somehow it plans a route from the origin waypoint to the destination waypoint.
- It moves to the origin waypoint and follows the route until reaching the destination.
When you read about pathfinding, step 2.3
(planning the route) is the bit everyone talks about: what algorithm to use, how to make it run fast, how to reuse results, etc.1
But in a game with destructible terrain, you can't place waypoints beforehand. And calculating them dynamically seemed like a hassle.2
Not Pathfinding
I realized enemies pretty much always want to move towards visible destinations, so I punted on the cleverness and came up with this:
- Decide on the destination
- Try to move towards it, in the horizontal direction only
- If you "see"3 a hole below you, jump to get over the hole
- If you "see" a wall in front of you, jump to get over the wall
That's it! 4
It's.. surprisingly good? Like, it doesn't work perfectly, but enemies feel a lot more dangerous now.
And I can probably slap a more complicated pathfinding-based approach on top later. 5
Updating Enemy Brains
Last week, we used a state machine to model pretty much all enemy behavior.
When I added in a state for "chasing player to get in range", we ended up with arrow storms:
To fix it, I reworked the enemy AI:
- Perception: look around to see all player targets (if any) and remember those targets.
- Memory: pick a target (prefer the last target, followed by targets being faced towards).
- Decision making: decide on a move goal or attack goal based on the target.
- Here's the state machine: each state decides where to move and where to attack.
- Movement: do our best to move towards the current move goal.
- Firing: if we're not moving, fire our weapon at our attack goal.
This moves the enemy AI towards a model of "make decisions, then move and attack like the player can".
And concretely, it solves the arrow storm by moving the weapon cooldown timer into step 5, where it's independent of our state machine's state.
Who Let The Dogs Out
I did! With enemies chasing players with some level of competence, I figured it was time to add an enemy that's deadly in close quarters:
Next steps
Things we still need to fix are:
- Enemies move with the same movement characteristics as the player - they jump exactly as high, run as fast, etc. It's very hard to outrun a hound without a conveniently-placed platform to escape onto!
- Enemies don't have any animations. Well, I guess the player doesn't either, so that's fair :)
- Most importantly, it's pretty hard to kill the enemies now! Our meager spell selection sucks against moving targets.
Playable web build
Can you kill all the enemies without dying? (I couldn't.)
Okay sometimes people talk about step 2.4
as well: if enemies follow waypoints very precisely then they can look a bit too robotic. And in a platformer, getting from one waypoint to another can be a bit tricky if enemies need to time their jumps like players do.
One clever way would be to "invert" the terrain colliders: that would create navigation meshes, where we know that every bottom edge of the 2D mesh is a floor (modulo the borders of every 64x64 chunk, which would have to be handled specially). But we don't generate terrain colliders everywhere because they're expensive to calculate, so you need a heuristic to figure out which extra ones to calculate for pathfinding, or a as-needed calculation driven by pathfinding. Or we smear generating the pathfinding waypoints/navmesh over X updates of the game world to amortize the performance impact, and deal with the world changing under us. Like I said, a hassle.
What do our inspirations do? Noita seems to have proper pathfinding, seemingly updated every few game ticks (with special behavior for enemies). Broforce's enemy behavior usually looks like it's using a naive approach: suicide bomber runs towards enemy, reaches edge of cliff, jumps, falls off the level and dies. But Broforce does have the Scout Mook which will run to an alarm post & activate it, and I have seen them reliably use a ladder to do it. So perhaps some Broforce enemies do have full pathfinding?
The "seeing" is implemented by raycasting (reminder: draw a line, find the intersections) and shapecasting (raycasting but with a shape, e.g. a rectangle)
That's not quite it. For example, you need to not jump when:
- going up a slope
- going onto a small ledge that we can step up onto (which is up to 3 atoms high currently)
- dropping into a hole that's small
And, one kinda obvious "rule" that's missing is that enemies don't try to jump to reach higher destinations right now. I kinda like that high points are safe though.