This tutorial is the fifth part of a series about hexagon maps. Up to this point we've worked with a very small map. It's time we scale it up.

Grid Chunks

We cannot make our grid too large, because we run into the limits of what can fit in a single mesh. The solution? Use more than one mesh. To do so, we have to partition our grid into multiple chunks. We'll use rectangular chunks of a fixed size.

Partitioning a grid into 3 by 3 segments.

Let's use 5 by 5 blocks, so that's 25 cells per chunk. Define that in HexMetrics .

public const int chunkSizeX = 5, chunkSizeZ = 5;

What is a good chunk size? It depends. Using larger chunks means that you'll have fewer but larger meshes. This leads to fewer draw calls. But smaller chunks work better with frustum culling, which leads to fewer triangles being drawn. The pragmatic approach is to just pick a size and fine-tune later.

Now we can no longer use any size for our grid, we have to use multiples of the chunk size. So let's change HexGrid so it defines its size in chunks instead of individual cells. Set it to 4 by 3 chunks by default, for a total of 12 chunks and 300 cells. This gives us a nice small test map.

public int chunkCountX = 4, chunkCountZ = 3;

We'll still use width and height , but they should become private. And rename them to cellCountX and cellCountZ . Use your editor to rename all occurrences of these variables in one go. Now it's clear when we're dealing with chunk or cell counts.

// public int width = 6; // public int height = 6; int cellCountX, cellCountZ;

Specifying size in chunks.

Adjust Awake so the cell counts are derived from the chunk counts before they are needed. Put the creation of the cells in their own method as well, to keep Awake tidy.

void Awake () { HexMetrics.noiseSource = noiseSource; gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateCells(); } void CreateCells () { cells = new HexCell[cellCountZ * cellCountX]; for (int z = 0, i = 0; z < cellCountZ; z++) { for (int x = 0; x < cellCountX; x++) { CreateCell(x, z, i++); } } }

Chunk Prefab We need a new component type to represents our grid chunks. using UnityEngine; using UnityEngine.UI; public class HexGridChunk : MonoBehaviour { } Next, create a chunk prefab. Do this by duplicating the Hex Grid object and renaming it to Hex Grid Chunk. Remove its HexGrid component and give it a HexGridChunk component instead. Then turn it into a prefab and remove the object from the scene.

Chunk prefab, with its own canvas and mesh. As HexGrid will be instantiating these chunks, give it a reference to the chunk prefab. public HexGridChunk chunkPrefab; Now with chunks. Instantiating chunks looks a lot like instantiating cells. Keep track of them with an array, and use a double loop to fill it. HexGridChunk[] chunks; void Awake () { … CreateChunks(); CreateCells(); } void CreateChunks () { chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(transform); } } } The initialization of a chunk is similar to how we used to initialize the hex grid. It sets things up in Awake and triangulates in Start . It need a reference to its canvas and mesh, and an array for its cells. However, it will not create these cells. We'll still let the grid do that. public class HexGridChunk : MonoBehaviour { HexCell[] cells; HexMesh hexMesh; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; } void Start () { hexMesh.Triangulate(cells); } }

Assigning Cells to Chunks HexGrid is still creating all the cells. This is fine, but it now has to add each cell to the correct chunk, instead of setting them up with its own mesh and canvas. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); // cell.transform.SetParent(transform, false); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … Text label = Instantiate<Text>(cellLabelPrefab); // label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = cell.coordinates.ToStringOnSeparateLines(); cell.uiRect = label.rectTransform; cell.Elevation = 0; AddCellToChunk(x, z, cell); } void AddCellToChunk (int x, int z, HexCell cell) { } We can find the correct chunk via integer divisions of x and z by the chunk sizes. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; } Using the intermediate results, we can also determine the cell's index local to its chunk. Once we have that, we can add the cell to the chunk. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; int localX = x - chunkX * HexMetrics.chunkSizeX; int localZ = z - chunkZ * HexMetrics.chunkSizeZ; chunk.AddCell(localX + localZ * HexMetrics.chunkSizeX, cell); } The HexGridChunk.AddCell then put the cell in its own array. Then it sets the parents of the cell and its UI. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }

