This tutorial is the seventh part of a series about hexagon maps. In part six, we added rivers to our terrain. This time, we'll add roads.

The other operation that can invalidate roads is an elevation change. In this case, we'll have to check for roads in all directions. If an elevation difference has become too great, an existing road has to be removed.

We can suffice with setting the road to false , regardless whether there actually was a road. This will always refresh both cells, so we don't have to explicitly invoke RefreshSelfOnly anymore in SetOutgoingRiver .

We made sure that roads are only added when allowed. Now we have to make sure to remove them when they become invalid later. For example, when adding a river. We could disallow rivers to be placed on top of roads, but rivers aren't stopped by roads. Let them wash the roads away.

Now we can enforce that roads are only added when the elevation difference is small enough. I'll limit it to slopes at most, so that's a maximum of 1.

Roads also cannot be combined with cliffs, as they're too steep. Or maybe you might be OK with a road across a low cliff, but not across a high cliff? To determine this, we can create a method that tells us the elevation difference in a certain direction.

We cannot have both a river and a road going in the same direction. So make sure that there is room for the new road, before adding it.

Adding a road works like removing a road. The only difference is that we set the boolean to true instead of to false . We can create a private method that can do either. Then we can use it both when adding and removing roads.

And after we're done, we have to make sure that both cells are refreshed. As the roads are local to the cells, we only have to refresh the cells themselves, not also their neighbors.

Of course we also have to disable the corresponding roads of the cell's neighbors.

Just like with rivers, we'll add a method to remove all roads from a cell. It's done with a loop that turns off each road that was previously enabled.

It is also handy to know whether a cell has at least one road, so add a property for that. Just loop through the array and return true as soon as you find a road. If there isn't any, return false .

The most straightforward way to keep track of roads per cell is to use an array of booleans. Add a private array field to HexCell and make it serializable, so we can see it in the inspector. Set the array size via the cell prefab so it supports six roads.

Allowing roads to go in all six directions means that a cell can contain from zero to six roads. This leads to fourteen possible road configurations. That's a lot more than the five possible configurations for rivers. To make this workable, we'll have to use a generic approach that can handle all configurations.

Like rivers, roads go from cell to cell, through the middle of cell edges. The big difference is that roads don't have flowing water, so they're bidirectional. Also, a functional road network requires crossroads, so we'll support more than two roads per cell.

You can now edit roads, although they aren't visible yet. You can use the inspector to verify that it does work.

Because I'm now using two rows of three options for the colors, there's room for another color. So I added an entry for orange.

This will result in a pretty tall UI. To combat this, I changed the layout of the color panel to match the more compact road and river panels.

You can quickly add a road panel to the UI by copying the river panel and adjusting the method that the toggles invoke.

The EditCell method now has to support removing and adding roads as well. That means it has two possible actions to take when a drag happened. Restructure the code a bit so both toggle states are checked when there's a valid drag.

Editing roads works exactly like editing rivers. So HexMapEditor requires another optional toggle, plus an accompanying method to set its state.

Triangulating Roads

To visualize the roads, we'll have to triangulate them. This works like the water mesh for rivers, except that the terrain doesn't get a channel.

First, create a new standard shader that once again uses the UV coordinates to color the road surface.

Shader "Custom/ Road " { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = fixed4(IN.uv_MainTex, 1, 1) ; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }

Create a road material that uses this shader.

Road material.

Then adjust the chunk prefab so it gets another hex mesh child, for the roads. This mesh shouldn't cast shadows, and only uses UV coordinates. The quickest way to do this – via a prefab instance – is to duplicate the Rivers object and change its material.

Roads child object.

After that, add a public HexMesh roads field to HexGridChunk and include it in Triangulate . Connect it to the Roads object via the inspector.

public HexMesh terrain, rivers , roads ; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); }

Roads object connected.

