Introduction

For Spellirium we had to develop a system that could determine the most efficient path between a series of nodes on a point. After some diligent research and careful consideration it became clear that the A* search algorithm was the way to go.

Now, I was familiar with the basics behind A* but had never actually implemented it. Thankfully though I had a copy of Keith Peter’s Advanced ActionScript 3.0 Animation which has a very nice chapter all about path finding and A*.

Why read this post? Just buy the book instead. ;)

Before we begin, some prerequisite knowledge is required. In order to follow along with this you should have a knowledge of AS3 Classes and Interfaces. It is certainly possible to implement A* without knowledge of these concepts but in order to build a flexible, reusable solution, we will be using both. Check out our Understanding Classes in AS3 tutorials if you need help getting up to speed.

What is A* path finding?

According to the ever-so-useful Wikipedia, A* path finding can be described as:

(A) computer algorithm that (finds an) efficiently traversable path between points, called nodes…. A* uses a best-first search and finds the least-cost path from a given initial node to one goal node (out of one or more possible goals).

Sounds nice, but what does this mean? It means that A* chooses a node (or tile in a tile based world) searches through all the directly connected nodes and calculates a cost to travel to all of them. It then selects the node that has the lowest cost and uses it as the starting node for the next phase of the search. A* continues to do this until it reaches the destination.

To implement the algorithm it is first important to understand a few concepts:

Node: This is a point along a path. In a tile-based environment you can think of it as a tile. The reason why we don’t call it a tile is because it can be used for non-tile based worlds. You will provide A* with a starting node and ending node. A* will then calculate all the nodes to get from the start to the end.

This is a point along a path. In a tile-based environment you can think of it as a tile. The reason why we don’t call it a tile is because it can be used for non-tile based worlds. You will provide A* with a starting node and ending node. A* will then calculate all the nodes to get from the start to the end. Parent Node: When a node is tested in the algorithm all of its directly connected nodes (nodes that are traversable in a single step) are assigned that node as their parent. By doing so, we can easily work backwards through all of the parent nodes to create our path after we reach the destination node.

When a node is tested in the algorithm all of its directly connected nodes (nodes that are traversable in a single step) are assigned that node as their parent. By doing so, we can easily work backwards through all of the parent nodes to create our path after we reach the destination node. Cost: Cost is assigned to each node based on two factors. Nodes with a lower cost are used over nodes with a higher cost. The two parts that make up the cost are: the cost to get to that node from the starting node and the estimated cost to get from that node to the end node. The cost of a node is usually expressed via 3 variables f, g and h.

Cost is assigned to each node based on two factors. Nodes with a lower cost are used over nodes with a higher cost. The two parts that make up the cost are: the cost to get to that node from the starting node and the estimated cost to get from that node to the end node. The cost of a node is usually expressed via 3 variables f, g and h. g: This is the cost to get to this node via the starting node. We know this value exactly because we can follow the parent nodes all the way to the start and add up their costs.

This is the cost to get to this node via the starting node. We know this value exactly because we can follow the parent nodes all the way to the start and add up their costs. h: This is the cost to get from this node to the final node. This value is estimated. It is not exact because we don’t know the path that we will take to get to the final node. Therefore we must estimate the cost and we do so using a function called a heuristic.

This is the cost to get from this node to the final node. This value is estimated. It is not exact because we don’t know the path that we will take to get to the final node. Therefore we must estimate the cost and we do so using a function called a heuristic. f: This is the total cost of the specific node – it is calculated by adding g + h.

This is the total cost of the specific node – it is calculated by adding g + h. Heuristic: This is the function that estimates the cost of getting from a particular node to the end node. The beauty of A* is that it supports a variety of heuristics, therefore you can try out different ones until you get the result you are looking for. Heuristic functions can differ based on their speed, efficiency and a variety of other criteria.

This is the function that estimates the cost of getting from a particular node to the end node. The beauty of A* is that it supports a variety of heuristics, therefore you can try out different ones until you get the result you are looking for. Heuristic functions can differ based on their speed, efficiency and a variety of other criteria. Open list: This is a list of all the nodes that have been visited (in an iteration of the search) and have been assigned a cost. The node that has the lowest cost will be used to perform the next iteration of the search.

This is a list of all the nodes that have been visited (in an iteration of the search) and have been assigned a cost. The node that has the lowest cost will be used to perform the next iteration of the search. Closed list: This is the list of nodes whose neighbors have been visited.

The Algorithm

