This is part 22 of a tutorial series about hexagon maps. After adding support for exploration, we'll upgrade our vision calculations and transitions.

Visibility Transitions

A cell is either visible or invisible, because it is either in vision range of a unit, or not. Even though it looks like a unit takes a while to travel between cells, its vision jumps from cell to cell instantaneously. As a result, the visibility of cells around it change suddenly. The unit's movement appears smooth, while the visibility changes are sudden.

Ideally, visibility also changes smoothly. Cells would light up gradually as they come into view and darken slowly once they're no longer visible. Or maybe you prefer immediate transitions? Let's add a property to HexCellShaderData to toggle whether we want immediate transitions. We'll make smooth transitions the default.

public bool ImmediateMode { get; set; }

Tracking Transitioning Cells Even when showing smooth transitions, the actual visibility data is still binary. So it's purely a visual effect. This means that it's up to HexCellShaderData to keep track of visibility transitions. Give it a list to keep track of the transitioning cells. Make sure that it's empty after each initialization. using System.Collections.Generic; using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; Color32[] cellTextureData; List<HexCell> transitioningCells = new List<HexCell>(); public bool ImmediateMode { get; set; } public void Initialize (int x, int z) { … transitioningCells.Clear(); enabled = true; } … } Currently, we directly set the cell data in RefreshVisibility . This is still correct when immediate mode is active. But when it's not, we should instead add the cell to the list of transitioning cells. public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else { transitioningCells.Add(cell); } enabled = true; } Visibility now no longer appears to work, because we don't do anything with the cells in the list yet.

Looping through Transitioning Cells Instead of immediately setting the relevant values to either 255 or 0, we going to gradually increase or decrease these values. How quickly we do that determines how smooth the transitions appear. We shouldn't do it too fast, but also not too slow. One second is a good compromise between nice visuals and playability. Let's define a constant for that, so it's easy to change. const float transitionSpeed = 255f; In LateUpdate , we can now determine the delta to apply to the values, by multiplying the time delta with the speed. This has to be an integer, because we don't know how large it could get. A freak frame-rate dip could make the delta larger than 255. Also, we must keep updating as long as there are cells in transition. So make sure we remain enabled while there's something in the list. void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0 ; } It is also theoretically possible to get very high frame rates. Together with a low transition speed, this could result in a delta of zero. To guarantee progress, force the delta to have a minimum of 1. int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } After we have our delta, we can loop through all transitioning cells and update their data. Let's assume we have an UpdateCellData method for that, which has the relevant cell and the delta as parameters. int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); } At some point, a cell's transition should be finished. Let's assume that the method returns whether it's still transitioning. When that's no longer the case, we can remove the cell from the list. Afterwards, we have to decrement the iterator to not skip any cells. for (int i = 0; i < transitioningCells.Count; i++) { if (! UpdateCellData(transitioningCells[i], delta) ) { transitioningCells.RemoveAt(i--); } } The order in which we process the transitioning cells doesn't matter. So we don't have to remove the cell at the current index, which forces RemoveAt to shift all cells after it. Instead, move the last cell to the current index and them remove the last one. if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt( transitioningCells.Count - 1 ); } Now we have to create the UpdateCellData method. It's going to need the cell's index and data to do its work, so begin by fetching those. It also has to determine whether this cell still requires further updating. By default, we'll assume this is not the case. Once the work is done, the adjusted data has to be applied and the still-updating status returned. bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; }

Updating Cell Data At this point, we have a cell that is in transition, or maybe it is already finished. First, let's consider the cell's exploration state. If the cell is explored but its G value isn't 255 yet, then indeed it is still in transition, so keep track of this fact. bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data; To progress the transition, add the delta to the cell's G value. Arithmatic operations don't work on bytes, they are always converted to integers first. So the sum is an integer, which has to be cast to a byte. if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; } But we must ensure that we do no exceed 255 before casting. int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t; Next, we have to do the same thing for the visibility, which uses the R value. if (cell.IsExplored && data.g < 255) { … } if (cell.IsVisible && data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } As cells can also become invisible again, we must also check whether we have to decrease the R value. This is the case when the cell isn't visible while R is larger than zero. if (cell.IsVisible ) { if ( data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } } else if (data.r > 0) { stillUpdating = true; int t = data.r - delta; data.r = t < 0 ? (byte)0 : (byte)t; } Now UpdateCellData is complete and the visibility transitions are functional. Visibility transitions.

Preventing Duplicate Transition Entries Although the transitions work, we can end up with duplicate entries in the list. This happens when a cell's visibility state changes while it is still in transition. For example, when a cell is only visible for a short while during a unit's journey. The result of duplicate entries is that the cell's transition gets updated multiple times per frame, which leads to faster transitions and more work than necessary. We could prevent this by checking whether the cell is already in the list before adding it. However, searching a list each time RefreshVisibility is invoked is expensive, especially when many cells are already in transition. Instead, let's use one of the yet-unused data channels to store whether a cell in transition, like the B value. Set this value to 255 when the cell is added to the list. Then only add cells whose B value isn't 255. public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else if (cellTextureData[index].b != 255) { cellTextureData[index].b = 255; transitioningCells.Add(cell); } enabled = true; } To make this work, we have to set the B value back to zero once a cell's transition is finished. bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; } Transitions without duplicates.