This tutorial is the ninth part of a series about hexagon maps. This installment is about adding details to the terrain. Features like buildings and trees.

It sure feels like it. But we shouldn't be concerned about that at this time. First we get feature placement right. Once we've covered that, and it turns out to be a bottleneck, then we can get smart about efficiency. That's when we might end up using the HexFeatureManager.Apply method as well. But that's for a future tutorial. Fortunately, it really isn't that bad, because we've split the terrain into chunks.

A quick way to do this is by creating a container game object and make all features children of this object. Then when Clear is invoked, we destroy this container and create a new one. The container itself will be a child of its manager.

Every time a chunk is refreshed, we create new features. This means that we currently keep creating more and more features in the same positions. To prevent duplicates, we have to get rid of the old features when the chunk is cleared.

Of course our cells are perturbed, so we should perturb the position of our features as well. That does away with the perfect regularity of the grid.

This approach is specifically for the default cube. If you're using a custom mesh, it is a better idea to design them so their local origin sits at their bottom. Then you don't have to adjust the position at all.

From now on, the terrain will be filled with cubes. At least, the top half of cubes. Because the local origin of Unity's cube mesh lies at the center of the cube, the bottom half is submerged below the terrain surface. To place the cubes on top of the terrain, we have to move them upwards by half their height.

Our setup is complete, we can start adding features! It's as simple as instantiating the prefab in HexFeatureManager.AddFeature and setting the position.

Our feature managers need a reference to this prefab, so add one to HexFeatureManager , then hook them up. Because placement requires accessing the transform component, use that as the reference type.

What kind of feature shall we make? For our first test, a cube will do. Create a fairly large cube, say scaled to (3, 3, 3), and turn it into a prefab. Create a material for it as well. I used a default material with a red color. Remove its collider, as we don't need it.

Now we need the actual feature manager. Add another child object to the Hex Grid Chunk prefab and give it a HexFeatureManager component. Then we can connect the chunk to it.

We can now add a reference to such a component to HexGridChunk . Then we can include it in the triangulation process, just like all the HexMesh children.

Let's create a HexFeatureManager component that's responsible for the features of a single chunk. Using the same design as HexMesh , we'll give it a Clear , an Apply , and an AddFeature method. As features have to be placed somewhere, the AddFeature method gets a position parameter.

HexGridChunk doesn't care about how a mesh works. It simply orders one of its HexMesh children to add a triangle, or a quad. Likewise, it can have a child that takes care of feature placement for it.

While the shape of our terrain has variation, there isn't much going on. It is a lifeless place. To make it come alive, we need to add things like trees and building. These features are not part of the terrain mesh. They are separate objects. But that doesn't stop us from adding them when triangulating the terrain.

Many features would produce many draw calls, but Unity's dynamic batching helps us out here. As the features are small, their meshes should have few vertices. That allows many of them to be combined in a single batch. But if it turns out to be a bottleneck, we'll deal with it later. It is also possible to use instancing, which is comparable to dynamic batching when using many small meshes.

This produces a lot more features! They appear next to roads, but they still shy away from rivers. To get features along rivers, we can also add them when inside TriangulateAdjacentToRiver . But once again only when not underwater, and not on top of a road.

We do this in the other Triangulate method, when we know that there isn't a river. We still have to check whether we're underwater or whether there's a road. But in this case, we only care about roads going in the current direction.

Having only a single feature per cell isn't very much. There's plenty of room for more. Let's add an addition feature to the center of each of a cell's six triangles. So one per direction.

So let's make sure that a cell is clear before we add a feature to it in HexGridChunk.Triangulate .

We're currently placing a feature in the center of every cell. This is fine for otherwise empty cells. But it doesn't look good for cells that contain rivers and roads, or that are underwater.

Now HexFeatureManager.AddFeature has access to two hash values. Let's use the first one to decide whether we actually add a feature, or skip it. If the value is 0.5 or larger, we bail. This will eliminate about half of the features. We use the second value to determine the rotation, as usual.

We're only storing these structures in our hash grid, which is static so isn't serialized by Unity during recompiles. So it doesn't need to be serializable.

So now we need two hash values instead of one. We support this by using Vector2 instead of float as our hash grid array type. But vector operations don't make sense for our hash values, so let's create a special struct for this purpose. All it needs are two floats. And let's add a static method to create a randomized value pair.

While features have varying rotations, their placement still has an obvious pattern. Every cell has seven features crowding it. We can introduce chaos to this setup by arbitrarily omitting some of the features. How can we decide whether to add a feature or not? By consulting another random value!

Go back to HexFeatureManager.AddFeature and use our new hash grid to obtain a value. Once we use that to set the rotation, our features will remain motionless when we edit the terrain.

Now we produce a different value for each square unit. We don't actually need a grid this dense. The features are further apart than that. We can stretch the grid by scaling down the position before computing the index. A unique value per 4 by 4 square should be sufficient.

This works for positive coordinates, but not for negative coordinates, as the remainder would be negative for those numbers. We can fix that by adding the grid size to negative results.

This is the modulo operator. It computer the remainder of divisions, in our case integer divisions. For example, the sequence −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 becomes −1, 0, −2, −1, 0, 1, 2, 0, 1.

To make use of the hash grid, add a sampling method to HexMetrics . Like SampleNoise , it uses the XZ coordinates of a position to retrieve a value. The hash index is found by clamping the coordinates to integer values, then taking the remainder of the integer division by the grid size.

The public seed allows us to choose a seed value for the map. Any value will do. I picked 1234.

Initialization of the hash grid is done by HexGrid , at the same time that it assigns the noise texture. So that's in HexGrid.Start and HexGrid.Awake . Make sure that we're not generating it more often than necessary.