Cleaning Up At this point HexGrid can get rid of its canvas and hex mesh child objects and code. // Canvas gridCanvas; // HexMesh hexMesh; void Awake () { HexMetrics.noiseSource = noiseSource; // gridCanvas = GetComponentInChildren<Canvas>(); // hexMesh = GetComponentInChildren<HexMesh>(); … } // void Start () { // hexMesh.Triangulate(cells); // } // public void Refresh () { // hexMesh.Triangulate(cells); // } Because we got rid of Refresh , HexMapEditor should no longer use it. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation; // hexGrid.Refresh(); } Cleaned up hex grid. After entering play mode, the map will still look the same. But the object hierarchy will be different. Hex Grid now spawns child chunk objects, which contain the cells, along with their mesh and canvas. Child chunks in play mode. There is probably something wrong with the cell labels. We initially set the label's width to 5. This was enough to show two symbols, which was fine for the small map that we used up to this point. But now we can get coordinates like −10, which have three symbols. These won't fit and will be cut off. To fix this, increase the cell label width to 10, or even more. Wider cell labels. We can now create much larger maps! As we generate the entire grid on startup, it might take a while before huge maps are created. But once that's finished, you have a large area to play with.

Fixing Editing Right now editing doesn't seem to work, because we no longer refresh the grid. We have to refresh the individual chunks, so let's add a Refresh method to HexGridChunk . public void Refresh () { hexMesh.Triangulate(cells); } When do we invoke this method? We used to refresh the entire grid every time, because there was only a single mesh. But now we have many chunks. Instead of refreshing them all every time, it would be much more efficient if we only refresh those chunks that have changed. Editing large maps would become very sluggish otherwise. How do we know which chunk to refresh? A simple way is to make sure each cell knows which chunk it belongs to. Then the cell can refresh its chunk whenever it is changed. So give HexCell a reference to its chunk. public HexGridChunk chunk; HexGridChunk can assign itself to the cell when it is added. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.chunk = this; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); } With that hooked up, add a Refresh method to HexCell as well. Whenever a cell is refreshed, it simply refreshes its chunk. void Refresh () { chunk.Refresh(); } We don't need to make HexCell.Refresh public, because the cell itself knows best when it has changed. For example, after it has adjusted its elevation. public int Elevation { get { return elevation; } set { … Refresh(); } } Actually, it would only need to refresh if its elevation has been set to a different value. It won't even need to recompute anything if we assign the same elevation to it later. So we can bail out at the start of the setter. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } … } } However, this will also skip the computation the first time the elevation is set to zero, because that is currently the grid's default elevation. To prevent this, make sure that the initial value is something that will never be used. int elevation = int.MinValue ; What's int.MinValue ? It is the lowest value that an integer can have. As C# integers are 32 bit numbers, there are 232 possible integers, divided between positive and negative numbers, and zero. One bit is used to indicate whether a number is negative. The minimum is −231 = −2,147,483,648. We'll never use that elevation level! The maximum is 231 − 1 = 2,147,483,647. It's one less than 231 because of zero. To detect a change to a cell's color, we have to turn it into a property as well. Rename it to the capitalized Color , then turn it into a property with a private color variable. The default color value is transparent black, which is fine. public Color Color { get { return color; } set { if (color == value) { return; } color = value; Refresh(); } } Color color; We now get null-reference exceptions when entering play mode. That's because we set the color and elevation to their default values, before assigning the cell to its chunk. It is fine that we don't refresh the chunks at this point, because we'll triangulate them all after initialization is done. In other words, only refresh the chunk if it has been assigned. void Refresh () { if (chunk) { chunk.Refresh(); } } We can once again edit cells! However, there is a problem. Seams can appear when painting along chunk boundaries. Errors at chunk boundaries. This makes sense, because when one cell changes, all connections with its neighbors change as well. And those neighbors can end up in different chunks. The simplest solution is to refresh the chunks of all neighbors as well, if they're different. void Refresh () { if (chunk) { chunk.Refresh(); for (int i = 0; i < neighbors.Length; i++) { HexCell neighbor = neighbors[i]; if (neighbor != null && neighbor.chunk != chunk) { neighbor.chunk.Refresh(); } } } } While this works, we can end up refreshing a single chunk multiple times. And once we start painting more than one cell at a time, this will only get worse. But we don't have to immediately triangulate when a chunk is refreshed. Instead, we can take note that an update is needed, and triangulate once editing is finished. Because HexGridChunk doesn't do anything else, we can use its enabled state to signal that an update is needed. Whenever it is refreshed, we enable the component. Enabling it multiple times doesn't change anything. Later on, the component is updated. We'll triangulate at that point, and disable the component again. We'll use LateUpdate instead of Update , so we can be sure that triangulation happens after editing is finished for the current frame. public void Refresh () { // hexMesh.Triangulate(cells); enabled = true; } void LateUpdate () { hexMesh.Triangulate(cells); enabled = false; } What is the difference between Update and LateUpdate ? Each frame, the Update methods of enabled components are invoked at some point, in arbitrary order. After that's finished, the same happens with LateUpdate methods. So there are two update steps, an early and a late one. Because our component is enabled by default, we don't need to explicitly triangulate in Start anymore. So we can remove that method. // void Start () { // hexMesh.Triangulate(cells); // } 20 by 20 chunks, containing 10,000 cells.