In this tutorial I will explain vector field pathfinding and its advantages over more traditional pathfinding algorithms, such as Dijkstra’s. A basic understanding of both Dijkstra’s algorithm and potential fields will help you to understand this article, but is not required.
Introduction
Pathfinding is a problem with many solutions, and each has its pros and cons. Many pathfinding algorithms work by calculating a path to the goal for every pathfinder, which means that the pathfinding will take twice as long to calculate with twice as many pathfinders. This is acceptable in many situations, but when working with thousands of pathfinders a more efficient approach is possible.
Known as vector field pathfinding, this approach calculates the path from the goal to every node in the graph. To solidify this explanation of vector field pathfinding, I will explain the algorithm using my particular implementation as an example.
Note: Vector field pathfinding can be abstracted to nodes and graphs in general; just because I explain it using my tile and grid based approach doesn’t mean this algorithm is limited to tile based worlds!
Video Overview
Vector field pathfinding is composed of three steps.
- First, a heatmap is generated that determines the path-distance between the goal and every tile/node on the map.
- Second, a vector field that designates the direction to go in to reach the goal is generated.
- Third, every particle that is seeking the shared goal uses the vector field to navigate towards the goal.
This video shows you the final results, and then give you a general overview of the concepts presented in the full tutorial below:
Heatmap Generation
The heatmap stores the path distance from the goal to every tile on the map. Path distance is distinct from Euclidean distance in that it is a calculation of distance between two points only passing through traversable terrain. A GPS, for example, always calculates path distance, with roads being the only traversable terrain.
Below, you can see the difference between path distance and linear distance from the goal (marked in red) to an arbitrary tile (marked in pink). Non traversable tiles are drawn in green. As you can see, the path distance (shown in yellow) is 9, while the linear distance (shown in light blue) is approximately 4.12.
The numbers in the top left of each tile show the path distance to the goal calculated by the heatmap generation algorithm. Note that there is more than one possible path-distance between two points; in this article, we are only interested in the shortest one.
The heatmap generation algorithm is a wavefront algorithm. It starts at the goal with a value of 0, and then flows outwards to fill the entire traversable region. There are two steps to the wavefront algorithm:
- First, the algorithm begins at the goal, and marks it with a path distance of
0
. - Then, it gets each marked tile’s unmarked neighbors, and marks them with
the previous tile's path distance + 1
. - This continues until the entire reachable map has been marked.
Note: The wavefront algorithm is simply running a breadth first search on the grid and storing how many steps it took to get to each tile along the way. This algorithm is sometimes also called the brushfire algorithm.
Vector Field Generation
Now that the path distance from every tile to the goal has been calculated, we can easily determine the path that needs to be taken to get closer to the goal. It is possible to do this at runtime for every pathfinder every frame, but it is often better to calculate a vector field once and then have all of the pathfinders refer to the vector field at runtime.
The vector field simply stores a vector that points down the gradient of the distance function (towards the goal) at every tile. Here is a visualization of the vector field, with the vectors pointing from the center of the tile along the shortest path to the goal (again shown in red).
This vector field is generated one tile at a time by looking at the heatmap. The x and y components of the vector are calculated separately, as shown in the pseudocode below:
Vector.x = left_tile.distance - right_tile.distance Vector.y = up_tile.distance - down_tile.distance
Note: Each tile’s distance variable stores the path distance to the goal as calculated by the wavefront algorithm above.
If any of the tiles referenced (left/right/up/down) are non-traversable and thus have no usable distance stored, the distance associated with the current tile is used in place of the missing value. Once the path vector has been roughly calculated, it is normalized to avoid inconsistencies later.
Pathfinder Movement
Now that the vector field has been calculated, it is very easy to calculate movement for a pathfinder. Assuming that vector_field(x,y) returns the vector we calculated earlier at the tile (x,y)
, and that desired_velocity is a scalar, the pseudocode to calculate the velocity of a particle at tile (x,y)
looks like this:
velocity_vector = vector_field(x, y) * desired_velocity
The particles simply need to start moving in the direction indicated by the vector. This is the simplest way of doing this, but more complicated movement systems can easily be implemented using flow fields.
For example, the techniques explained in Understanding Steering Behaviors could be applied to pathfinder movement. In such a situation, the velocity_vector
we calculated above would be used as the desired velocity, and the steering behaviors would be used to calculate the actual movement at every time step.
Local Optima
When calculating movement, there is one problem that can sometimes occur, known as local optima. This occurs when there are two optimal (shortest) paths to take to get to the goal from a given tile.
This issue can be seen in the image below. The tile (shown in pink) immediately to the left of the center of the wall has a path vector whose components (x and y) are equal to 0.
Local optima cause pathfinders to get stuck; they will refer to the vector field which will fail to indicate a direction to go in. When this happens, the pathfinders will remain in the same location unless a fix is implemented.
The most elegant way (I’ve found) to fix the problem is to subdivide both the heatmap and the vector field once. Every single heatmap and vector field tile has now been split into four smaller tiles. The problem remains the same with a subdivided grid; it has only been slightly minimized.
The real trick that solves the local optima problem is to initially add four goal nodes, instead of just one. To do this we simply have to modify the first step of the heatmap generation algorithm. When we used to only add one goal with a path distance of 0, we now add the four tiles that are closest to the goal.
There are several ways to choose the four tiles, but how they are chosen is largely irrelevant – as long as the four tiles are adjacent (and traversable), this technique should work.
Here is the altered pseudocode for the heatmap generation:
- First, the algorithm begins at the four goal tiles, and marks all four goal tiles with a path distance of
0
. - Then, it gets each marked tile’s unmarked neighbors, and marks them with
the previous tile's path distance + 1
. - This continues until the entire reachable map has been marked.
And now, here are the final results, clearly showing that the local optima problem has been eliminated:
Although this solution is elegant, it is far from ideal. Using it means that calculating the heatmap and vector field takes four times longer because of the increased number of tiles.
Other solutions require making checks and then determining what direction to go to on a case by case basis, which significantly slows down the particle movement calculations. In my case, subdividing the maps was the better option.
Conclusion
Hopefully this tutorial has taught you how to implement goal-based pathfinding in a tile based world. Keep in mind that this kind of pathfinding is, at its core, simple: particles follow the gradient of the distance function towards the goal.
The implementation is more complex, but can be broken down into the following three manageable steps:
- Heatmap Generation
- Vector Field Generation
- Particle Movement
I hope to see people expand upon the ideas presented here. As always, if you have questions, feel free to ask them in the comments below!