The distances that we compute match the lengths of the shortest paths from the selected cell to every other cell. You cannot find a path with a shorter length. But these paths are only guaranteed to be valid if there's nothing that blocks travel. Cliffs, water, and other obstacles might force us to make a detour. Some cells might not be reachable at all.

To be able to find our way around obstacles, we have to use a different approach than simply computing the distance between coordinates. We can no longer evaluate each cell in isolation. Instead, we have to search our map until we have found every cell that can be reached.

We should ensure that only a single search is active at any time. So stop all coroutines before starting a new search.

Searching through the map is an iterative process. To understand what we're doing, it's useful to be able to see each step of the search. We can do so by turning our search algorithm into a coroutine, which requires us to use the System.Collections namespace. An update frequency of 60 iterations per second is slow enough that we can see what's happening, without taking too long on a small map.

Breadth-First Search

Before we even begin searching, we know that the distance to the selected cell is zero. And of course the distance of all its neighbors is 1, as long as they can be reached. We can then look at one of these neighbors. This cell likely has neighbors of its own that are reachable and don't have a distance yet. If so, the distance of these neighbors must be 2. We can repeat this for all neighbors at distance 1. After that, we repeat it for all neighbors at distance 2. And so on, until we have reached all cells.

So we first find all the cells at distance 1, then we find all those at distance 2, then those at distance 3, until finished. This guarantees that we find the smallest distance to every reachable cell. This algorithm is known as breadth-first search.

For this to work, we have to know whether we've already determined the distance of a cell. Often, this is done by putting them in a collection known as the finished or closed set. But we can set the cell's distance to int.MaxValue to indicate that we haven't visited it yet. We have to do this for all cells, right before searching.

IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } … }

We can also use this to hide the labels of unvisited cells, by adjusting HexCell.UpdateDistanceLabel . That way, we start each search with a clean map.

void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance == int.MaxValue ? "" : distance.ToString(); }

Next, we have to keep track of which cells we have to visit, and in what order. This collection is often known as the frontier or the open set. We simply have to process cells in the same order that we encountered them. We can use a Queue for that, which is part of the System.Collections.Generic namespace. The selected cell is the first to be put in this queue, at distance zero.

IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); Queue<HexCell> frontier = new Queue<HexCell>(); cell.Distance = 0; frontier.Enqueue(cell); // for (int i = 0; i < cells.Length; i++) { // yield return delay; // cells[i].Distance = // cell.coordinates.DistanceTo(cells[i].coordinates); // } }

From this point, the algorithm loops as long as there is something in the queue. Each iteration, the front-most cell is taken out of the queue.

frontier.Enqueue(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); }

We now have a current cell, which could be at any distance. Next, we add all its neighbors to the queue, one step further away from the selected cell.

while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor != null) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } } }

But we should only add cells that we haven't given a distance yet.

if (neighbor != null && neighbor.Distance == int.MaxValue ) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }