Weighted Jump Point A* (WJPA*) Pathfinding

JPA*is a variation of A* developed by Daniel Harabor and Alban Grastein (http://users.cecs.anu.edu.au/~dharabor/data/papers/harabor-grastien-aaai11.pdf). It is a A* optimisation for unweighted grids. You can read full explanations of it at http://zerowidth.com/2013/05/05/jump-point-search-explained.html and http://www.grastien.net/ban/articles/hg-socs12.pdf , but basically the way I explain it to myself is that JPA* eliminates unnecessary expansions by eliminating nodes that would provide suboptimal/equally optimal paths. Consult the following diagram. Note that for now I’m just introducing the idea of eliminating suboptimal paths. We aren’t at the implementation yet!

B can be reached optimally from A, because the paths through the grey nodes would be longer. The same goes for C and D. Now, JPA* builds on this principle.

In the diagram above, any path through the grey squares is longer than a path through the white squares. Feel free to check yourself a few times to convince yourself. Things get a bit trickier with walls. If the walls “close in”…

… it is still fine. You can’t have a shorter path if it goes through an obstacle! It is when the walls are suddenly further away e.g.

…that we have a problem. What happens now is that we have a corner, and we can’t assume that jumping further to the right still yields the shortest path. I consider JPA* a wall hugging search, because when we find the corner it becomes a new point to start a jump search. This means that JPA* tends to search along walls. The red square is known as a forced neighbor – we are forced to consider this node and add it to our open list.

Now, what precisely is JPA* doing? It “looks ahead” 45 degrees to the left and right of the expansion direction before each “jump”. Look at the following diagram to see what a JPA* search will do:

The arrows indicate the algorithm looking ahead from the current jump point. If you aren’t sure why the search has to stop when it does, see that the shortest path from B to the forced neighbor is through that dotted line. To ensure we get the optimal path we stop here knowing that we have the shortest path from A to B, the shortest path from B to the forced neighbor, and the shortest path from A to the forced neighbor (A -> B -> forced neighbor).

Now, uniform grids are a perfectly reasonable thing to have in a game, but wouldn’t it be great if we couldn’t somehow get some of this speedup on weighted grids? I sought to see if such an adaption of JPA* was possible.

WJPA*

Let’s go back to the whole idea of a uniform cost grid and how it allows the speedup. Since we assume the weights are the same, we can know without any calculations that the paths through neighbours are shorter. Now let’s look at a weighted grid:

Are the paths through the grey cells not still longer than the path through the white cells? They are. So we don’t need for the cells to have identical costs. They must just be as expensive- or more expensive – to move through than our nodes on the jump path (white). Now depending on your terrain, you will likely have areas or “blots” of terrain of a certain type and movement cost. WJPA* will be a waste on some map where the terrain varies from cell to cell – although I think you should not need highly efficient pathfinding for such maps because they should be relatively small (i.e. for high level strategy games).

The idea behind WJPA* is that within areas of a certain terrain cost we can do a jump point search. WJPA breaks off the jump search when it encounters the border of a terrain region which costs less than the one it is currently jumping through. Are the resulting paths optimal? Not necessarily, as you can see:

So we see that we don’t get the same path as A*. I think that to a certain extent it is due to the “jumping” of the jump point search. Because A*advances one node at a time, it has more opportunities to steer the path. WJPA* produces a more jagged path thanks to these jumps that leave it with less opportunities to steer. It’s up to you whether you want to trade speed for the absolute best path. So what kind of boost will JPA* give you? I’m going to run a series of tests, starting with the following test on an obstacle free weighted terrain.

Edit: There is one critical change to the JPA* algorithm I made that helps explain why WJPA* does not produce a identical path to A*. I forced the WJPA* algorithm to terminate it’s jumps after a set number of steps, to give it more changes to steer. However because it can’t steer on every step, the nodes that end up at the front of the openlist are different.

Generating the Map

I’ve looked at the maps at http://movingai.com/benchmarks/ and decided they weren’t rigorous enough for this particular test. I wanted to do everything I could to break WJPA*. I first generated a grid with uniform weights. Using a seed-based random number generator I coded, I created a bunch of randomly positioned circles with random radiuses. I clamped the circle positions to the map bounds and I clamped the radiuses to a maximum value. For each cell falling within a circle I incremented the weight by 1. Clamping the circle radiuses enforced a more varied terrain, to prevent the circles from possibly overlapping to produce a single “mountain”. I needed for WJPA* to have to handle both going “uphill” and “downhill”. I chose circular weight regions over square ones because real terrain patches would have some kind of rounded edge, and those rounded edges would force WJPA* to possibly shorten it’s jumps, due to many scenarios like this happening:

The bright red nodes are “forced neighbours” bordering the light green area with the lower terrain cost. Note that I assume that the green node does not provide a shorter path to the forced neighbour than the straight line B -> forced neighbour. This assumption is necessary to avoid the computational cost of determining the actual path costs.

All it takes is a single lower cost node to mess everything up. The way I think of jump point search is that in order to jump over a point you need to be able to expand an area of nodes that offer suboptimal/equally optimal paths. This area has nodes that have the same cost or a higher cost than the node you want to jump to. All it takes is a single lower cost node to prevent the expansion of this area. With square or rectangular weight painting regions, the dimensions of the regions are parallel or at 45 degrees to the expansion directions, so it’s harder for a single low cost node to mess up the search. With the overlapping circles, we have more irregular areas that can force WJPA* to stop jumping earlier.

With WJPA* (and normal JPA*) there is an implementation choice to be made when it comes to your looking ahead. You could either actually inspect the grid cells over and over, indexing them one by one, or you could pre-calculate for each node, for all 8 directions, for how many cells in that direction the weight is more or equal to the weight of the node. I decided the storage overhead of pre-calculating the values was not acceptable, and that bit-packing the distances wasn’t worthwhile since it’d introduce overhead due to unpacking the data. Also, I prefer a solution that can adapt to dynamic weight changes.

The Benchmark

For this first test, I will not have obstacles. My current benchmark is based on my previous benchmark https://codingctsa.wordpress.com/2016/12/28/some-a-benchmarks/ so I won’t repeat the entire setup. The key thing is WJPA* being added to the contender pool, and a new map:

The map has been rendered so that the colour of a cell depends on the weight of a cell, with 100% green as minimum weight and 100% red as maximum weight. To verify paths I would render each path once I’d completed measuring how long it took to find the path. All the A* versions had generated the same path, while WJPA* found a different path.

I ran a set of 435 path searches 5 times, with each search starting at the centre of a weight painting radius. For now I’m going to just present the results for a smaller 300 x 300 map because with larger maps I found that I either had to A) leave the test running for ages and sit around twiddling my thumbs or B) Limit the number of iterations, which lead to WJPA* sometimes finding a result when the others didn’t. Anyway, there will be more tests on larger maps down the line. On to the results!

Results

Average Run Time:

WJPA*: 6.72 x 10^-4 seconds A* (storing number of uses per cell): 7.58 x 10^-4 seconds A* (Bool of whether node visited, keep list of nodes visited): 7.75 x 10^-4 seconds A*(Bit mask to indicate if node visited): 8.10 x 10^-4 seconds A* (Bit mask, list of nodes visited): 8.89 x 10^-4 seconds A* (bool per cell, no list): 1.23 x 10^-3 seconds

Conclusion:

WJPA* has won round one of testing, running on average in 88% of the time A* would take. However the testing is not over. I’m going to keep trying to break WJPA*. It’s time to throw in walls, more varying terrain and bigger maps. If anything, this first test just explores if WJPA* has any potential.

Edit: This algorithm’s implementation forms part of the code of a Unity A* benchmarking project. I want to try and tweak the project to be able to test any A* implementation (using delegates) before releasing it.