Now that is out of the way we can move on to the actual algorithm. Before we actually begin coding, let’s just take a look at how A* actually works.

Provide a starting node and an destination node Add the starting node to our open list (which should have been empty) Start our search loop: a. Pick the node currently in the open list with the lowest cost – we will call this node the current node. When we first run the loop this will obviously be our starting node.

b. If the current node is the same as the destination then we are done and we can move on to step 5.

c. Check every directly connected node. In a tile based world this would be up to 8 tiles. For our implementation we will provide a function that determines all the connected nodes. For each node that is connected: i. If the node is not traversable (you can’t move to it because it Is occupied or for some other reason) or the node is already in the open list or in the closed list we can skip and move on to the next node in the list of connected nodes. Otherwise continue to ii. ii. Calculate the cost of that node. iii. Set the current node as the parentNode. iv. Add the node to the open list.

d. Add the current node to the closed list. Now return to step 3 (loop again) with the newly updated open list. (The loop should keep running until it hits the destination node) We have found the destination node (hurrah!) Now we create a list of nodes that will be our path list and we will add the destination node to that list. Add the parent node of the destination node to the path list. Add the parent of the previous node to the path list, we will continue to do this until we have added our starting node.

We should now have a list of all the nodes that make up the best path from our starting node to our destination node.

The Implementation

Now that our theory is out of the way, let’s start writing some code. First we will start with our Node class. For this I will actually create an interface for your node:

public interface INode { function get f ( ) : Number ; function get g ( ) : Number ; function get h ( ) : Number ; function get x ( ) : Number ; function get y ( ) : Number ; function get parentNode ( ) :INode; function get traversable ( ) : Boolean ; function set f ( value: Number ) : void ; function set g ( value: Number ) : void ; function set h ( value: Number ) : void ; function set parentNode ( value:INode ) : void ; function set traversable ( value: Boolean ) : void ; } public interface INode { function get f():Number; function get g():Number; function get h():Number; function get x():Number; function get y():Number; function get parentNode():INode; function get traversable():Boolean; function set f(value:Number):void; function set g(value:Number):void; function set h(value:Number):void; function set parentNode(value:INode):void; function set traversable(value:Boolean):void; }

This is a pretty straight-forward interface. It includes the variables for all the properties we discussed above (f,g,h and parentNode ). The other variables are fairly self-explanatory. X and Y obviously indicate the position of the node. We will use these values in our heuristic to estimate the cost of traveling from the node to the destination. The traversable variable is a simple Boolean that indicates if the node can be used in our path finding. If it is not then the algorithm will simply skip it when it is encountered. This is useful in a world where a node might be occupied by a prop (say a rock, tree or wall) or if there is another player/character occupying that space and you don’t want to support multiple characters on a single node. Implementing this in a class should be very easy.

Now we will build a class called Pathfinder. We will create the class only using static methods. Hopefully this will make it very flexible. It will not take much to turn this into a standard class but for our purposes static methods will do very well.

