Pathfinding can be a crucial aspect of game development, for example, when you want enemy characters to chase the
player and kick their butts defeat them in a challenging and satisfying manner, or when an enemy is supposed to
patrol through a series of checkpoints. To achieve this, you need to find a path between two points in the game world.
But how do you find a way from one point to another? Well, there are algorithms for this, one of which is the
famous A* search algorithm.
The A* search algorithm is a popular choice for this purpose. It not only finds a path between two points (if there is
such a path), it finds the shortest path possible. While applying this algorithm to a game with a top-down
perspective is straight-forward, it can become
a challenge in a side-scrolling platformer scenario.
We will start the series by having a look at the simple case of a top-down game and how the A* algorithm is applied.
Later in the series we’ll look into adapting the algorithm such that it also works in a side-scrolling situation.
A* Pathfinding in a Top-Down Game
Let’s assume we have a grid of square tiles and that the
character can move freely in 4 directions being up, down, left and right.
The character can go up, down, left and right.
In addition some of the cells are blocked and hence are not walkable
by the character which means a path needs to go around those blocked cells. These are typically obstacles such as
walls, trees, water, or … pyramids:
A grid with free and blocked cells.
Let’s further assume the character is at a certain position named A on the grid (i.e. in a particular cell or
tile) and they need to go to some other position B.
We need to find a path from A to B.
The A* algorithm’s input is basically the grid with all its blocked and non-blocked cells, position A and position B.
Its output is the shortest path (if such a path exists) from A to B which is basically a list of cells.
Setting Up the Environment
First, we need to represent our game world in a way that the A* algorithm can process. This involves creating a grid
where each cell corresponds to a tile and therefore to a position in the game world. Each cell can be walkable or
unwalkable. Here’s a possible implementation in C# for a Cell class - To make it easier to deal with coordinates, we
throw a struct Point into the mix and add a property to Cell accordingly:
The properties are pretty much self-explanatory: Position, X, Y and IsWalkable store the cell position in the grid and if the cell is walkable.
Now a grid can simply be represented by a 2-dimensional array of Cell objects. First we create a 2-dimensional array
of int, where 0 means walkable and 1 means blocked. That way we can define the game world in a simpler and more
concise way:
We then turn this into an array of Cell objects. Since the first index is the y-coordinate but we’d like it to be the
x-coordinate for convenience reasons we need to transpose the map matrix:
To make sure we don’t end up with Cell objects with invalid coordinates with respect to the grid they’re in, let’s whip
up a Grid class with a static constructor which incorporates the transpose magic as well as a method for getting a cell
at a certain position following the Try-Parse Pattern:
publicclassGrid{publicrequiredCell[,]Cells{get;init;}privateGrid(){}publicstaticGridCreateFromArray(int[,]map){varcells=newCell[map.GetLength(1),map.GetLength(0)];for(vary=0;y<map.GetLength(0);y++){for(varx=0;x<map.GetLength(1);x++){cells[x,y]=newCell(x,y,isWalkable:map[y,x]==0);}}returnnewGrid{Cells=cells};}/// <summary>/// Try get the cell at the given position/// </summary>/// <param name="pos">The position to get the cell at from the grid</param>/// <param name="cell">The cell at the position if it exists</param>/// <returns>true on success, false on failure</returns>publicboolTryGetCellAtPosition(Pointpos,outCellcell){if(pos.X<0||pos.X>=Cells.GetLength(0)||pos.Y<0||pos.Y>=Cells.GetLength(1)){cell=newCell(0,0,false);returnfalse;}cell=Cells[pos.X,pos.Y];returntrue;}}
A* Algorithm Implementation
Now that we have the grid we can move on to the algorithm itself.
The A* algorithm is a generalization and extension of Dijkstra's algorithm.
It belongs to the class of so-called informed search algorithms which use an estimation function (heuristic) to search
in a targeted way and thus reduce the runtime. The A* algorithm always looks at the cells that are likely to lead quickly
to the destination first. To figure out which cell is the best bet, we assign each known cell a cost value based on a metric.
This gives us an idea of how long the path from the start to the destination would be in the best case if we used that cell.
Then, we look at the cell with the lowest value.
For that we need to extend the Cell class from earlier:
We already looked at isWalkable as well as X, Y and Position.
CostFromStart is the cost (in our case cost is the same as distance) from the start cell to this cell. It’s initialized
with float.PositiveInfinity, because we don’t know the distance yet.
CostToTarget is the heuristic estimated cost (or distance) of the path from the start cell to the target cell
passing through this cell. This is also initialized with float.PositiveInfinity.
Parent is used to track from which cell we came from. This is later used to reconstruct the path.
Here’s the core of the A* pathfinding algorithm. We assume the cost for going from one cell to an adjacent cell is 1.
What the algorithm basically does is the following:
Initialization: Set the starting cell’s cost to reach itself (CostFromStart) to 0 and add it to openSet, a set
of cells to evaluate.
Main Loop: Run while openSet has cells. Get the cell with the lowest estimated cost from the start to the target
via that cell (CostToTarget) from openSet.
Check for Target: If this cell is the target, then we’ve found the path and the algorithm stops.
Process Neighbors: Otherwise, for each neighbor of the cell calculate the cost to reach the cell plus one (tentativeCost).
If it’s is less than the neighbor’s current cost (which is +∞ for all cells initially), then we’ve found a shorter path
to the neighbor. Set the neighbor’s CostFromStart = tentativeCost and CostToTarget = tentativeCost + GetDistance(n, target),
and update its parent cell. Then add the neighbor to openSet if not already present.
Repeat until the target is found or openSet is empty in which case the target isn’t reachable and thus there is no path.
What is the GetDistance(n, target) method in step 4 above? This is the so-called heuristic whose purpose is to estimate the
distance between the current cell and the target. The typical choice in a case like ours is the Manhattan distance.
The Manhattan distance between two points in a grid is basically the sum of the absolute differences of their coordinates:
Math.Abs(a.X-b.X)+Math.Abs(a.Y-b.Y)
It’s also known as the Taxicab distance, because it represents the shortest path a taxi would take in a city with a
rectangular grid of streets. Unlike Euclidean distance, it only considers horizontal and vertical movements,
not diagonal ones.
Here’s a flowchart visualizing the algorithm:
A flowchart showing the algorithm
And here’s an implementation of the algorithm outlined above:
publicclassAStarPathfinder{publicList<Cell>FindPath(PointstartPos,PointtargetPos,Gridgrid){// If startPos or targetPos is out of bounds, return an empty pathif(!grid.TryGetCellAtPosition(startPos,outvarstart))return[];if(!grid.TryGetCellAtPosition(targetPos,outvartarget))return[];start.CostFromStart=0;start.CostToTarget=GetDistance(start,target);List<Cell>openSet=[start];while(openSet.Count>0){varcurrent=openSet.OrderBy(c=>c.CostToTarget).First();if(current==target)returnReconstructPath(current);openSet.Remove(current);vartentativeCost=current.CostFromStart+1;foreach(varninNeighbors(current,grid.Cells).Where(n=>tentativeCost<n.CostFromStart)){n.Parent=current;n.CostFromStart=tentativeCost;n.CostToTarget=tentativeCost+GetDistance(n,target);if(!openSet.Contains(n))openSet.Add(n);}}return[];}privatestaticList<Cell>ReconstructPath(Cellcurrent){List<Cell>path=[current];varwalkingCell=current;while(walkingCell.Parent!=null){path.Insert(0,walkingCell.Parent);walkingCell=walkingCell.Parent;}returnpath;}privatestaticfloatGetDistance(Cella,Cellb){returnMath.Abs(a.X-b.X)+Math.Abs(a.Y-b.Y);}privatestaticList<Cell>Neighbors(Cellcell,Cell[,]grid){List<Cell>neighbors=[];int[]dx=[-1,1,0,0];int[]dy=[0,0,-1,1];for(vari=0;i<4;i++){varcheckX=cell.X+dx[i];varcheckY=cell.Y+dy[i];if(checkX>=0&&checkX<grid.GetLength(0)&&checkY>=0&&checkY<grid.GetLength(1)&&grid[checkX,checkY].IsWalkable){neighbors.Add(grid[checkX,checkY]);}}returnneighbors;}}
Example Grid Setup
Let’s put everything together and whip up a console program with our example grid:
By the way, you can find all of the above code at GitHub.
Here is the path visualized (the upper left cell’s coordinates are (0, 0)):
A pathfinding diagram showing a grid with green cells representing a path.
Bonus: Interactive Example
Let’s take the algorithm outlined above and assemble an app that lets you watch the A* algorithm at work:
Click to load app
The interactive example lets you step through the algorithm step by step or watch it do its work. You can also manipulate
the grid by changing cells to be free or blocked.
Walkthrough
Note
When we say “click” we also mean “touch” interactions on touchscreen devices. You can use both mouse clicks and touch
events to interact with the app.
When you load the app, you see the same map with cells and obstacles as before. Below the map there are a few buttons:
Play/Stop: When first clicked, the algorithm will automatically run at a relatively slow pace so you can watch it in all its beauty; when clicked again, it will stop.
Step: Click to execute a single step of the algorithm.
Reset: Click to reset the algorithm.
Show/Hide Parent: When switched on, shows the relationship of the cells to their parent cells.
You can even manipulate the cells:
Click and hold the start cell (A) or target cell (B) to move them around.
Click and hold a free or a blocked cell to “paint” or “erase” walls.
When the algorithm is running, the cells change color:
Orange: These cells are in the openSet and are about to be examined.
Yellow: These cells have their parent cell set.
Green: These cells are part of the path from A to B.
Additionally, cells that are being checked by the algorithm display two numbers. The first number in the upper left
corner displays the cost from the start cell to this cell. The second number below shows the estimated cost from the start to the
destination via that cell.
Example:The cost
from the start to this cell is 7, and the estimated cost from the start to the target via this cell is 10.
A Note about the Example Application
The application is made with KNI, a MonoGame derivative with support for WebGL. This may be
overkill for the task at hand, but I always wanted to try KNI and its WebGL capabilities, and this seemed like a small enough
project to give it a try.
Conclusion
In this article, we looked at the basics of using A* pathfinding in a 2D top-down game. We set out the basics of how the
A* algorithm can find the shortest path between two points on a grid. Next, we’ll look at how to apply A* pathfinding in
a 2D side-scrolling platformer. This will involve adapting the algorithm to account for additional challenges like gravity.