A* pathfinding is a pathfinding algorithm very commonly used in games because of its performance in small-to-medium-sized node graphs, its reliability in finding the shortest path from A to B, and its ability to be tweaked and modified. This makes it ideal for determining a path from one entity located on a node graph to another, for example in the case of an angry dungeon-dwelling goblin trying to find the shortest path to the intruding player.
The specific algorithm I have implemented boils down to the following:
- Set the
current node
to be the starting path node and add it to theopen list
. Thecurrent node
is the node we’re currently processing and focusing on. Theopen list
is a list of nodes which have adjacent nodes we haven’t looked at yet. - While the
open list
isn’t empty, do the following:- Add the
current node
to theclosed list
and remove it from theopen list
. Theclosed list
is a list of nodes we’ve finished processing and don’t have to come back to. - For each node adjacent to
current node
, do the following:- If the adjacent node isn’t traversable or is in the
closed list
then skip steps 2.2.2 and 2.2.3. - If it’s not already in the
open list
then add it to theopen list
and set itsparent node
to thecurrent node
. This is important for generating the actual path later: we need to be able to trace our way back to the starting path node. We now need to calculate the node’sG
andH
components. TheG
component is calculated by taking the G component of thecurrent node
and adding to it the distance between thecurrent node
and the adjacent node. TheH
here stands for Heuristic, and hence is an approximation of how far the node is from the target node. In my implementation theH
component is simply the distance between the node and the target node, ignoring any obstacles. - If the adjacent node is already in the
open list
then we can check to see if we can improve itsG
component. All we need to do is compare the currentG
component with what theG
component would be if thecurrent node
was its parent. If it’s lower in the latter case then we update theG
component with the new value and also update itsparent node
.
- If the adjacent node isn’t traversable or is in the
- Check to see if the
open list
is empty. If it is this means that there is no path available and the process stops. - Search the
open list
for the node with the lowestF
component (G
+H
) and set thecurrent node
to it. - Check to see if the
current node
is now the same as the target node. If it is then we can generate our path. This is simply a case of addingcurrent node
to the path, settingcurrent node
to theparent node
ofcurrent node
, and repeating this process untilcurrent node
is the node we started pathfinding from; the start node. - If the
open list
isn’t empty and we’ve still yet to find our target, then repeat steps (2.1) through (2.4).
- Add the
While a bit of a mouthful, that’s all there is to it! The actual code that goes along with this implementation is by no means complicated at all and is extremely straight forward. I’ve written an implementation of this algorithm in JavaScript so it can be easily available on the web. While most of the actual JS file consists of code for the UI and utility methods, there are two relevant chunks of code: the findPath()
function, and the PathNode
class.
First, let’s have a look at the class we’ll be using to represent each node:
// A* PATHFINDING IMPLEMENTATION
// -----------------------------
/**
* Represents a single node in a larger collection of nodes
* @param x PathNode X position
* @param y PathNode Y position
* @param z PathNode Z position
* @param walkable Specifies whether the node is traversable or not
*/
function PathNode(x, y, z, walkable) {
this.pos = new Vec3(x, y, z);
this.walkable = walkable;
this.g = 0; // G: distance to path start via linked parent nodes
this.h = 0; // H: distance to path target (estimated via)
this.f = 0; // F: Overall score
this.adjacentNodes = []; // List of nodes that can be accessed from this one
this.parentNode = null; // The node which this node points back to for when
// tracing a path back to the start
/** Set the parent node of this node */
this.setParent = function(parentNode) {
this.parentNode = parentNode;
// Here the G component of the node is calculated by taking the parent
// node's G component and increasing it by the
// distance between the two nodes
this.g = parentNode.g + Vec3.distance(parentNode.pos, this.pos);
// Update the F component
this.f = this.g + this.h;
}
/** Set the path's target node */
this.setTarget = function(targetNode) {
// The H component of the node is calculated here by finding the distance
// between this node and the specified target node
this.h = Vec3.distance(targetNode.pos, this.pos);
// Update the F component
this.f = this.g + this.h;
}
}
While I’m using the word “class” JS is of course a prototypal language where everything is an object. This allows us to use functions
in a similar way to how we might use classes in a more Object-Oriented feature-rich language like C++, because these functions can have attributes and can be instantiated.
First of all when we call PathNode
to instantiate a new PathNode
object, we pass in four parameters: an X, Y, and Z position, and a flag (walkable
) which represents whether the node we’re creating is traversable by entities or not. Next we have the properties that are needed for the pathfinding algorithm:
- The three pathfinding scores/components:
G
,H
, andF
. - An array of nodes representing all the nodes which can be directly accessed from this one.
- This node’s
parentNode
.
Finally we have a couple of methods which the pathfinding algorithm will use to set the node’s G and H components.
That’s all there really is to it; nothing particularly special. The main thing to note regarding this is that it’s designed to be quite flexible. The nodes can be positioned anywhere within 3D space, for example we could have a vast collection of nodes littered across the surface of some 3D terrain, providing us with a system that allows entities to navigate across virtual mountain ranges. Once we have our nodes positioned, the actual links between them exist by the contents of each node’s adjacentNodes
array. We might have nodes with just a few nearby neighbours, or we could have some nodes that have a bunch of neighbours, some near, some very far away. However just because we can have these complex node graphs doesn’t mean we have to! We can set up a uniform 2D node graph quite easily, enabling us to use this implementation not just in 3D games with complex terrain, but also 2D games that might have restricted tile-based entity movement - a 2D dungeon crawler for example.
Now that we know how a PathNode
works, let’s have a look at the main attraction; the findPath()
function itself:
/**
* Uses the A* Pathfinding algorithm to find a path of nodes between two specified nodes
* @param startNode The PathNode where the path begins
* @param targetNode The PathNode we want to search towards
* @returns The path, if found. Returns an empty array if no path can be found
*/
function findPath(startNode, targetNode) {
var openList = [startNode]; // Nodes that haven't been processed - initially
// just the start node.
closedList = []; // Nodes that have been processed.
var currentNode = startNode; // The current node being looked at.
// Only while we still have nodes to process...
while (openList.length != 0) {
// First we swap the node out from the openList and in to the closedList
closedList.push(currentNode);
remove(openList, currentNode);
// For each of the nodes accessible via the current node...
for (var n = 0; n < currentNode.adjacentNodes.length; n++) {
var adj = currentNode.adjacentNodes[n];
// If we can't walk on it, don't bother do anything with it.
if (adj.walkable) {
// If we've never touched it before then give it a parent, set the
// target, and add it to the open list for future processing.
if (closedList.indexOf(adj) == -1 && openList.indexOf(adj) == -1) {
adj.setParent(currentNode);
adj.setTarget(targetNode);
openList.push(adj);
} else if (adj.g > currentNode.g + Vec3.minus(currentNode.pos, adj.pos).magnitude())
// We've already calculated this nodes F value, but we can check
// if we can improve it! If the G value is better when the
// current node is the parent, then make the switch.
adj.setParent(currentNode);
}
}
var openListLength = openList.length;
// If there are no more nodes in the open list then we couldn't find a path,
// return empty handed.
if (openListLength == 0) {
return [];
} else {
// Find the next node with the lowest F score to be processed.
currentNode = openList[0];
for (var n = 1; n < openListLength; n++) {
if (openList[n].f < currentNode.f)
currentNode = openList[n];
}
}
// Check to see if we've found the target node.
if (currentNode == targetNode) {
var path = [];
currentNode = currentNode.parentNode;
// Simply trace our way back to the start node via all of the parent
// nodes.
while (currentNode != startNode) {
path.push(currentNode);
currentNode = currentNode.parentNode;
}
// The path is back-to-front at the moment with the target at the
// begginning - so we return the reversed path.
return path.reverse();
}
}
}
The two things the A* pathfinding algorithm needs to know about to do its job are the node where our path begins, and the node where we want our path to take us to - so we pass these in to the function as arguments. As this really is just a direct implementation of the algorithm that was outlined toward the beginning of this post, there’s not much to say about the code. As mentioned before, in terms of actual code it’s relatively simple.
Something worth talking about is the way that the path is generated once the algorithm has been successful. The first thing we do after finding out the node currently being processed is actually the target node is setting the currentNode
variable to the target node’s parent:
currentNode = currentNode.parentNode;
This is because this specific implementation is tweaked so that the generated path takes us next to the target, and not on the target. This behaviour is desirable when an entity wants to find another entity, such as when an enemy might want to get near the player to attack them, or an NPC trying to get close to a door to open it. If we did actually want the path to lead us on to the target, then we simply would skip this step. The rest of the path generation is extremely simple and consists of two steps:
- Add
currentNode
to thepath
array - Set
currentNode
to theparentNode
ofcurrentNode
- Repeat the above until
currentNode
is equal tostartNode
Or in actual JavaScript:
while (currentNode != startNode) {
path.push(currentNode);
currentNode = currentNode.parentNode;
}
See; easy! We could happily stop there and return the path we’d have at the moment, however I’d like the path the entity recieves to be in the reverse order:
return path.reverse();
To me this makes more sense. This way the index of any given node in the path
array corrisponds with how far into the path the node actually is. The node path[0]
will be the first node that an entity will travel to on its path, path[1]
will be the next, and so on.
You can play around with the implementation here. Prior to having any knowledge of how A* pathfinding worked I found this article by Patrick Lester which explains it excellently.
Update: The link to the article above has since been disabled. There are plenty of online resources out there which do a great job at breaking down and explaining this algorithm if this article doesn’t quite do it for you. Here’s one such example on YouTube.