In a previous article I mentioned how I had some issues with the way I decided to render isometric tiles, and implementing pathfinding. So lets talk about how it is I overcame those problems and why implementing A* was way more difficult then it should have been. First, so we are all on the same page, lets go over the A* algorithm itself.

The A* Algorithm

What A* is meant to do is find the shortest and most efficient path to get from point A to point B. Its important to make the distinction that not only is it the shortest as in the most direct path, but also the most efficient. The reason that this algorithm is so widespread is that it can adjust for both distance and efficiency.

This algorithm creates the best path by testing every node that is accessible to the object it is moving. In our case, a node can be a tile on a tile map. It tests every object for two values. First, its g cost which is usually the physical (or digital) distance that the mover would have to go to reach that node or tile. The second value it tests, is the node’s heuristic a value that estimates how cheap it is to go to that node. As an example, say you are racing in a dirt bike trying to escape from a bipedal amphibious radioactive monster. You come across a turn in a road that goes around a pit of quicksand. The shortest distance from point A to point B, that is, your current location to a nuclear bunker stocked with bipedal monster repellent, would be to cut across the muddy path. So your g cost tells you to keep going straight. However, the heuristic of the dirt road is way less than the heuristic of quicksand so the best path is to stay just on the edge of quicksand.

That’s what makes A* so convenient, it can be applied to myriad of situations and is (usually) not to hard to implement. Lets look at how we would go about implementing in java.

Implementation

A* works by keeping two sets or lists of nodes to check and compare their heuristic and g costs against each other. The first set is called the Open Set which contains all the nodes to be checked. Nodes are added to the open set based on if they are adjacent to either the starting position, or adjacent to a node in the closed set. Nodes are added to the closed set when their g cost and heuristic are calculated. The algorithm decides which nodes from the open set to check by defaulting to the one with the lowest combined g cost and heuristic. You can see how easy this would be to achieve in Java by using a SortedList for the open set and an ArrayList for the closed set. The open list is sorted by the combined g cost and heuristic of every node, the f value. However in order to populate the lists with nodes we need to define what exactly a node is.

private class Node implements Comparable<Node> { private int x; private int y; private int cost; private float f; private float heuristic; private Node parent; private int depth; public Node(int x, int y) { this.x = x; this.y = y; f = cost + heuristic; } public int setParent(Node parent) { depth = parent.depth + 1; this.parent = parent; return depth; } public int compareTo(Node other) { if (f < other.f) { return -1; } else if (f > other.f) { return 1; } else { return 0; } } }

So this is the creation of our node class, basically it is defined by its x and y values, which are taken from our level data array. Its other attributes are its ability to have a parent node, basically it saves the node that it is connected to so we can later recreate the pathway once the algorithm is done. Also it has a depth value, telling us how many levels we are within the algorithm and also the number of nodes that make up the path. Finally it can be compared to other nodes based on what its f value is, a prerequisite for being included in a SortedList.

Before going into how g cost and heuristics are calculated lets look at a visual representation of the algorithm using what we already know.

Here we can see the open circles represent nodes in the open set while colored nodes are in the closed set. The more green the color is the closer to the goal the nodes are. Right away you can see how the algorithm investigates all the nodes in a straight line towards the goal until it hits a barrier, because those nodes all have much lower f values than the nodes way back by the starting position. Once it hits the barrier then it doubles back and investigates those nodes around the start.

Understanding nodes and the open and closed sets is all well and good, but how do we go about calculating the g cost and heuristic? Here is where it gets a little tricky. Lets start with the g cost.

// Get movement cost of node public int getMovementCost(GameObject mover, int sx, int sy, int tx, int ty) { CoordinatePair start = map.tileToCoords(sy, sx); CoordinatePair target = map.tileToCoords(ty, tx); int dx = Math.abs(target.getX() - start.getX()); int dy = Math.abs(target.getY() - start.getY()); if (dx != 0 && dy != 0) { return 108; } else { return 208; } }

This function accepts the type of object that is being moved (in case you want a person and a car to behave differently), the starting x and y coordinates, and the target x and y coordinates. I convert the values from level data coordinates to actual x and y coordinates using my function I detailed in a previous article and then get the distance between the two point on the x and y axis. I then check if the mover is moving on more than one axis i.e. diagonally, and give it a lower movement cost than if the mover were to go directly up, down, left or right.

This is to avoid people from walking in crazy zigzag patterns that don’t make any sense, which did happen a lot prior to fine tuning the movement cost of the two different types of moves.

We then calculate the heuristic by finding calculating the Euclidean distance between the node we are investigating and the goal.

