This is part 17 of a tutorial series about hexagon maps. This time, we'll split movement into turns and search as quickly as possible.

Turn-based Movement

Strategy games that use hexagon grids are nearly always turn-based. Units that navigate the map have a limited speed, which constrains how far they can move in a single turn.

Speed To support limited movement, let's add a speed integer parameter to HexGrid.FindPath and HexGrid.Search . It defines the movement budget for one turn. public void FindPath (HexCell fromCell, HexCell toCell , int speed ) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell , speed )); } IEnumerator Search (HexCell fromCell, HexCell toCell , int speed ) { … } In a game, different unit types will often have different speeds. Cavalry is fast, infantry is slow, and so on. We don't have units yet, so we'll use a fixed speed for now. Let's use 24. This is a reasonably large value which isn't divisible by 5, which is our default movement cost. Add the constant speed as an argument for FindPath in HexMapEditor.HandleInput . if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell , 24 ); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell , 24 ); }

Turns Besides keeping track of the total movement cost of a path, we now also have to know how many turns it requires to travel along it. But we do not have to store this information per cell. We can derive it by dividing the traveled distance by the speed. As these are integers, we use an integer division. So total distances of at most 24 correspond to turn 0. This means that the entire path can be traveled in the current turn. If the destination were at distance 30, then its turn would be 1. The unit will have to use all of its movement of its current turn and part of its next turn before it would reach the destination. Let's figure out the turn of the current cell and that of its neighbors, inside HexGrid.Search . The current cell's turn can be computed once, just before looping through the neighbors. The neighbor's turn can be determined once we have found its distance. int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … }

Lost Movement If the neighbor's turn is larger than the current turn, then we have passed a turn boundary. If the movement required to enter the neighbor was 1, then all is fine. But when it's more costly to enter the neighbor, then it gets more complicated. Suppose that we're moving across a featureless map, so all cells require 5 movement to enter. Our speed is 24. After four steps, we have used 20 of our movement budget, with 4 remaining. The next step again requires 5 movement, which is one more than we have. What should we do at this point? There are two ways to deal with this situation. The first is to allow the fifth cell to be entered during the current turn, even if we don't have enough movement. The second is to disallow the movement during the current turn, which means that the leftover movement points cannot be used and are lost. Which option is best depends on the game. In general, the first approach works well for games with units that can only move a few steps per turn, like the Civilization games. This ensures that units can always move at least a single cell per turn. If the units can move many cells per turn, like in Age of Wonders or Battle for Wesnoth, then the second approach works better. As we use speed 24, let's go for the second option. To make this work, we have to isolate the cost to the enter the neighboring cell, before adding it to the current distance. // int distance = current.Distance; int moveCost ; if (current.HasRoadThroughEdge(d)) { moveCost = 1; } else if (current.Walled != neighbor.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int distance = current.Distance + moveCost; int turn = distance / speed; If we end up crossing a turn boundary, then we first use up all the movement of the current turn. We can do this by simply multiplying the turn by the speed. After that, we add the movement cost. int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } The result of this will be that we end the first turn in the fourth cell, with 4 movement points left unused. Those wasted points are factored into the cost of the fifth cell, so its distance will become 29 instead of 25. As as result, distances will end up longer than before. For example, the tenth cell used to have a distance of 50. But now two turn boundaries are crossed before we get there, wasting 8 movement points, so its distance is now 58. Taking longer than expected. Because the unused movement points are added to the cell distances, they are taken into consideration when determining the shortest path. The most efficient path wastes as few points as possible. So different speeds can result in different paths.