public class Pathfinder { public static var heuristic: Function = Pathfinder. diagonalHeuristic ; public class Pathfinder { public static var heuristic:Function = Pathfinder.diagonalHeuristic;

Here, we define the heuristic function we will use to estimate the cost of traveling from a node to the end. In this example we will use the diagonal heuristic. At the end of this article, we will discuss some other heuristics and how to implement them.

Let’s move on to our actual path finding function:

public static function findPath ( firstNode:INode, destinationNode:INode, connectedNodeFunction: Function ) : Array { public static function findPath(firstNode:INode, destinationNode:INode, connectedNodeFunction:Function ):Array {

Let’s examine the parameters:

firstNode: INode – this is the first node in our path

destinationNode: INode – The is the node that we are going to be traveling to

connectedNodeFunction:Function – This is a function that will return an array of nodes that are connected to a given node.

Since each implementation might be different we abstract it out this way. After we write our class I will use a tile- based example to illustrate how this works, but your method might be different.

Lastly, the function returns an Array. This will contain all the nodes along the path.

Variable Setup

So now that is out of the way let’s do some initial work:

var openNodes: Array = [ ] ; var closedNodes: Array = [ ] ; var currentNode:INode = firstNode; var testNode:INode; var connectedNodes: Array ; var travelCost: Number = 1.0 ; var g: Number ; var h: Number ; var f: Number ; currentNode. g = 0 ; currentNode. h = Pathfinder. heuristic ( currentNode, destinationNode, travelCost ) ; currentNode. f = currentNode. g + currentNode. h ; var l: int = nodes. length ; var i: int ; var openNodes:Array = []; var closedNodes:Array = []; var currentNode:INode = firstNode; var testNode:INode; var connectedNodes:Array; var travelCost:Number = 1.0; var g:Number; var h:Number; var f:Number; currentNode.g = 0; currentNode.h = Pathfinder.heuristic(currentNode, destinationNode, travelCost); currentNode.f = currentNode.g + currentNode.h; var l:int = nodes.length; var i:int;

The first two arrays will hold our open nodes and our closed nodes. Then we have a variable currentNode which will keep track of the node we are currently on. We set this to be our firstNode since that is where we will start.

Next we keep a variable testNode:INode, which will be the node that will be a connected node of our current node.

Next we have g,h and f values we will keep track of for each node.

We then set the initial g value of the currentNode (which is also our first node) to 0 since it does not cost anything to get there since it is already our current location.

Then, we calculate the estimated cost to reach the end using our heuristic, and finally the final cost which is calculated by adding g + h (the starting cost plus the final cost).

Lastly, we have some variables that we will use inside of loops in our A* algorithm.

The Loop

Let’s move on to the actual loop.

while ( currentNode ! = destinationNode ) { connectedNodes = connectedNodeFunction ( currentNode ) ; l = connectedNodes. length ; for ( i = 0 ; i < l; ++i ) { testNode = connectedNodes [ i ] ; if ( testNode == currentNode || testNode. travesable == false ) continue ; g = currentNode. g + travelCost; h = heuristic ( testNode, destinationNode, travelCost ) ; f = g + h; if ( Pathfinder. isOpen ( testNode, openNodes ) || Pathfinder. isClosed ( testNode, closedNodes ) ) { if ( testNode. f > f ) { testNode. f = f; testNode. g = g; testNode. h = h; testNode. parentNode = currentNode; } } else { testNode. f = f; testNode. g = g; testNode. h = h; testNode. parentNode = currentNode; openNodes. push ( testNode ) ; } } closedNodes. push ( currentNode ) ; if ( openNodes. length == 0 ) { return null ; } openNodes. sortOn ( 'f' , Array . NUMERIC ) ; currentNode = openNodes. shift ( ) as INode; } while (currentNode != destinationNode) { connectedNodes = connectedNodeFunction( currentNode ); l = connectedNodes.length; for (i = 0; i < l; ++i) { testNode = connectedNodes[i]; if (testNode == currentNode || testNode.travesable == false) continue; g = currentNode.g + travelCost; h = heuristic( testNode, destinationNode, travelCost); f = g + h; if ( Pathfinder.isOpen(testNode, openNodes) || Pathfinder.isClosed( testNode, closedNodes) ) { if(testNode.f > f) { testNode.f = f; testNode.g = g; testNode.h = h; testNode.parentNode = currentNode; } }else { testNode.f = f; testNode.g = g; testNode.h = h; testNode.parentNode = currentNode; openNodes.push(testNode); } } closedNodes.push( currentNode ); if (openNodes.length == 0) { return null; } openNodes.sortOn('f', Array.NUMERIC); currentNode = openNodes.shift() as INode; }

Whoa! There’s a lot going on there, so let’s try to break it down piece by piece.

while (currentNode != destinationNode) { while (currentNode != destinationNode) {

Start our loop. If our current node is the same as our destination then we are done and we can build our path!

connectedNodes = connectedNodeFunction ( currentNode ) ; l = connectedNodes. length ; for ( i = 0 ; i < l; ++i ) { connectedNodes = connectedNodeFunction( currentNode ); l = connectedNodes.length; for (i = 0; i < l; ++i) {

So fetch an array of all of the connected nodes of our current node. Store the length and then loop through all of them.

testNode = connectedNodes [ i ] ; if ( testNode == currentNode || testNode. travesable == false ) continue ; testNode = connectedNodes[i]; if (testNode == currentNode || testNode.travesable == false) continue;

Test each node out of the connected nodes. If the node is the same as our current node or it is not traversable, move on to the next connected node.

Note: Your connectedNodeFunction might omit non traversable nodes which would make the above line redundant and unnecessary.

g = currentNode. g + travelCost; h = heuristic ( testNode, destinationNode, travelCost ) ; f = g + h; g = currentNode.g + travelCost; h = heuristic( testNode, destinationNode, travelCost); f = g + h;

Ok, so now we are calculating the actual cost of our node. First, g:

g

In a tile based world, we simply add the cost of the currentNode to the travelCost to determine the cost of the tile. In some tile based environments, a diagonal move sometimes cost more than other moves. In this case you would need to determine if the testNode is diagonal to the currentNode and adjust the travelCost value accordingly.

In our case here at Untold, we had a situation where the cost of each connected node was not the same because the nodes could be spread out across variable distances. So in our case we changed the g calculation to use our heuristic between the currentNode and the testNode.

g = currentNode. g + Pathfinder. heuristic ( currentNode, testNode, travelCost ) ; g = currentNode.g + Pathfinder.heuristic( currentNode, testNode, travelCost);

Now, h:

h

This is where we use our heuristic. Our heuristic takes in our testNode and our destination node. We also supply the cost to the heuristic which will return a number. Again, we will go over the heuristic later.

f

Finally, we are at f. This is a straightforward calculation to determine the total cost of the node.

if ( Pathfinder. isOpen ( testNode, openNodes ) || Pathfinder. isClosed ( testNode, closedNodes ) ) { if ( Pathfinder.isOpen(testNode, openNodes) || Pathfinder.isClosed( testNode, closedNodes) ) {

Here, we use some simple utility functions to determine if the node is in either our open list or our closed list. If it is, then we should only adjust its f,g and h value if its current cost (f) is greater than the cost we just calculated.

if ( testNode. f > f ) { testNode. f = f; testNode. g = g; testNode. h = h; testNode. parentNode = currentNode; } else { testNode. f = f; testNode. g = g; testNode. h = h; testNode. parentNode = currentNode; openNodes. push ( testNode ) ; } if(testNode.f > f){ testNode.f = f; testNode.g = g; testNode.h = h; testNode.parentNode = currentNode; }else { testNode.f = f; testNode.g = g; testNode.h = h; testNode.parentNode = currentNode; openNodes.push(testNode); }

If the node is not in either the open list or in the closed list, then we definitely want to assign it the values we just calculated. We also want to add it to the list of open nodes and make sure its parent is set to our currentNode.

We are now done our loop and can add our currentNode to our list of closed nodes.

closedNodes. push ( currentNode ) ; if ( openNodes. length == 0 ) { return null ; } openNodes. sortOn ( 'f' , Array . NUMERIC ) ; currentNode = openNodes. shift ( ) as INode; closedNodes.push( currentNode ); if (openNodes.length == 0) { return null; } openNodes.sortOn('f', Array.NUMERIC); currentNode = openNodes.shift() as INode;

If there are no more open nodes, then no path is available between the provided nodes, so we return NULL.

Otherwise, sort our open nodes based on their cost and use the one with the lowest cost as our new currentNode.

If a path is available, the currentNode will eventually be the same as our destination node, and the while loop will exit. At this point, we will be ready to build our path and end our function which we do with a single line:

return Pathfinder. buildPath ( destinationNode, firstNode ) ; return Pathfinder.buildPath(destinationNode, firstNode);

Of course this means we need a function called buildPath(). Luckily, it is quite simple and looks like this:

public static function buildPath ( destinationNode:INode, startNode:INode ) : Array { var path: Array = [ ] ; var node:INode = destinationNode; path. push ( node ) ; while ( node ! = startNode ) { node = node. parentNode ; path. unshift ( node ) ; } return path; } public static function buildPath(destinationNode:INode, startNode:INode):Array { var path:Array = []; var node:INode = destinationNode; path.push(node); while (node != startNode) { node = node.parentNode; path.unshift( node ); } return path; }

In this function, we simply keep moving through all the parent nodes of the provided nodes and put them at the front of a new array we return to the caller.

The Heuristic

We are almost ready to test out our class with an example but as promised we will first discuss our heuristic.

There is a wide range of heuristics used to calculate an optimum path. The methodology is based on a variety of criteria. Different heuristics return different results and you will need to find the one that is best for your case. Right now, we’ll examine three simple heuristics, but don’t be afraid to go out and find your own.

Now, I was familiar with the basics behind A* but had never actually implemented it. Thankfully though I had a copy of Keith Peter’s Advanced ActionScript 3.0 Animation which has a very nice chapter all about path finding and A*.

Manhattan: This heuristic is the most simple. It ignores any diagonal movement, so if your world does not allow for diagonal movement it might be a good fit. It simply adds the total number of rows and columns between the two provided nodes.

public static function manhattan ( node:INode, destinationNode:INode, cost: Number = 1.0 ) : Number { return Math . abs ( node. x – destinationNode. x ) * cost + Math . abs ( node. y + destinationNode. y ) * cost; } public static function manhattan(node:INode, destinationNode:INode, cost:Number = 1.0):Number { return Math.abs(node.x – destinationNode.x) * cost + Math.abs(node.y + destinationNode.y) * cost; }

Euclidian: This heuristic simply draws a straight line between the two provided nodes and returns its length. You might recall the Pythagorean Theorem from math class? Well, here it is in action.

public static function euclidianHeuristic ( node:INode, destinationNode:INode, cost: Number = 1.0 ) : Number { var dx: Number = node. x - destinationNode. x ; var dy: Number = node. y - destinationNode. y ; return Math . sqrt ( dx * dx + dy * dy ) * cost; } public static function euclidianHeuristic(node:INode, destinationNode:INode, cost:Number = 1.0):Number { var dx:Number = node.x - destinationNode.x; var dy:Number = node.y - destinationNode.y; return Math.sqrt( dx * dx + dy * dy ) * cost; }

Diagonal: This heuristic is the most accurate of the three. It is more complex than the previous two, but runs fewer times because of its accuracy. It takes into account a diagonal cost, if it is more expensive than a regular movement.

public static function diagonalHeuristic ( node:INode, destinationNode:INode, cost: Number = 1.0 , diagonalCost: Number = 1.0 ) : Number { var dx: Number = Math . abs ( node. x - destinationNode. x ) ; var dy: Number = Math . abs ( node. y - destinationNode. y ) ; var diag: Number = Math . min ( dx, dy ) ; var straight: Number = dx + dy; return diagonalCost * diag + cost * ( straight - 2 * diag ) ; } public static function diagonalHeuristic(node:INode, destinationNode:INode, cost:Number = 1.0, diagonalCost:Number = 1.0):Number { var dx:Number = Math.abs(node.x - destinationNode.x); var dy:Number = Math.abs(node.y - destinationNode.y); var diag:Number = Math.min( dx, dy ); var straight:Number = dx + dy; return diagonalCost * diag + cost * (straight - 2 * diag); }

Make it Go

Now that we are done with our pathfinding system, we can put it to work. Take a look at the example below. You can click on any two tiles in the grid and then we will then draw a path between the two tiles. You can change the heuristic at any point to see the different results you might get. Also highlighted are any tiles that the pathfinder tests. Notice that the Manhattan heuristic tests far more nodes than the other two.

[SWF]http://www.untoldentertainment.com/blog/img/2010_08_13/pathfinding_example.swf, 640, 480[/SWF]

I implement the pathfinding simply by calling the Pathfinder.findPath() function and passing the start node, end node and the function that determines connected nodes. I then pass the returned array to a function called drawPath. For your usage you probably want to do a lot more than draw a path, you probably want to have a character traverse along that path, but this is just meant to show you how to acquire which path to follow.

drawPath ( Pathfinder. findPath ( _startNode, _endNode, findConnectedNodes ) ) ; drawPath( Pathfinder.findPath(_startNode, _endNode, findConnectedNodes) );

The findConnectedNodes method is very straightforward. It simply returns all nodes that are adjacent to a given node. You can check it out here:

public function findConnectedNodes ( node:INode ) : Array { var n:Node = node as Node; var connectedNodes: Array = [ ] ; var testNode:Node; for ( var i: int = 0 ; i < _nodes. length ; i++ ) { testNode = _nodes [ i ] ; if ( testNode. row < n. row - 1 || testNode. row > n. row + 1 ) continue ; if ( testNode. col < n. col - 1 || testNode. col > n. col + 1 ) continue ; connectedNodes. push ( testNode ) ; } return connectedNodes; } public function findConnectedNodes( node:INode ):Array { var n:Node = node as Node; var connectedNodes:Array = []; var testNode:Node; for (var i:int = 0; i < _nodes.length; i++) { testNode = _nodes[i]; if (testNode.row < n.row - 1 || testNode.row > n.row + 1) continue; if (testNode.col < n.col - 1 || testNode.col > n.col + 1) continue; connectedNodes.push( testNode ); } return connectedNodes; }

Of course, this implementation will not be sufficient if you want different tiles to have different costs based on terrain (quicksand might be more expensive than grass), but if you can grasp the above you should be able to make the adjustments fairly easily.

Hopefully you will now have a solid understanding of A* path finding and you can now use it in your own applications.

Click here to download the source code for this tutorial.