// Get Heuristic of node public int getHeuristicCost(GameObject mover, int x, int y, int tx, int ty) { CoordinatePair start = map.tileToCoords(y, x); CoordinatePair target = map.tileToCoords(ty, tx); int dx = Math.abs(target.getX() - start.getX()); int dy = Math.abs(target.getY() - start.getY()); return (int) Math.sqrt((dx*dx)+(dy*dy)); }

An alternative to this is calculating Manhattan distance, essentially just adding the difference between the x and y positions of the two nodes without square rooting or squaring them. This would obviously be much faster, but does not work with the “zigzag” style system we are using to store our level data. This leads well into the next topic, the problems introduced by using zig zag style isometric rendering, which are not introduced when using “normal” style isometric data. More info can be found in the aforementioned previous article.

Problems with Isometric Data

As mentioned in the previous article, I am storing all my level data as an offset grid to better approximate how the level data will actually look in the game. An array of tiles might look like this:

While Manhattan distance works just fine with a standard grid, this offset data will not work at all with that distance calculation. This is because the tiles directly above the starting tile are actually either to the left or right of the starting tile, and not just above it. This is also why we needed to convert the coordinates of the nodes to actual pixel coordinates, because the cell coordinates of where the node is located in the array are inaccurate to where it is located on the screen. Essentially the need to convert the data slows down the algorithm with extra calculations.

Now to the issue that gave me the most trouble with implementing the A* algorithm into the zigzag data: selecting adjacent nodes to enter the open set. Lets start by looking at what we want to do, select the 8 nodes around the starting position.

The green color denotes our starting position, while the red show the tiles with the smallest movement cost and the blue are the larger movement cost. You can immediately see a problem when looking at the coordinates shown on the image. If the algorithm simply added and subtracted 1 to the x and y location of the node, it would not get the right nodes at all. Instead the algorithm needs to take into account 5 different rows of data, with a different number of columns for each row. My solution to this is certainly not the cleanest but I couldn’t think of some kind of miracle equation that would give me all the correct adjacent tiles mathematically, so I had to iterate through each row individually. Here’s what the code looks like:

// Look at each tile neighboring our current one in a diamond pattern for (int y = -2; y < 3; y++) { // Check if y is odd to offset accordingly on the diagonal neighbors int offsetX; if ((current.y & 1) != 0) { // cellY is odd and y is odd offsetX = 1; } else { // cellY is even offsetX = 0; } // Make sure the diamond is wide enough in each row int numXNodes = 3 - Math.abs(y) + offsetX; // Add enough new x nodes per row to make a diamond for (int countX = 0; countX < numXNodes; countX++) { // Define each x starting place, sure wish I had some kind of formula for this. int x; if (y == 0) { x = -1 + countX; } else if (Math.abs(y) == 1) { x = -1 + countX + offsetX; } else { x = 0; } // if this is the node we are on then skip it if ((x == 0) && (y == 0)) { continue; } int xp = x + current.x; int yp = y + current.y;

So that’s how I added eligible tiles to the open set. After I got that out of the way it was pretty much standard implementation from there, you would check all the surrounding nodes for the one with the lowest heuristic and g cost then move onto the next one. That code looks like this:

if (isValidLocation(mover, sx, sy, xp, yp)) { int nextStepCost = current.cost + getMovementCost(mover, current.x, current.y, xp, yp); Node neighbour = nodes[xp][yp]; if (nextStepCost < neighbour.cost) { if (open.contains(neighbour)) { open.remove(neighbour); } if (closed.contains(neighbour)) { closed.remove(neighbour); } } if (!open.contains(neighbour) && !(closed.contains(neighbour))) { neighbour.cost = nextStepCost; neighbour.heuristic = getHeuristicCost(mover, xp, yp, tx, ty); maxDepth = Math.max(maxDepth, neighbour.setParent(current)); open.add(neighbour); }

This code essentially removes a node from the open list and into the closed list if it has a lower f value than its neighbors and then uses that node as the next starting point. The maxDepth is the predetermined limit to how long the algorithm will search before it gives up.

All of the previous code is executed in a loop until either maxDepth is reached or the goal is found by the algorithm. The while loop saves the closed set and exits where a Path is constructed. This part is made much easier by having the nodes save their parents!

// If finding a path was unsuccessful return null if (nodes[tx][ty].parent == null) { return null; } // We have now definitely found one, build the path starting from the target and work our way backwards using the parent identities Path path = new Path(); Node target = nodes[tx][ty]; while (target != nodes[sx][sy]) { CoordinatePair targetCoords = map.tileToCoords(target.y, target.x); path.prependStep(targetCoords.getX(), targetCoords.getY()); target = target.parent; } return path;

Finally we return Path and the algorithm is complete. The path is essentially a collection of the x and y values for the mover to follow, in the next update I’ll go into how this path is followed by a walking person and how it was adapted to also serve as the code for waiting in line too. I hope you enjoyed the article, I’ll see you in the next update!