Now that we have initialized the random number stream, we'll always get the same sequence out of it. So all supposedly random events that would happen after generating the map will always be the same as well. We can prevent this by saving the state of the random number generator before initializing it. After we're done, we set it back to its old state.

To allow recreation of the exact same features, we have to add a seed parameter to our initialization method.

The random values are generated by a mathematical formula that always produces the same results. Which sequence you get depends on a seed number, which defaults to the current time value. That's why you get different results each play session.

We can create a hash grid with an array of floats and fill it once with random values. That way we don't need a texture at all. Let's add it to HexMetrics . 256 by 256 should offer enough variety.

We have a noise texture, which is always the same. However, that texture contains Perlin gradient noise, which is locally coherent. This is exactly what we want when perturbing the cell positions of vertices. But we don't need coherent rotations. All rotations should be equally likely and mixed up. So what we need is a texture with non-gradient random values, and sample it without bilinear filtering. That is actually a hash grid, which forms the basis for gradient noise.

This produces a much more varied result. Unfortunately, every time a chunk is refreshed, its features end up with new random rotations. Editing something shouldn't case the nearby features to spasm, so we need a different approach.

All our feature objects have the exact same orientation, which doesn't look organic at all. So let's give each a random rotation.

A quick way to make use of the urban level is to multiply it by 0.25 and use that as the new threshold to bail. That way, the probability of a feature appearing increases by 25% per level.

Now that we have an urban level, we have to use that to determine whether we place features or not. To do so, we have to add the urban level as an extra parameter to HexFeatureManager.AddFeature . Let's go one step further and just pass along the cell itself. That will be more convenient later.

How many levels do we need? Let's stick to four, representing zero, low, medium, and high density development.

Add another slide to the UI and connect it to the appropriate methods. I put it in a new panel on the right side of the screen, to prevent overcrowding of the left panel.

We could ensure that the urban level is zero for underwater cell, but that is not necessary. We already omit features when underwater. And maybe we'll add urban water features at some point, like docks or underwater structures.

As our red cubes don't look like natural features of the terrain, let's say that they are buildings. They represent urban development. So let's add an urban level to HexCell .

Instead of placing features everywhere, let's make them editable. But we're not going to paint individual features. Instead, we'll add a feature level to every cell. This level controls the likelihood of features appearing in the cell. The default is zero, which guarantees that there are no features present.

Multiple Feature Prefabs

A difference in feature probability is not sufficient to create a clear distinction between lower and higher urban levels. Some cells simply end up with fewer or more buildings than expected. We can make the difference much clearer by using a different prefab for each level.

Get rid of the featurePrefab field in HexFeatureManager and replace it with an array for the urban prefabs. Use the urban level minus one as an index to retrieve the appropriate prefab.

// public Transform featurePrefab; public Transform[] urbanPrefabs; public void AddFeature (HexCell cell, Vector3 position) { … Transform instance = Instantiate( urbanPrefabs[cell.UrbanLevel - 1] ); … }

Create two duplicates of the feature prefab and rename and adjust them to represent the three different urban levels. Level 1 is low density, so I used a unit-sized cube to represent a hovel. I set the scale of the level 2 prefab to (1.5, 2, 1.5) to suggest a larger two-story building. For level 3, I used (2, 5, 2) to indicate a high-rise.

Using a different prefab for each urban level.

Mixing Prefabs We don't need to limit ourselves to a strict segregation of building type. We can mix them a bit, just like in the real world. Instead of using a single threshold per level, let's use three per level, one per building type. For level 1, let's use a 40% chance for a hovel. The other building won't appear at all. This requires the threshold triplet (0.4, 0, 0). For level 2, let's replace the hovels with larger buildings, and add a 20% chance for additional hovels. Still no high-rises. That suggests the threshold triplet (0.2, 0.4, 0). For level 3, let's upgrade the medium buildings to high-rises, replace the hovels again, and add another 20% change for more hovels. The thresholds for that would be (0.2, 0.2, 0.4). So the idea is that we upgrade existing building and add new ones in empty lots as the urban level increases. To replace an existing building, we have to use the same hash value ranges. If hashes between 0 and 0.4 were hovels at level 1, the same range should produce high-rises at level 3. Specifically, at level 3 high-rises should spawn for hash values in the 0–0.4 range, the two-story houses in the 0.4–0.6 range, and the hovels in the 0.6–0.8 range. If we check them from highest to lowest, we can do this with the threshold triplet (0.4, 0.6, 0.8). The level 2 thresholds then become (0, 0.4, 0.6), and the level 1 thresholds become (0, 0, 0.4). Let's store these thresholds in HexMetrics as a collection of arrays, with a method to get the thresholds for a specific level. As we're only concerned with levels that have features, we ignore level 0. static float[][] featureThresholds = { new float[] {0.0f, 0.0f, 0.4f}, new float[] {0.0f, 0.4f, 0.6f}, new float[] {0.4f, 0.6f, 0.8f} }; public static float[] GetFeatureThresholds (int level) { return featureThresholds[level]; } Next, we add a method to HexFeatureManager which uses a level and hash value to select a prefab. If the level is larger than zero, we retrieve the thresholds using the level decreased by one. Then we loop through the thresholds until one exceeds the hash value. That means we found a prefab. If we didn't, we return null. Transform PickPrefab (int level, float hash) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i]; } } } return null; } This approach requires us to reorder the prefab references so they go from high to low density. Reversed prefabs. Use this new method in AddFeature to pick a prefab. If we end up without one, bail. Otherwise, instantiate it and continue as before. public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); // if (hash.a >= cell.UrbanLevel * 0.25f) { // return; // } // Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f); instance.SetParent(container, false); } Mixing prefabs.