This is part 19 of a tutorial series about hexagon maps. This time, we'll make units travel along paths instead of teleporting.

Traveling a Path

We added units and the ability to move them around in the previous tutorial. Althrough we used pathfinding to detect valid destinations for units, we teleported them when issuing a move order. To make them actually follow the path that was found, we'll need to keep track of this path, and create an animation process to make the unit travel from cell to cell. Because it is hard to exactly see how a unit moves by watching animations, we'll also visualize the traveled path using gizmos. But before we proceed, we should fix a bug.

Turn Bug Due to an earlier oversight, we incorrectly calculate the turn at which a cell is reached. Right now, we determine the turn by dividing the cumulative distance by the unit's speed, `t = d / s`, discarding the remainder. This goes wrong when entering a cell ends up exactly consuming all remaining movement of a turn. For example, when each step costs 1 and we have speed 3, then we can move three cells per turn. However, our current calculation would only allow two steps on the first turn, because for the third step `t = d / s = 3 / 3 = 1`. Cumulative movement costs with incorrect turns, speed 3. To correctly calculate the turns, we have to move the turn boundary one step away from the starting cell. We can do this by decreasing the distance by one before determining the turn. Then the turn for the third step becomes `t = 2 / 3 = 0`. Correct turns. We can do this by changing our turn calculation to `t = (d - 1) / s`. Make this adjustment in HexGrid.Search . bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; } Adjust the turn labels as well. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … } Note that with this approach the starting cell's turn is −1. That is fine, because we don't display it and the search algorithm remains valid.

Retrieving the Path Traveling a path is an unit's job. In order to do so, it needs to know which path to travel. HexGrid has this information, so give it a method to retrieve the current path in the form of a list of cells. It can grab one from the list pool and return it, if there actually is a path. public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; } The list is filled by following the path reference from the destination back to the start, like we do when visualizing it. List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path; In this case, we want the entire path, which also includes the starting cell. for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path; We now have the path in reverse order. While we could work with this, it isn't intuitive. So let's reverse the list so it's ordered from start to destination. path.Add(currentPathFrom); path.Reverse(); return path;

Requesting a Journey Now we can add a method to HexUnit to order it to follow a path. Initially, we'll simply let it teleport to the destination. We won't immediately put the list back in its pool, because we'll be using it for a while. using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … } To actually request a journey, change HexGameUI.DoMove so it invokes the new method with the current path, instead of directly setting the unit's location. void DoMove () { if (grid.HasPath) { // selectedUnit.Location = currentCell; selectedUnit.Travel(grid.GetPath()); grid.ClearPath(); } }

Visualizing the Path Before we start animating the unit, let's check whether the paths are correct. We'll do this by having HexUnit remember the path it's supposed to travel, so it can visualize it using gizmos. List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; } Add an OnDrawGizmos method to show the last path that should be traveled, if there is any. If the unit hasn't moved yet, the path should be null . But it could also be an empty list after a recompile while in play mode, due to Unity's edit-time serialization. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } } The simplest way to show the path is to draw a gizmo sphere for each cell in it. Spheres with a radius of 2 units are clearly visible, without obstructing too much. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } } Because we show the paths per unit, we can see all their latest paths at once. Gizmos show the paths last traveled. To get a better indication of how the cells are connected, draw multiple spheres on a line between the previous and current cells in the loop. This requires us to start the process at the second cell. The spheres can be placed via linear interpolation, in increments of 0.1, so we get ten spheres per segment. for (int i = 1 ; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere( Vector3.Lerp(a, b, t) , 2f); } } More obvious paths.

Sliding Along the Path We can use the same approach to move units. Let's create a coroutine for this. Instead of drawing a gizmo, set the unit's position. Use the time delta instead of fixed 0.1 increments. And yield each iteration. That will move the unit from one cell to the next in one second. using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … } Start the coroutine at the end of the Travel method. But before that, stop all existing coroutines. That ensures that we don't have two running at the same time, which could produce very weird results. public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); } Moving only one cell per second is rather slow. Player's don't want to wait that long while playing a game. You could make unit movement speed a configuration option, but for now let's just use a constant. I set it to four cells per second, which is reasonably fast while still slow enough that you can see what's going on. const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed ) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } Just like we can visualize multiple paths at the same time, we can have multiple units traveling at the same time as well. As far as the game state is concerned, movement is still teleportation. The animation is purely visual. Units immediately occupy their destination cell. You could even find paths and initiate a new journey before they have arrived. In that case, they'll visually teleport to the start of the new path. You could prevent this by locking units or even the entire UI while they're moving, but this quick response is quite convenient while developing and testing movement. Traveling units. What about elevation differences? Because we interpolate between the cell positions, we also interpolate the vertical position of the unit. As this doesn't match the actual geometry, the unit ends up hovering above and sinking below the terrain while moving. As the animation is fast and typically seen from afar, this is usually not visually obvious. Players are busy playing the game, not scrutinizing the motion of individual units. If you analyze strategy games with varying elevation – for example Endless Legend – you will find that their units hover and sink as well. If they can get away with it, so can we.