A* grid-based pathfinding works well for games in which the characters can move freely along both the x- and y-axes. The problem with applying this to platformers is that movement on the y-axis is heavily restricted, due to simulated gravitational forces.
In this tutorial, I'll give a broad overview of how to modify a standard A* algorithm to work for platformers by simulating these gravitational restrictions. (In the next part, I'll walk through actually coding the algorithm.) The adapted algorithm could be used to create an AI character that follows the player, or to show the player a route to their goal, for example.
I assume here that you're already familiar with A* pathfinding, but if you need an introduction, Patrick Lester's A* Pathfinding for Beginners is excellent.
You can play the Unity demo, or the WebGL version (64MB), to see the final result in action. Use WASD to move the character, left-click on a spot to find a path you can follow to get there, and right-click a cell to toggle the ground at that point.
By the end of this series, we'll also have added one-way platforms, extended the code to deal with different sizes of character, and coded a bot that can automatically follow the path! Check out that Unity demo here (or the 100MB+ WebGL version).
Defining the Rules
Before we can adapt the pathfinding algorithm, we need to clearly define what forms the paths themselves can take.
What Can the Character Do?
Let's say that our character takes up one cell, and can jump three cells high.
We won't allow our character to move diagonally through cells, because we don't want it to go through solid terrain. (That is, we won't allow it to move through the corner of a cell, only through the top, bottom, left, or right side.) To move to a diagonally adjacent cell, the character must move one cell up or down, and one cell left or right.
Since the character's jump height is three cells, then whenever it jumps, after it has moved up three times, it should not be able to move up any more cells, but it should still be able to move to the side.
Based on these rules, here is an example of the path the character would take during its maximum distance jump:
Of course the character can jump straight up, or with any combination of left and right movements, but this example shows the kind of approximation we'll need to embrace when calculating the path using the grid.
Representing the Jump Paths With Jump Values
Each of the cells in the path will need to keep data about the jump height, so that our algorithm can detect that the player can jump no higher and must start falling down.
Let's start by assigning each cell's jump value by increasing it by one, each cell, for as long as the jump continues. Of course, when the character is on the ground, the jump value should be 0.
Here's that rule applied to the same maximum-distance jump path from before:
The cell that contains a 6 marks the highest point in the path. This makes sense, since that's twice the value of the character's maximum jump height, and the character alternates moving one cell up and one cell to the side, in this example.
Note that, if the character jumps straight up, and we continue increasing the jump value by one each cell, then this no longer works, because in that case the highest cell would have a jump value of 3.
Let's modify our rule to increase the jump value to the next even number whenever the character moves upwards. Then, if the jump value is even, the character can move either left, right, or down (or up, if they haven't reached a jump value of 6 yet), and if the jump value is odd, the character only move up or down (depending on whether they have reached the peak of the jump yet).
Here's what that looks like for a jump straight up:
And here's a more complicated case:
Here's how the jump values are calculated:
- Start on the ground: jump value is 0.
- Jump horizontally: increase the jump value by 1, so the jump value is 1.
- Jump vertically: increase the jump value to the next even number: 2.
- Jump vertically: increase the jump value to the next even number: 4.
- Jump horizontally: increase the jump value by 1; jump value is now 5.
- Jump vertically: increase the value to the next even number: 6. (The character can no longer move upwards, so only left, right and bottom nodes are available.)
- Fall horizontally: increase the jump value by 1; jump value is now 7.
- Fall vertically: increase the jump value to the next even number: 8.
- Fall vertically: increase the value to the next even number: 10.
- Fall vertically: since the character is now on the ground. set the jump value to 0.
As you can see, with this kind of numbering we know exactly when the character reaches its maximum jump height: it's the cell with the jump value equal to twice the maximum character jump height. If the jump value is less than this, the character can still move upwards; otherwise, we need to ignore the node directly above.
Now that we're aware of the kind of movements the character can make on the grid, let's consider the following setup:
The green cell at position (3, 1) is the character; the blue cell at position (2, 5) is the goal. Let's draw a route that the A* algorithm may choose first to attempt to reach the goal@
The yellow number in the top right corner of a cell is the cell's jump value. As you can see, with a straight upwards jump, the character can jump three tiles up, but no further. This is no good.
We may find more luck with another route, so let's rewind our search and start again from node (3, 2).
As you can see, jumping on the block to the right of the character allowed it to jump high enough to get to the goal! However, there is a big problem here...
In all likelihood, the first route the algorithm will take is the first one we examined. After taking it, the algorithm will not get very far, and will end up back at node (3, 2). It can then search through nodes (4, 2), (4, 3), (3, 3) (again), (3, 4) (again), (3, 5), and finally the goal cell, (2, 5).
In a basic version of the A* algorithm, if a node has been visited once already, then we don't need to process it ever again. In this version, however, we do. This is because the nodes are not distinguished solely by x- and y-coordinates, but also by jump values.
In our first attempt to find a path, the jump value at node (3, 3) was 4; in our second attempt, it was 3. Since in the second attempt the jump value at that cell was smaller, it meant that we could potentially get higher from there than we could during the first attempt.
This basically means that node (3, 3) with a jump value of 4 is a different node than the node at (3, 3) with a jump value of 3. The grid essentially needs to become three-dimensional at some coordinates to accommodate for these differences, like so:
We can't simply change the jump value of the node at (3, 3) from 4 to 3, because some paths use the same node multiple times; if we did that, we would basically override the previous node and that would of course corrupt the end result.
We'd have the same issue if the first path would have reached the goal despite the higher jump values; if we had overridden one of the nodes with a more promising one, then we would have no way to recover the original path.
The Strengths and Weaknesses of Using Grid-Based Pathfinding
Remember, it's the algorithm that uses a grid-based approach; in theory, your game and its levels don't have to.
- Works really well with tile-based games.
- Extends the basic A* algorithm, so you don't have to have two completely different algorithms for flying and land characters.
- Works really well with dynamic terrain; doesn't require a complicated node-rebuilding process.
- Inaccuracy: minimum distance is the size of a single cell.
- Requires a grid representation of each map or level.
Engine and Physics Requirements
Character Freedom vs Algorithm Freedom
It is important for the character to have at least as much freedom of movement as the algorithm expects—and preferably a bit more than that.
Having the character movement match exactly the algorithm's constraints is not a viable approach, due to the discrete nature of the grid and the grid's cell size. It is possible to code the physics in such a way that the algorithm will always find a way if there is one, but that requires you to build the physics for that purpose.
The approach I take in this tutorial is to fit the algorithm to the game's physics, not the other way around.
The main problems occur in edge cases when the algorithm's expected freedom of movement freedom does not match the true, in-game character's freedom of movement.
When the Character Has More Freedom Than the Algorithm Expects
Let's say the AI allows for jumps six cells long, but the game's physics allow for a seven-cell jump. If there is a path that requires the longer jump to reach the goal in the quickest time, then the bot will ignore that path and choose the more conservative one, thinking that the longer jump is impossible.
If there is a path that requires the longer jump and there is no other way to reach the goal, then the pathfinder will conclude that the goal is unreachable.
When the Algorithm Expects More Freedom Than the Character Has
If, conversely, the algorithm thinks that it is possible to jump seven cells away, but the game's physics actually allow only for a six-cell jump, then the bot may either follow the wrong path and fall into a place from which it cannot get out, or try to find a path again and receive the same incorrect result, causing a loop.
(Out of these two problems, it's preferable to let the game's physics allow for more freedom of movement than the algorithm expects.)
Solving These Problems
The first way to ensure that the algorithm is always correct is to have levels which players cannot modify. In this case, you just need to make sure that whatever terrain you design or generate works well with the pathfinding AI.
The second solution to these problems is to tweak the algorithm, the physics, or both, to make sure that they match. As I mentioned earlier, this doesn't mean they need to match exactly; for example, if the algorithm thinks the character can jump five cells upwards, it is fine to set the real maximum jump at 5.5 cells high. (Unlike the algorithm, the game's physics can use fractional values.)
Depending on the game, it could also be true that the AI bot not finding an existing path is not a huge deal; it will simply give up and go back to its post, or just sit and wait for the player.
At this point, you should have a decent conceptual understanding of how A* pathfinding can be adapted to a platformer. In my next tutorial, we'll make this concrete, by actually adapting an existing A* pathfinding algorithm in order to implement this in a game!