Roads Between Cells Let's first consider the road segments in between cells. Like rivers, roads will cover the middle two quads. We'll completely cover these connection quads with road quads, so we can use the same six vertex positions. Add a TriangulateRoadSegment method to HexGridChunk for this. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); } As we don't have to worry about water flow, we don't need the V coordinate, so we'll just set it zero everywhere. We can use the U coordinate to indicate whether we're at the middle of the road, or at the side. Let's set it to 1 at the middle and 0 at both sides. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); } Road segment between cells. TriangulateEdgeStrip is the logical place to invoke this method, but only when a road is actually there. Add a boolean parameter to the method, so we can communicate this information. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 , bool hasRoad ) { … } Of course we get compiler errors now, because we're not supplying this information yet. The solution is to add false as a final argument everywhere we invoke TriangulateEdgeStrip . However, we can also declare that the default value for this parameter is false . This turns it into an optional parameter and solves the compile errors. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { … } How do optional parameters work? Think of them as a shorthands for writing alternative methods that fill in the missing arguments. For example, the method int MyMethod (int x = 1, int y = 2) { return x + y; } is equivalent to three methods int MyMethod (int x, int y) { return x + y; } int MyMethod (int x) { return MyMethod(x, 2); } int MyMethod () { return MyMethod(1, 2}; } Order matters here. Optional parameters can be omitted from right to left. The last parameter is dropped first. They always come after mandatory parameters. To triangulate the road, simply invoke TriangulateRoadSegment with the middle six vertices, if there is a need for it. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } } That takes care of flat cell connections. To support roads on terraces, we have to also tell TriangulateEdgeTerraces whether it has to add a road. It can simply pass this knowledge on to TriangulateEdgeStrip . void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell , bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, 1); TriangulateEdgeStrip(begin, beginCell.Color, e2, c2 , hasRoad ); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, i); TriangulateEdgeStrip(e1, c1, e2, c2 , hasRoad ); } TriangulateEdgeStrip(e2, c2, end, endCell.Color , hasRoad ); } TriangulateEdgeTerraces is invoked inside TriangulateConnection . This is where we can determine whether there's actually a road going through the current direction. Both when triangulating an edge, and when triangulating terraces. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces( e1, cell, e2, neighbor , cell.HasRoadThroughEdge(direction) ); } else { TriangulateEdgeStrip( e1, cell.Color, e2, neighbor.Color , cell.HasRoadThroughEdge(direction) ); } Road segments between cells.

Rendering On Top When drawing roads, you'll see road segments pop up in between cells. The middle of these segments will be magenta, transitioning to blue at the sides. However, when you move the camera around, the segments will probably flicker, and sometimes disappear completely. This happens because the road triangles exactly overlap the terrain triangles. It is arbitrary which ends up rendered on top. Fixing this requires two steps. First, we want to always draw the roads after the terrain has been drawn. This is accomplished by rendering them after the regular geometry is drawn, by putting them in a later render queue. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } Second, we want to make sure that the roads are drawn on top of the terrain triangles that sit in the same position. We do this by adding a depth test offset. This lets the GPU treat the triangles as if they are closer to the camera than they really are. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } LOD 200 Offset -1, -1

Roads Across Cells When triangulating rivers, we only had to deal with at most two river directions per cell. We could identify the five possible scenarios and triangulate them differently to create well-behaved rivers. However, there are fourteen possible scenarios for roads. We're not going to use a different approach for each of these scenarios. Instead, we'll treat each of the six cell directions the exact same way, regardless of the specific road configuration. When there's a road running across a cell part, we'll run it straight to the cell center, without going outside of the triangular zone. We'll draw a road segment from the edge halfway towards the center. Then we'll use two triangles to cover the rest of the way to the center. Triangulating part of a road. To triangulate this, we need to know the cell's center, the left and right middle vertices, and the edge vertices. Add a TriangulateRoad method with the corresponding parameters. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { } We need one additional vertex to construct the road segment. It sits between the left and right middle vertices. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); } Now we can also add the remaining two triangles. TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); And we have to add the UV coordinates of the triangles as well. Two of their vertices sit in the middle of the road, the other at its edge. roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); For now, let's only concern ourselves with cells that don't have a river in them. In those cases, Triangulate simply created an edge fan. Move this code to its own method. Then add an invocation of TriangulateRoad , when there's actually a road. The left and right middle vertices can be found by interpolating between the center and the two corner vertices. void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); } … } void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoadThroughEdge(direction)) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e ); } } Roads across cells.

Road Edges We can now see the roads, but they taper toward the cell centers. Because we're not checking which of the fourteen road scenarios we're dealing with, we can't move the road center to produce more pleasing shapes. What we can do instead is add additional road edges in other parts of the cell. When a cell has roads through it, but not in the current direction, add a road edge triangle. This triangle is defined by the center, and the left and right middle vertices. In this case, only the center vertex lies in the middle of the road. The other two vertices sits at its edge. void TriangulateRoadEdge (Vector3 center, Vector3 mL, Vector3 mR) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); } Part of the edge of a road. Whether we should triangulate a full road or only an edge, is something we'll leave up to TriangulateRoad . To do so, it needs to know whether a road is going through the direction of the current cell edge. So add a parameter for that. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e , bool hasRoadThroughCellEdge ) { if (hasRoadThroughCellEdge) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { TriangulateRoadEdge(center, mL, mR); } } TriangulateWithoutRiver will now have to invoke TriangulateRoad whenever the cell has any roads going through it. And it'll have to pass along whether a road goes through the current edge. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if ( cell.HasRoads ) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e , cell.HasRoadThroughEdge(direction) ); } } Roads with complete edges.