📜 ⬆️ ⬇️

Cards from hexagons in Unity: water, objects of a relief and fortress walls

Parts 1-3: mesh, cell colors and heights

Parts 4-7: bumps, rivers and roads

Parts 8-11: water, landforms and walls
')
Parts 12-15: saving and loading, textures, distances

Parts 16-19: Pathfinding, Player Squads, Animations

Parts 20-23: fog of war, map exploration, procedural generation

Parts 24-27: water cycle, erosion, biomes, cylindrical map

Part 8: water



We have already added support for the rivers, and in this part we will completely submerge the cells in water.


Water is coming.

Water level


The easiest way to implement water support is to set it at the same level. All cells below this level are submerged in water. But a more flexible way is to support water at different heights, so let's make the water level changeable. For this, HexCell needs to monitor its water level.

  public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } waterLevel = value; Refresh(); } } int waterLevel; 

If desired, you can make sure that certain features of the relief do not exist under water. But for now I will not do this. Things like underwater roads suit me. They can be considered areas that have recently been flooded.

Flooding of cells


Now that we have water levels, the most important question is whether the cells are under water. The cell is under water if its water level is higher than its height. To get this information, we will add a property.

  public bool IsUnderwater { get { return waterLevel > elevation; } } 

This means that when the water level and height are equal, the cell rises above the water. That is, the real surface of the water is below this height. As is the case with river surfaces, let's add the same offset - HexMetrics.riverSurfaceElevationOffset . Let's change its name to more general.

 // public const float riverSurfaceElevationOffset = -0.5f; public const float waterElevationOffset = -0.5f; 

Change HexCell.RiverSurfaceY so that it uses the new name. Then add a similar property to the water surface of the flooded cell.

  public float RiverSurfaceY { get { return (elevation + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } public float WaterSurfaceY { get { return (waterLevel + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } 

Water editing


Editing the water level is similar to changing the height. Therefore, HexMapEditor should monitor the active water level and whether it should be applied to cells.

  int activeElevation; int activeWaterLevel; … bool applyElevation = true; bool applyWaterLevel = true; 

Add methods to connect these parameters with the UI.

  public void SetApplyWaterLevel (bool toggle) { applyWaterLevel = toggle; } public void SetWaterLevel (float level) { activeWaterLevel = (int)level; } 

And add the water level in EditCell .

  void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } … } } 

To add a water level to the UI, duplicate the label and the height slider, and then change them. Do not forget to attach their events to the appropriate methods.


Water level slider.

unitypackage

Water triangulation


To triangulate water, we need a new mesh with new material. First, create a Water shader by duplicating the River shader. Change it to use the color property.

 Shader "Custom/Water" { 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"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha #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 = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" } 

Create a new material with this shader by duplicating the Water material and replacing it with a shader. Let's leave the noise texture, because later we use it.


Material Water.

Add a new child to the prefab by duplicating the Rivers child. It does not need UV coordinates, and it must use Water . As usual, let's do this by creating an instance of the prefab, changing it, and then applying the changes to the prefab. After that, get rid of the instance.



Child Water object.

Next we add water mesh support to HexGridChunk .

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

And connect it with the child of the prefab.


Water object is connected.

Water hexagons


Since water forms the second layer, let's give it its own triangulation method for each of the directions. We need to call it only when the cell is immersed in water.

  void Triangulate (HexDirection direction, HexCell cell) { … if (cell.IsUnderwater) { TriangulateWater(direction, cell, center); } } void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { } 

As in the case of rivers, the height of the water surface does not vary greatly in cells with the same water level. Therefore, we do not seem to need complex edges. A simple triangle is enough.

  void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { center.y = cell.WaterSurfaceY; Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction); water.AddTriangle(center, c1, c2); } 


Hexagons of water.

Water connections


We can connect adjacent cells with water by one quadrilateral.

  water.AddTriangle(center, c1, c2); if (direction <= HexDirection.SE) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null || !neighbor.IsUnderwater) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 e1 = c1 + bridge; Vector3 e2 = c2 + bridge; water.AddQuad(c1, c2, e1, e2); } 


Joining the edges of the water.

And fill the corners with one triangle.

  if (direction <= HexDirection.SE) { … water.AddQuad(c1, c2, e1, e2); if (direction <= HexDirection.E) { HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor == null || !nextNeighbor.IsUnderwater) { return; } water.AddTriangle( c2, e2, c2 + HexMetrics.GetBridge(direction.Next()) ); } } 


Connection angles of water.

Now we have water cells connected when they are nearby. They leave a gap between themselves and dry cages with greater height, but we will leave that for later.

Consistent water levels


We assumed that the neighboring submarine cells had the same water level. If it is, then everything looks good, but if this assumption is violated, then errors occur.


Inconsistent water levels.

We can make water stay on par. For example, if the water level of a flooded cell changes, we can spread the changes to neighboring cells to keep the levels in sync. However, this process must continue until it encounters cells that are not immersed in water. These cells set the boundaries of the water body.

The danger of this approach is that it can quickly get out of control. If editing is unsuccessful, water can cover the entire map. Then all fragments will have to be triangulated at the same time, which will lead to a huge jump in delays.

So let's not do it yet. This function can be added in a more complex editor. As long as the consistency of water levels, we leave on the conscience of the user.

unitypackage

Animating water


Instead of a uniform color, we will create something resembling waves. As in other shaders, we still will not strive for beautiful graphics, we only need to designate the waves.


Perfectly flat water.

Let's do the same thing we did with the rivers. Let's sample the noise with the position of the world and add it to the uniform color. To animate the surface, add time to the V coordinate.

  struct Input { float2 uv_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz; uv.y += _Time.y; float4 noise = tex2D(_MainTex, uv * 0.025); float waves = noise.z; fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


Water scrolling, time × 10.

Two directions


So far this is not at all like waves. Let's complicate the picture by adding a second sample noise
and this time adding the U coordinate. Use a different noise channel to get two different patterns as a result. The finished waves will be these two samples stacked together.

  float2 uv1 = IN.worldPos.xz; uv1.y += _Time.y; float4 noise1 = tex2D(_MainTex, uv1 * 0.025); float2 uv2 = IN.worldPos.xz; uv2.x += _Time.y; float4 noise2 = tex2D(_MainTex, uv2 * 0.025); float waves = noise1.z + noise2.x; 

When summing both samples, we get results in the interval 0–2, so we need to scale it back to 0–1. Instead of simply dividing the waves in half, we can use the smoothstep function to create a more interesting result. We will impose ¾ – 2 on 0–1, so that there are no visible waves on the surface of the water.

  float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves); 


Two directions, time × 10.

Waves of mixing


It is still noticeable that we have two moving noise patterns that do not actually change. It would be plausible if the patterns were changed. We can accomplish this by interpolating between different channels of noise samples. But this cannot be done the same way, otherwise the entire surface of the water will change simultaneously, and this is very noticeable. Instead, we will create a wave of mixing.

We will create a mixing wave using a sine wave that moves diagonally across the surface of the water. We will do this by adding the coordinates of the world X and Z and using the sum as input to the sin function. Zoom out to get fairly large bands. And of course, add the same value to animate them.

  float blendWave = sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y); 

Sinusoids range from -1 and 1, and we need an interval of 0–1. You can get it by squaring the wave. To see the isolated result, use it instead of the changed color as the output value.

  sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y); blendWave *= blendWave; float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves); fixed4 c = blendWave; //saturate(_Color + waves); 


Waves of mixing.

To make the blending waves less noticeable, let's add a little noise from both samples to them.

  float blendWave = sin( (IN.worldPos.x + IN.worldPos.z) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave; 


Distorted mixing waves.

Finally, use a mixing wave to interpolate between the two channels of both noise samples. For maximum variability, take four different channels.

  float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave); waves = smoothstep(0.75, 2, waves); fixed4 c = saturate(_Color + waves); 


Wave mixing, time × 2.

unitypackage

Coast


We ended up with open water, but now we need to fill the gap in the water along the coast. Since we have to comply with the land contours, the coastal water requires a different approach. Let's divide the TriangulateWater into two methods - one for open water, the second for the coast. To understand when we work with the coast, we need to look at the next cell. That is, in TriangulateWater we will get a neighbor. If there is a neighbor and he is not under water, then we are dealing with the coast.

  void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { center.y = cell.WaterSurfaceY; HexCell neighbor = cell.GetNeighbor(direction); if (neighbor != null && !neighbor.IsUnderwater) { TriangulateWaterShore(direction, cell, neighbor, center); } else { TriangulateOpenWater(direction, cell, neighbor, center); } } void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction); water.AddTriangle(center, c1, c2); if (direction <= HexDirection.SE && neighbor != null) { // HexCell neighbor = cell.GetNeighbor(direction); // if (neighbor == null || !neighbor.IsUnderwater) { // return; // } Vector3 bridge = HexMetrics.GetBridge(direction); … } } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { } 


There is no triangulation along the coast.

Since the coast is distorted, we must distort the triangles of water along the coast. Therefore, we need vertices of edges and a fan of triangles.

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); water.AddTriangle(center, e1.v1, e1.v2); water.AddTriangle(center, e1.v2, e1.v3); water.AddTriangle(center, e1.v3, e1.v4); water.AddTriangle(center, e1.v4, e1.v5); } 


Fan triangles along the coast.

Next comes a strip of edges, as in the usual relief. However, we are not obliged to limit ourselves to only certain directions, because we only call TriangulateWaterShore when we meet a coast for which a strip is always needed.

  water.AddTriangle(center, e1.v4, e1.v5); Vector3 bridge = HexMetrics.GetBridge(direction); EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); water.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); water.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); water.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); 


Stripes of ribs along the coast.

Similarly, we must also add an angular triangle every time.

  water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { water.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); } 


The angles of the ribs along the coast.

Now we have ready water coast. A part of it is always below the relief relief, so there are no holes.

UV coast


We can leave everything as it is, but it would be interesting if the coastal water had its own schedule. For example, the effect of foam, which becomes larger when approaching the coast. To implement it, the shader must know how close the fragment is to the coast. We can transmit this information via UV coordinates.

Open water does not have UV coordinates, and does not need foam. It is needed only for water near the coast. Therefore, the requirements for both types of water are quite different. It would be logical to create your own mesh for each type. Therefore, we will add support for one more mesh object to HexGridChunk.

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

This new mesh will use TriangulateWaterShore .

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); } } 

Duplicate the water object, connect it to the prefab and configure it so that it uses the UV coordinates. We will also create a shader and material for coastal water, duplicating the existing shader and water material.


Water shore object and material with UV.

Change the Water Shore shader so that instead of water it displays the UV coordinates.

  fixed4 c = fixed4(IN.uv_MainTex, 1, 1); 

Since the coordinates are not yet specified, it will display a solid color. This makes it easy to see that the coast actually uses a separate mesh with material.


Separate mesh for the coast.

Let's put information about the coast in the coordinate V. On the water side, we assign it a value of 0, on the land side - a value of 1. Since we don’t need to transmit anything more, all U coordinates will simply be 0.

  waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0f) ); } 


Transitions to the coasts, wrong.

The above code works for edges, but is erroneous in some angles. If the next neighbor is under water, then this approach will be correct. But when the next neighbor is not under water, the third vertex of the triangle will be under dry land.

  waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) ); 


Transitions to the coast, correct.

Foam on the coast


Now that the transitions to the coast have been implemented correctly, you can use them to create a foam effect. The easiest way is to add the value of the shore to a uniform color.

  void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = shore; fixed4 c = saturate(_Color + foam); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


Linear foam.

To make the foam more interesting, multiply it by the square of the sine wave.

  float foam = sin(shore * 10); foam *= foam * shore; 


Sinusoid square damping foam.

Let's make the foam front bigger as we get closer to shore. This can be done by taking its square root before using the value of the coast.

  float shore = IN.uv_MainTex.y; shore = sqrt(shore); 


Foam becomes thicker near the shore.

Add distortion to make it look more natural. Let us make it so that when approaching the shore, the distortion becomes weaker. So it will better fit the coastline.

  float2 noiseUV = IN.worldPos.xz; float4 noise = tex2D(_MainTex, noiseUV * 0.015); float distortion = noise.x * (1 - shore); float foam = sin((shore + distortion) * 10); foam *= foam * shore; 


Distorted foam.

And, of course, all this is animated: a sine wave, and distortion.

  float2 noiseUV = IN.worldPos.xz + _Time.y * 0.25; float4 noise = tex2D(_MainTex, noiseUV * 0.015); float distortion = noise.x * (1 - shore); float foam = sin((shore + distortion) * 10 - _Time.y); foam *= foam * shore; 


Animated foam.

In addition to foam arriving, there is a retreating. Let's add a second sinusoid for its simulation, which moves in the opposite direction. Make it weaker and add a time shift. The finished foam will be the maximum of these two sine waves.

  float distortion1 = noise.x * (1 - shore); float foam1 = sin((shore + distortion1) * 10 - _Time.y); foam1 *= foam1; float distortion2 = noise.y * (1 - shore); float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2); foam2 *= foam2 * 0.7; float foam = max(foam1, foam2) * shore; 


Inbound and receding foam.

Mixing waves and foam


There is a sharp transition between open and coastal waters, because open water waves are not included in the coastal waters. To fix this, we need to include these waves in the Water Shore shader.

Instead of copying the code of the waves, let's paste it into the Water.cginc include file. In fact, we insert the code for foam and for waves, each as a separate function.

How do shader include files work?
Creating your own shaders include-files is covered in the Rendering 5, Multiple Lights tutorial.

 float Foam (float shore, float2 worldXZ, sampler2D noiseTex) { // float shore = IN.uv_MainTex.y; shore = sqrt(shore); float2 noiseUV = worldXZ + _Time.y * 0.25; float4 noise = tex2D(noiseTex, noiseUV * 0.015); float distortion1 = noise.x * (1 - shore); float foam1 = sin((shore + distortion1) * 10 - _Time.y); foam1 *= foam1; float distortion2 = noise.y * (1 - shore); float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2); foam2 *= foam2 * 0.7; return max(foam1, foam2) * shore; } float Waves (float2 worldXZ, sampler2D noiseTex) { float2 uv1 = worldXZ; uv1.y += _Time.y; float4 noise1 = tex2D(noiseTex, uv1 * 0.025); float2 uv2 = worldXZ; uv2.x += _Time.y; float4 noise2 = tex2D(noiseTex, uv2 * 0.025); float blendWave = sin( (worldXZ.x + worldXZ.y) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave; float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave); return smoothstep(0.75, 2, waves); } 

Change the Water shader to use the new include file.

  #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 

The Water Shore shader calculates values ​​for both foam and waves. Then we mute the waves as we approach the shore. The finished result will be a maximum of foam and waves.

  #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


Mixing foam and waves.

unitypackage

Again about coastal water


Part of the coastal mesh is hidden under the mesh of the relief. This is normal, but only a small part is hidden. Unfortunately, steep cliffs hide most of the coastal water, and therefore foam.


Almost hidden coastal water.

We can handle this by increasing the size of the coastline. This can be done by reducing the radius of the hexagons of water. To do this, in addition to the integrity coefficient, we need HexMetrics a water ratio, as well as methods for obtaining water angles.

The integrity coefficient is 0.8. To double the size of the water compounds, we need to assign a value of 0.6 to the water coefficient.

  public const float waterFactor = 0.6f; public static Vector3 GetFirstWaterCorner (HexDirection direction) { return corners[(int)direction] * waterFactor; } public static Vector3 GetSecondWaterCorner (HexDirection direction) { return corners[(int)direction + 1] * waterFactor; } 

We use these new methods HexGridChunkto find the angles of the water.

  void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstWaterCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondWaterCorner(direction); … } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstWaterCorner(direction), center + HexMetrics.GetSecondWaterCorner(direction) ); … } 


Using angles of water.

The distance between the hexagons of water has indeed doubled. Now HexMetricsalso should have a method of creating bridges in the water.

  public const float waterBlendFactor = 1f - waterFactor; public static Vector3 GetWaterBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * waterBlendFactor; } 

Let's change it HexGridChunkso that it uses the new method.

  void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (direction <= HexDirection.SE && neighbor != null) { Vector3 bridge = HexMetrics.GetWaterBridge(direction); … if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); } } } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … Vector3 bridge = HexMetrics.GetWaterBridge(direction); … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetWaterBridge(direction.Next()) ); … } } 


Long bridges in the water.

Between the ribs of water and land


Although this gives us more space for the foam, now even more is hidden under the relief. Ideally, we will be able to use the water edge from the water side, and the land edge from the land side.

We cannot use a simple bridge to find the opposite edge of the land, if we start from the corners of the water. Instead, we can go in the opposite direction, from the center of the neighbor. Modify TriangulateWaterShoreto use this new approach.

 // Vector3 bridge = HexMetrics.GetWaterBridge(direction); Vector3 center2 = neighbor.Position; center2.y = center.y; EdgeVertices e2 = new EdgeVertices( center2 + HexMetrics.GetSecondSolidCorner(direction.Opposite()), center2 + HexMetrics.GetFirstSolidCorner(direction.Opposite()) ); … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { Vector3 center3 = nextNeighbor.Position; center3.y = center.y; waterShore.AddTriangle( e1.v5, e2.v5, center3 + HexMetrics.GetFirstSolidCorner(direction.Previous()) ); … } 


The wrong corners of the edges.

It worked, but now we again need to consider two cases for corner triangles.

  HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { // Vector3 center3 = nextNeighbor.Position; // center3.y = center.y; Vector3 v3 = nextNeighbor.Position + (nextNeighbor.IsUnderwater ? HexMetrics.GetFirstWaterCorner(direction.Previous()) : HexMetrics.GetFirstSolidCorner(direction.Previous())); v3.y = center.y; waterShore.AddTriangle(e1.v5, e2.v5, v3); waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) ); } 


The correct angles of the edges.

It worked well, but now that most of the foam is visible, it becomes quite pronounced. To compensate for this, we will make the effect a bit weaker, reducing the scale of the coast in the shader.

  shore = sqrt(shore) * 0.9; 


Ready foam.

unitypackage

Submarine rivers


We ended up with water, at least in those places where rivers do not flow into it. Since water and rivers do not notice each other yet, rivers will flow through and under the water.


Rivers flowing in water.

The order in which translucent objects are rendered depends on their distance from the camera. The closest objects are rendered last, so they are at the top. When you move the camera, this will mean that sometimes rivers and sometimes water will appear over each other. Let's start by making the rendering order constant. Rivers must be drawn on top of the water in order to properly display waterfalls. We can accomplish this by changing the River shader's queue .

  Tags { "RenderType"="Transparent" "Queue"="Transparent+1" } 


Draw the river last.

Hide underwater rivers


Although the river bed may well be under water, and water can actually flow through it, we should not see this water. And even more so it should not be rendered on top of the real surface of the water. We can get rid of the water of submarine rivers by adding river segments only when the current cell is not under water.

  void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; … } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; … } } 

To TriangulateConnectionbegin, we will add a segment of the river, when neither the current nor the neighboring cell is under water.

  if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; if (!cell.IsUnderwater && !neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction ); } } 


No more submarine rivers.

Waterfalls


There are no more submarine rivers, but now we have holes in those parts of the rivers where they meet the surface of the water. Rivers flush with water create small holes or overlaps. But the most noticeable are the missing waterfalls for rivers flowing from a greater height. Let's do it first.

The river segment with a waterfall used to pass through the surface of the water. As a result, it was partially above, and partly under water. We need to keep a part above the water level, discarding everything else. For this we have to work hard, so we will create a separate method.

The new method requires four peaks, two levels of rivers and water level. We will set it up so that we look in the direction of the current, down the waterfall. Therefore, the first two vertices and the left and right sides will be on top, followed by the lower ones.

  void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); } 

Let's call this method in TriangulateConnection, when a neighbor is under water and we have a waterfall created.

  if (!cell.IsUnderwater) { if (!neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY ); } } 

We also need to process waterfalls in the opposite direction, when the current cell is under water and the next cell is not.

  if (!cell.IsUnderwater) { … } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY ); } 

So we again get the quad of the source river. Next, we need to change TriangulateWaterfallInWaterso that it raises the lower peaks to the water level. Unfortunately, changing only the Y coordinates will not be enough. This can move the waterfall away from the cliff, which may cause holes to form. Instead, you have to move the lower vertices to the upper using interpolation.


We interpolate.

To move the lower peaks up, divide their distance below the water surface by the height of the waterfall. This will give us the value of the interpolator.

  v1.y = v2.y = y1; v3.y = v4.y = y2; float t = (waterY - y2) / (y1 - y2); v3 = Vector3.Lerp(v3, v1, t); v4 = Vector3.Lerp(v4, v2, t); rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); 

As a result, we get a shortened waterfall, having the same orientation. However, since the positions of the lower vertices have changed, they are distorted not in the same way as the original vertices. This means that the end result will still not coincide with the original waterfall. To solve this problem, we need to manually distort vertices before interpolation, and then add undistorted quad.

  v1.y = v2.y = y1; v3.y = v4.y = y2; v1 = HexMetrics.Perturb(v1); v2 = HexMetrics.Perturb(v2); v3 = HexMetrics.Perturb(v3); v4 = HexMetrics.Perturb(v4); float t = (waterY - y2) / (y1 - y2); v3 = Vector3.Lerp(v3, v1, t); v4 = Vector3.Lerp(v4, v2, t); rivers.AddQuadUnperturbed(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); 

Since we already have a method for adding undistorted triangles, we really do not need to create it for quad-s. Therefore, we add the required method HexMesh.AddQuadUnperturbed.

  public void AddQuadUnperturbed ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4 ) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); } 


Waterfalls end on the surface of the water.

unitypackage

Estuary


When rivers flow at the same height as the surface of the water, the mesh of the river touches the mesh of the coast. If it were a river that flows into the sea or into the ocean, then there would be a current of the river with a surf. Therefore, we will call these areas mouths.


The river meets the coast without distorting the peaks.

Now we have two problems with the mouths. Firstly, the quad rivers join the second and fourth tops of the ribs, passing the third. Since the coast of the water does not use the third peak, it can create a hole or overlap. We can solve this problem by changing the mouth geometry.

The second problem is that there is a sharp transition between foam and river materials. To solve it, we need another material that blends the effects of the river and water.

This means that the mouths require a special approach, so let's create a separate method for them. It should be called in TriangulateWaterShorewhen there is a river moving in the current direction.

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2); } else { waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); } … } void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { } 

A region that mixes both effects is not required to fill the entire band. The shape of a trapezoid is enough for us. Therefore, we can use two coastal triangles on the sides.

  void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { waterShore.AddTriangle(e2.v1, e1.v2, e1.v1); waterShore.AddTriangle(e2.v5, e1.v5, e1.v4); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); } 


Trapezoidal hole for the mixing area.

UV2 coordinates


To create the effect of the river, we need UV-coordinates. But to create a foam effect, we also need UV coordinates. That is, when mixing them, we need two sets of UV-coordinates. Fortunately, Unity engine meshes can support up to four sets of UV. We just need to add in HexMeshsupport of the second set.

  public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; [NonSerialized] List<Vector2> uvs, uv2s; public void Clear () { … if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { … if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } … } 

To add a second set of UV, we duplicate the methods of working with UV and change the image we need.

  public void AddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); } public void AddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); uv2s.Add(uv4); } public void AddQuadUV2 (float uMin, float uMax, float vMin, float vMax) { uv2s.Add(new Vector2(uMin, vMin)); uv2s.Add(new Vector2(uMax, vMin)); uv2s.Add(new Vector2(uMin, vMax)); uv2s.Add(new Vector2(uMax, vMax)); } 

River shader function


Since we will use the river effect in two shaders, we will move the code from the River shader to the new function of the include file Water .

 float River (float2 riverUV, sampler2D noiseTex) { float2 uv = riverUV; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(noiseTex, uv); float2 uv2 = riverUV; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(noiseTex, uv2); return noise.x * noise2.w; } 

Change the River shader to use this new feature.

  #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … } 

Objects of the mouth


Add in HexGridChunksupport of the mouth object mesh.

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

Create a shader, material and object mouth, duplicating the coast and changing it. Connect it to the fragment and make it use the UV and UV2 coordinates.


Estuarties object.

Estuary triangulation


We can solve the problem of a hole or overlap by placing a triangle between the end of the river and the middle edge of the water. Since our mouth shader is a duplicate of the coast shader, let's set the UV coordinates that match the foam effect.

  void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { … estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); } 


Middle triangle.

We can fill the entire trapezoid by adding a quad on both sides of the middle triangle.

  estuaries.AddQuad(e1.v2, e1.v3, e2.v1, e2.v2); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV(0f, 0f, 0f, 1f); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadUV(0f, 0f, 0f, 1f); 


Ready trapezoid.

Let's rotate the orientation of the quad to the left so that it has a shortened diagonal connection, and as a result we get a symmetric geometry.

  estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0f) ); // estuaries.AddQuadUV(0f, 0f, 0f, 1f); 


Rotated quad, symmetric geometry

River flow


To support the effect of the river, we need to add UV2 coordinates. The bottom of the middle triangle is in the middle of the river, so its U coordinate should be equal to 0.5. As the river flows in the direction of the water, the left point receives the U coordinate equal to 1, and the right one - the U coordinate with the value 0. Let the Y coordinates be 0 and 1, corresponding to the direction of flow.

  estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); 

The quadrilaterals on both sides of the triangle must coincide with this orientation. Let's keep the same U coordinates for points that exceed the width of the river.

  estuaries.AddQuadUV2( new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(1f, 0f), new Vector2(0.5f, 1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1f), new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); 


UV2 trapezoid.

To make sure that we set the UV2 coordinates correctly, let the Estuary shader render them. We can access these coordinates by adding to the input structure float2 uv2_MainTex.

  struct Input { float2 uv_MainTex; float2 uv2_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = fixed4(IN.uv2_MainTex, 1, 1); … } 


UV2 coordinates.

Everything looks good, you can use a shader to create a river effect.

  void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.uv2_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … } 


Use UV2 to create a river effect.

We created rivers in such a way that when triangulating the connections between the cells, the V coordinates of the river change from 0.8 to 1. Therefore, here we should also use this interval, and not values ​​from 0 to 1. However, the coast connection is 50% larger than normal cell connections . Therefore, for best fit with the course of the river, we must change the values ​​from 0.8 to 1.1.

  estuaries.AddQuadUV2( new Vector2(1f, 0.8f), new Vector2(1f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0f, 1.1f), new Vector2(0f, 0.8f), new Vector2(0f, 0.8f) ); 



Synchronized flow of the river and the mouth.

Flow control


While the flow of the river moves in a straight line. But when water flows into a larger area, it expands. The flow will be bent. We can simulate this by folding the UV2 coordinates.

Instead of keeping the upper coordinates U constant beyond the width of the river, let's shift them by 0.5. The leftmost point is set to 1.5, the rightmost one is −0.5.

At the same time, we expand the flow by shifting the U coordinates of the left and right points of the bottom. Change the left from 1 to 0.7, and the right - from 0 to 0.3.

  estuaries.AddQuadUV2( new Vector2(1.5f, 0.8f), new Vector2(0.7f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); … estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.1f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 0.8f) ); 



The expansion of the flow of the river.

To complete the curvature effect, change the V coordinates of the same four points. As the water flows away from the end of the river, we will increase the V coordinates of the upper points to 1. And in order to create a better curve, we will increase the V coordinates of the two lower points to 1.15.

  estuaries.AddQuadUV2( new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f) ); 



Curved river flow.

Mixing river and coast


All we have left is to mix the effects of the coast and the river. To do this, we use linear interpolation, taking the value of the coast as an interpolator.

  float shoreWater = max(foam, waves); float river = River(IN.uv2_MainTex, _MainTex); float water = lerp(shoreWater, river, IN.uv_MainTex.x); fixed4 c = saturate(_Color + water); 

Although this should work, you may get a compilation error. The compiler complains about overriding _MainTex_ST. The reason is an error inside the Unity surface shader compiler caused by using uv_MainTexand simultaneously uv2_MainTex. We need to find a workaround.

Instead of using uv2_MainTexit, we’ll have to transfer the secondary UV coordinates manually. To do this, rename uv2_MainTexto riverUV. Then we add a vertex function to the shader, which assigns coordinates to it.

  #pragma surface surf Standard alpha vertex:vert … struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; }; … void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; } void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.riverUV, _MainTex); … } 


Interpolation based on the value of the coast.

Interpolation works, except for the left and right vertex at the top. At these points, the river should disappear. Therefore, we can not use the value of the coast. We have to use a different value, which in these two vertices is 0. Fortunately, we still have the U coordinate of the first UV set, so we can store this value there.

  estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 0f) ); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f) ); estuaries.AddQuadUV( new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) ); // estuaries.AddQuadUV(0f, 0f, 0f, 1f); 


Proper mixing.

Now the mouths have a good mix between the expanding river, the coastal water and the foam. Although it does not create an exact match with waterfalls, this effect looks good with waterfalls.


The mouths in unitypackage action



Rivers flowing from reservoirs


We already have rivers flowing into reservoirs, but there is no support for rivers flowing in a different direction. There are lakes from which rivers flow, so we need to add them too.

When a river flows out of a reservoir, it actually flows towards a greater height. This is currently not possible. We need to make an exception and allow this situation if the water level corresponds to the height of the target point. Let's add to the HexCellprivate method, checking by our new criterion whether the neighbor is the correct target point for the outgoing river.

  bool IsValidRiverDestination (HexCell neighbor) { return neighbor && ( elevation >= neighbor.elevation || waterLevel == neighbor.elevation ); } 

We will use our new method to determine whether it is possible to create an outgoing river.

  public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); // if (!neighbor || elevation < neighbor.elevation) { if (!IsValidRiverDestination(neighbor)) { return; } RemoveOutgoingRiver(); … } 

Also there it is necessary to check the rivers when the height of the cell or the water level changes. Create a private method that will take care of this task.

  void ValidateRivers () { if ( hasOutgoingRiver && !IsValidRiverDestination(GetNeighbor(outgoingRiver)) ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && !GetNeighbor(incomingRiver).IsValidRiverDestination(this) ) { RemoveIncomingRiver(); } } 

We use this new method in the properties Elevationand WaterLevel.

  public int Elevation { … set { … // if ( // hasOutgoingRiver && // elevation < GetNeighbor(outgoingRiver).elevation // ) { // RemoveOutgoingRiver(); // } // if ( // hasIncomingRiver && // elevation > GetNeighbor(incomingRiver).elevation // ) { // RemoveIncomingRiver(); // } ValidateRivers(); … } } public int WaterLevel { … set { if (waterLevel == value) { return; } waterLevel = value; ValidateRivers(); Refresh(); } } 


Outgoing and entering the lakes of the river.

Unfolding flow


We created HexGridChunk.TriangulateEstuaryby suggesting that rivers can only flow into reservoirs. Therefore, as a result, the flow of a river always moves in one direction. We need to reverse the flow when dealing with a river flowing from a reservoir. For this you need to TriangulateEstuaryknow about the direction of flow. Therefore, we will give it a boolean parameter that determines whether we are dealing with an incoming river.

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … } 

We will transmit this information when calling this method from TriangulateWaterShore.

  if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2, cell.IncomingRiver == direction); } 

Now we need to turn the flow of the river, changing the coordinates of UV2. The coordinates of U for outgoing rivers need to be mirrored: −0.5 becomes 1.5, 0 becomes 1, 1 becomes 0, and 1.5 becomes −0.5.

With V coordinates, things are a little more complicated. If you look at how we worked with inverted river connections, then 0.8 should be 0, and 1 should be −0.2. This means that 1.1 becomes −0.3, and 1.15 becomes −0.35.

Since in each case the coordinates of UV2 are very different, let's write a separate code for them.

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … if (incomingRiver) { estuaries.AddQuadUV2( new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f) ); } else { estuaries.AddQuadUV2( new Vector2(-0.5f, -0.2f), new Vector2(0.3f, -0.35f), new Vector2(0f, 0f), new Vector2(0.5f, -0.3f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, -0.3f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, -0.3f), new Vector2(0.7f, -0.35f), new Vector2(1f, 0f), new Vector2(1.5f, -0.2f) ); } } 


The correct flow of the rivers.

unitypackage

Part 9: relief objects



In this part we will talk about adding objects to the relief. We will create objects such as buildings and trees.


Conflict between forests, agricultural land and urbanization.

Add support for objects


Although the shape of the relief has variations, while nothing happens on it. It is a lifeless land. To breathe life into it, you need to add such objects. like trees and houses. These objects are not part of the relief mesh, but will be separate objects. But this will not prevent us from adding them when triangulating the terrain.

HexGridChunkdon't care how the mesh works. He simply orders one of his children to HexMeshadd a triangle or quad. Likewise, it can have a child element that places objects on them.

Object Manager


Let's create a component HexFeatureManagerthat deals with objects within one fragment. We use the same scheme as in HexMesh- we give it methods Clear, Applyand AddFeature. Since the object needs to be placed somewhere, the method AddFeaturegets the position parameter.

We will start with the implementation-procurement, which so far will not do anything.

 using UnityEngine; public class HexFeatureManager : MonoBehaviour { public void Clear () {} public void Apply () {} public void AddFeature (Vector3 position) {} } 

Now we can add a link to such a component in HexGridChunk. Then you can include it in the process of triangulation, as well as all child elements HexMesh.

  public HexFeatureManager features; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); estuaries.Clear(); features.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); estuaries.Apply(); features.Apply(); } 

Let's start by placing one object in the center of each cell.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } features.AddFeature(cell.Position); } 

Now we need a real object manager. Add another child to the Hex Grid Chunk prefab and give it a component HexFeatureManager. Then you can connect a fragment with it.




Object manager added to fragment prefab.

Prefab objects


What terrain object will we create? For the first test, the cube is quite suitable. Create a sufficiently large cube, for example, with a scale (3, 3, 3), and turn it into a prefab. Also create a material for it. I used the default material with red color. Remove it from the collider, because we do not need it.


Prefab Cube.

Object managers will need a link to this prefab, so we will add it to HexFeatureManager, and then connect them. Since the placement of the object requires access to the transform component, we use it as a reference type.

  public Transform featurePrefab; 


Object manager with prefab.

Creating instances of objects


The structure is ready, and we can begin to add objects of relief! Simply create an instance of the prefab in HexFeatureManager.AddFeatureand set its position.

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); instance.localPosition = position; } 


Instances of relief objects.

From this point on, the relief will be filled with cubes. At the very least, the upper halves of the cubes, because the local origin point for the cube mesh in Unity is in the center of the cube, and the lower part is below the surface of the relief. To place the cubes on topography, we need to move them up half their height.

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = position; } 


Cubes on the surface of the relief.

What if we use another mesh?
. , , . .

Of course, our cells are distorted, so we need to distort the positions of objects. So we get rid of the perfect repeatability of the grid.

  instance.localPosition = HexMetrics.Perturb(position); 


Distorted positions of objects.

Destruction of relief objects


With each update of the fragment, we create new relief objects. This means that while we are creating more and more objects in the same positions. To avoid duplicates, we need to get rid of old objects when cleaning up a fragment.

The fastest way to do this is by creating a game container object and turning all the relief objects into its children. Then, when Clearwe call, we will destroy this container and create a new one. The container itself will be a child of its manager.

  Transform container; public void Clear () { if (container) { Destroy(container.gameObject); } container = new GameObject("Features Container").transform; container.SetParent(transform, false); } … public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); } 

Probably inefficient every time to create and destroy objects of relief.
, , . . . , , , . HexFeatureManager.Apply . . , , .

unitypackage

Placement of objects of relief


For now, we place objects in the center of each cell. For empty cells, it looks normal, but on cells containing rivers and roads, as well as flooded with water, it seems strange.


Objects are located everywhere.

Therefore, before placing an object, let's check in HexGridChunk.Triangulatewhether the cell is empty.

  if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell.Position); } 


Limited accommodation.

One object per direction


Only one object per cell is not too much. There is plenty of room for heaps of objects. Therefore, we add an additional object to the center of each of the six triangles of the cell, that is, one per direction.

We will do this in another method Triangulatewhen we know that there is no river in the cell. We still need to check whether we are under water and whether there is a road in the cell. But in this case, we are only interested in the roads going in the current direction.

  void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } } … } 


Many facilities, but not in the vicinity of the rivers.

This creates many more objects! They appear alongside the roads, but they still avoid the rivers. To place objects along the rivers, we can also add them inside TriangulateAdjacentToRiver. But again only when the triangle is not under water and there is no road on it.

  void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } } 


Objects appeared alongside the rivers.

Is it possible to render so many objects?
, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.

unitypackage

Variety of objects


All our relief objects have the same orientation, which looks completely unnatural. Let's give each of them a random turn.

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * Random.value, 0f); instance.SetParent(container, false); } 


Random turns.

So the result becomes much more diverse. Unfortunately, with each update of the fragment, the objects receive a new random rotation. Editing cells should not change the objects in the neighborhood, so we need a different approach.

We have a noise texture that is always the same. However, this texture contains Pearl's gradient noise, and it is locally matched. This is what we need when we distort the positions of the vertices in the cells. But the turns do not have to be coordinated. All turns should be equally probable and mixed. Therefore, we need a texture with non-gradient random values ​​that can be sampled without bilinear filtering. In essence, this is a hash grid that creates the basis of gradient noise.

Creating a hash table


We can create a hash table from an array of float values ​​and fill it once with random values. Because of this, we do not need a texture at all. Let's add it to HexMetrics. A size of 256 by 256 is enough for sufficient variability.

  public const int hashGridSize = 256; static float[] hashGrid; public static void InitializeHashGrid () { hashGrid = new float[hashGridSize * hashGridSize]; for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } } 

Random values ​​are generated by a mathematical formula that always gives the same results. The resulting sequence depends on the number of seed, which by default is equal to the current time value. That is why in each game session we will receive different results.

To ensure the reconstruction of always identical objects, we need to add the seed parameter to the initialization method.

  public static void InitializeHashGrid (int seed) { hashGrid = new float[hashGridSize * hashGridSize]; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } } 

Now that we have initialized the random number stream, we will always get the same sequence from it. Therefore, seemingly random events occurring after the map generation will also always be the same. We can avoid this by maintaining the state of the random number generator before it is initialized. After completing the work, we can ask him the old state.

  Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } Random.state = currentState; 

The hashes table is initialized HexGridat the same time that it assigns a noise texture. That is, in the methods HexGrid.Startand HexGrid.Awake. Let's do it so that values ​​are generated no more often than necessary.

  public int seed; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); … } void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); } } 

The common seed variable allows us to choose the seed value for the card. Any value will do. I chose 1234.


Selection of seed.

Using the hash table


To use the hash table, add to the HexMetricssampling method. Like it SampleNoise, it uses the xz position to get the value. The hash index is found by limiting the coordinates to integer values, and then obtaining the remainder of the integer division by the size of the table.

  public static float SampleHashGrid (Vector3 position) { int x = (int)position.x % hashGridSize; int z = (int)position.z % hashGridSize; return hashGrid[x + z * hashGridSize]; } 

What is he doing %?
, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.

This works for positive coordinates, but not for negative ones, because for such numbers the remainder will be negative. We can fix this by adding a table size to negative results.

  int x = (int)position.x % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)position.z % hashGridSize; if (z < 0) { z += hashGridSize; } 

Now for each square unit we create our own value. However, in reality, we do not need such a density table. Objects are separated from each other further. We can stretch the table by reducing the scale of the position before calculating the index. One unique value for a 4 by 4 square will be enough for us.

  public const float hashGridScale = 0.25f; public static float SampleHashGrid (Vector3 position) { int x = (int)(position.x * hashGridScale) % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)(position.z * hashGridScale) % hashGridSize; if (z < 0) { z += hashGridSize; } return hashGrid[x + z * hashGridSize]; } 

Let's go back to HexFeatureManager.AddFeatureand use our new hash table to get the value. After we apply it to set the rotation, the objects while editing the relief will remain motionless.

  public void AddFeature (Vector3 position) { float hash = HexMetrics.SampleHashGrid(position); Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash, 0f); instance.SetParent(container, false); } 

Accommodation threshold


Although the objects have a different turn, the pattern is still noticeable in their placement. There are seven objects in each cell. We can add chaos to this scheme by arbitrarily omitting some of the objects. How do we decide whether to add an object or not? Of course, testing another random value!

That is, now instead of one hash value we need two. Their support can be added by using a hash table instead of a floatvariable as the array type Vector2. But vector operations do not make sense for hash values, so let's create a special structure for this purpose. She will need only two float values. And let's add a static method to create a pair of randomized values.

 using UnityEngine; public struct HexHash { public float a, b; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; return hash; } } 

Shouldn't it be serialized?
, , Unity. , .

Let's change it HexMetricsso that it uses the new structure.

  static HexHash[] hashGrid; public static void InitializeHashGrid (int seed) { hashGrid = new HexHash[hashGridSize * hashGridSize]; Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = HexHash.Create(); } Random.state = currentState; } public static HexHash SampleHashGrid (Vector3 position) { … } 

Now HexFeatureManager.AddFeaturehas access to two hash values. Let's use the first to decide whether to add an object, or skip it. If the value is equal to or greater than 0.5, then we skip it. In this case, we get rid of about half of the objects. The second value will normally be used to determine the turn.

  public void AddFeature (Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= 0.5f) { return; } Transform instance = Instantiate(featurePrefab); 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); } 


The density of objects is reduced by 50%.

unitypackage

Drawing objects


Instead of placing objects everywhere, let's make them editable. But we will not draw separate objects, but add the level of objects to each cell. This level will control the likelihood of objects in the cell. By default, the value is zero, that is, the objects will be absent.

Since red cubes do not look like natural objects on our relief, let's call them buildings. They will represent urbanization. Let's add to the HexCelllevel of urbanization.

  public int UrbanLevel { get { return urbanLevel; } set { if (urbanLevel != value) { urbanLevel = value; RefreshSelfOnly(); } } } int urbanLevel; 

We can make it so that the level of urbanization for the underwater cell is zero, but this is not necessary, we already skip the creation of underwater objects. And perhaps at some stage we will add water objects of urbanization, such as docks and underwater structures.

Density slider


To change the level of urbanization, we will add HexMapEditorone more slider in support.

  int activeUrbanLevel; … bool applyUrbanLevel; … public void SetApplyUrbanLevel (bool toggle) { applyUrbanLevel = toggle; } public void SetUrbanLevel (float level) { activeUrbanLevel = (int)level; } void EditCell (HexCell cell) { if (cell) { … if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } … } } 

Add another slider to the UI and connect it with the appropriate methods. I will place a new panel on the right side of the screen to avoid overflowing the left panel.

How many levels will we need? Let's look at four, denoting zero, low, medium and high density.



Urbanization slider.

Threshold change


Now that we have a level of urbanization, we need to use it to determine whether to place objects. To do this, we need to add the level of urbanization as an additional parameter in HexFeatureManager.AddFeature. Let's take one more step and just pass the cell itself. In the future we will be more comfortable.

The fastest way to use the level of urbanization is by multiplying it by 0.25 and using the value as a new threshold for skipping objects. Due to this, the probability of an object will increase with each level by 25%.

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= cell.UrbanLevel * 0.25f) { return; } … } 

To make it work, pass the cells to HexGridChunk.

  void Triangulate (HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } } void Triangulate (HexDirection direction, HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } … } … void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } } 


Drawing density levels of urbanization.

unitypackage

Several prefabs of relief objects


The differences in the probability of the appearance of objects is not enough to create a clear separation between low and high levels of urbanization. In some cells, there will simply be more or less than the expected number of buildings. We can make the difference clearer by using our own prefab for each level.

We will get rid of the field featurePrefabin HexFeatureManagerand replace it with an array for the prefabs of urbanization. To obtain the corresponding prefab, we will subtract one from the level of urbanization and use the value as an index.

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

We will create two duplicates of the prefab of the object, rename and change them so that they denote three different levels of urbanization. Level 1 is low density, so we use a cube with a unit edge length, which denotes a shack. I will scale the prefab level 2 to (1.5, 2, 1.5) to make it look like a two-story building. For tall buildings of level 3, I used the scale (2, 5, 2).



Using different prefabs for each level of urbanization.

Mixing prefabs


We are not obliged to limit ourselves to strict separation of building types. You can mix them up a bit, as it happens in the real world. Instead of one threshold per level, let's use three, one for each type of building.

At level 1, we use shackling in 40% of cases. There will be no other buildings here at all. For the level we use three values ​​(0.4, 0, 0).

At level 2, we replace the shacks with more buildings, and add a 20% probability for additional shacks. High buildings will not do. That is, we use the threshold three values ​​(0.2, 0.4, 0).

At level 3, we replace medium buildings with high ones, replace shacks again, and add another probability of 20% shacks. The threshold values ​​will be equal to (0.2, 0.2, 0.4).

That is, the idea is that as the level of urbanization increases, we will upgrade existing buildings and add new ones to empty places. To delete an existing building, we need to use the same hash value intervals. If the hashes between 0 and 0.4 at level 1 were shacks, then at level 3 the same spacing would create tall buildings. At level 3, tall buildings should be created with hash values ​​in the range of 0–0.4, two-story buildings in the range of 0.4–0.6, and shacks in the range of 0.6–0.8. If you check them from highest to lowest, then this can be done with the help of a triple of thresholds (0.4, 0.6, 0.8). The thresholds of level 2 will then become (0, 0.4, 0.6), and the thresholds of level 1 will become (0, 0, 0.4).

Let's save these thresholds inHexMetricsas a collection of arrays with a method that allows you to get thresholds for a certain level. Since we are only interested in levels with objects, 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, add to the HexFeatureManagermethod that uses the level and the value of the hash to select the prefab. If the level is greater than zero, then we get the thresholds using the level reduced by one. Then we cycle through the thresholds until one of them exceeds the value of the hash. This will mean that we have found the prefab. If not found, then 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 changing the order of references to prefabs so that they go from high to low density.


Inverted prefab order.

We use our new method in AddFeatureto select the prefab. If we do not receive it, then skip the object. Otherwise, create an instance of 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); } 


Mix prefabs.

Variations at the level


Now we have well-mixed buildings, but so far there are only three of them. We can further increase the variability by tying a collection of prefabs to each level of density of urbanization. After that you can choose one of them randomly. This will require a new random value, so add the third in HexHash.

  public float a, b, c; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; hash.c = Random.value; return hash; } 

Let's turn HexFeatureManager.urbanPrefabsinto an array of arrays, and add a PickPrefabparameter to the method choice. Use it to select the index of the built-in array, multiplying it by the length of this array and converting it into an integer.

  public Transform[][] urbanPrefabs; … Transform PickPrefab (int level, float hash, float choice) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i][(int)(choice * urbanPrefabs[i].Length)]; } } } return null; } 

Let's justify our choice on the value of the second hash (B). It will then take a turn from B to C.

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a, hash.b); 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.c, 0f); instance.SetParent(container, false); } 

Before proceeding, we need to consider what the Random.valuevalue of 1 can produce. Because of this, the array index can go out of bounds. To prevent this from happening, we slightly change the scale of the hash values. We simply scale them all so as not to worry about the particular we use.

  public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; return hash; } 

Unfortunately, the arrays are not displayed in the inspector. Therefore, we can not configure them. To circumvent this limitation, create a serializable structure in which we encapsulate the embedded array. Give it a method that converts from choice to an array index and returns the prefab.

 using UnityEngine; [System.Serializable] public struct HexFeatureCollection { public Transform[] prefabs; public Transform Pick (float choice) { return prefabs[(int)(choice * prefabs.Length)]; } } 

We use in HexFeatureManagerinstead of the built-in arrays an array of such collections.

 // public Transform[][] urbanPrefabs; public HexFeatureCollection[] urbanCollections; … Transform PickPrefab (int level, float hash, float choice) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanCollections[i].Pick(choice); } } } return null; } 

Now we can assign several buildings to each density level. Since they are independent, we do not need to use the same amount per level. I just used two options per level, adding a longer lower version to each one. I chose for them the scales (3.5, 3, 2), (2.75, 1.5, 1.5) and (1.75, 1, 1).



Two types of buildings per level of density.

unitypackage

Several types of objects


In the existing scheme, we can create quite decent urban structures. But the relief can contain not only buildings. How about farms or plants? Let's add to the HexCelllevels and for them. They are not mutually exclusive and can be mixed.

  public int FarmLevel { get { return farmLevel; } set { if (farmLevel != value) { farmLevel = value; RefreshSelfOnly(); } } } public int PlantLevel { get { return plantLevel; } set { if (plantLevel != value) { plantLevel = value; RefreshSelfOnly(); } } } int urbanLevel, farmLevel, plantLevel; 

Of course, this requires support in HexMapEditortwo additional sliders.

  int activeUrbanLevel, activeFarmLevel, activePlantLevel; bool applyUrbanLevel, applyFarmLevel, applyPlantLevel; … public void SetApplyFarmLevel (bool toggle) { applyFarmLevel = toggle; } public void SetFarmLevel (float level) { activeFarmLevel = (int)level; } public void SetApplyPlantLevel (bool toggle) { applyPlantLevel = toggle; } public void SetPlantLevel (float level) { activePlantLevel = (int)level; } … void EditCell (HexCell cell) { if (cell) { … if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (applyFarmLevel) { cell.FarmLevel = activeFarmLevel; } if (applyPlantLevel) { cell.PlantLevel = activePlantLevel; } … } } 

Add them to the UI.


Three sliders.

Also additional collections will be needed HexFeatureManager.

  public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections; 


Three collections of objects of relief.

I created both prefabs for density and for farms and plants, as well as for collections of buildings. For all of them, I used cubes. Farms have a light green material, plants - dark green.

I made cubes of truss with a height of 0.1 units to designate square allotments of agricultural land. I chose (2.5, 0.1, 2.5) and (3.5, 0.1, 2) as high density scales. On average, the sites have an area of ​​1.75 and a size of 2.5 by 1.25. The low level of density received an area of ​​1 and a size of 1.5 by 0.75.

Plant prefabs denote tall trees and large shrubs. High density prefabs are the largest, (1.25, 4.5, 1.25) and (1.5, 3, 1.5). The average scales are (0.75, 3, 0.75) and (1, 1.5, 1). The smallest plants have dimensions (0.5, 1.5, 0.5) and (0.75, 1, 0.75).

Selection of objects of relief


Each type of object must receive its own hash value so that they have different creation patterns and can be mixed. Add in HexHashtwo additional values.

  public float a, b, c, d, e; public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; hash.d = Random.value * 0.999f; hash.e = Random.value * 0.999f; return hash; } 

Now you have HexFeatureManager.PickPrefabto work with different collections. Add a parameter to simplify the process. Also change the hash used by the option of the selected prefab to D, and the hash to rotate to E.

  Transform PickPrefab ( HexFeatureCollection[] collection, int level, float hash, float choice ) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return collection[i].Pick(choice); } } } return null; } public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); … instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); } 

Currently AddFeaturechooses prefab urbanization. This is normal, we need more options. Therefore, add another prefab from the farm. We use B. as the hash value. The choice of the option will again be D.

  Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (!prefab) { return; } 

What copy of the prefab will we create as a result? If one of them turns out to be null, then the choice is obvious. However, if both exist, then we need to make a decision. Let's just add the prefab with the lowest hash value.

  Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; } 


Mixing urban and rural sites.
Next, do the same with the plants, using the value of the hash C.

  if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; } 

However, we cannot just copy the code. When we choose a rural object instead of an urban object, we need to compare the plant hash with the farm hash, not with the urban one. Therefore, we need to track the hash that we decided to select and compare with it.

  float usedHash = hash.a; if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; usedHash = hash.b; } } else if (otherPrefab) { prefab = otherPrefab; usedHash = hash.b; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < usedHash) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; } 


Mixing of urban, rural and vegetable objects.

unitypackage

Part 10: walls



In this section we will add walls between the cells.


There is nothing more welcoming than the high wall.

Editing the walls


To support the walls, we need to know where to place them. We will place them between cells, along the edges connecting them. Since the existing objects are located in the central part of the cells, we do not need to worry that the walls will pass through them.


The walls are located along the edges.

The walls are objects of relief, although they are large. Like other objects, we will not edit them directly. Instead, we will change the cells. We will not have separate segments of the walls, but we will be engaged in enclosing the cells as a whole.

Walled property


To support fenced cells, add to the HexCellproperty Walled. This is a simple switch. Since the walls are located between the cells, we need to update both the edited cells and their neighbors.

  public bool Walled { get { return walled; } set { if (walled != value) { walled = value; Refresh(); } } } bool walled; 

Editor switch


To switch the state of “fencing” of cells, we need to add HexMapEditorsupport to the switch. Therefore, we will add another field OptionalToggleand method for setting it.

  OptionalToggle riverMode, roadMode, walledMode; … public void SetWalledMode (int mode) { walledMode = (OptionalToggle)mode; } 

Unlike rivers and roads, the walls do not go from cell to cell, but lie between them. Therefore, we do not need to think about dragging. When the wall switch is active, we simply set the state of “fencing” of the current cell based on the state of this switch.

  void EditCell (HexCell cell) { if (cell) { … if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (walledMode != OptionalToggle.Ignore) { cell.Walled = walledMode == OptionalToggle.Yes; } if (isDrag) { … } } } 

We duplicate one of the previous elements of the UI switches and change them so that they control the state of "fencing". I will put them in the UI panel along with other objects.


Switch "fence".

unitypackage

Making walls


Since the walls follow the contours of the cells, they should not have a permanent shape. Therefore, we cannot simply use the prefab for them, as was done with other objects of relief. Instead, we need to build a mesh, as we did with the relief. This means that our fragment prefab needs another child HexMesh. Duplicate one of the remaining child meshes and make the new Walls objects cast shadows. They do not need anything but vertices and triangles, so all options HexMeshneed to be disabled.



Child Prefab Walls.

It will be logical that the walls are an urban object, so for them I used the red building material.

Wall Management


Since the walls are objects of relief, they must deal with HexFeatureManager. Therefore, we give the manager of the relief objects a reference to the Walls object , and make it so that it calls the Clearand methods Apply.

  public HexMesh walls; … public void Clear () { … walls.Clear(); } public void Apply () { walls.Apply(); } 


Walls connected to the manager of the relief objects.

Shouldn't Walls be a child of Features?
, . , Walls Hex Grid Chunk .

Now we need to add a method to the manager that allows to add walls to it. Since the walls are along the edges between the cells, he needs to know the corresponding edges of the edges and cells. HexGridChunkwill call it through TriangulateConnection, at the time of the triangulation of the cell and one of its neighbors. From this point of view, the current cell is located on the near side of the wall, and the other is on the far side.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { } 

Call this new method in HexGridChunk.TriangulateConnectionafter completing all the other connective work and immediately before moving on to the corner triangle. We will provide the manager of the relief objects to decide for themselves where the wall should actually be located.

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { … } else { … } features.AddWall(e1, cell, e2, neighbor); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { … } } 

Build a wall segment


The entire wall will wriggle through several cell edges. Each edge contains only one wall element. From the point of view of the near cell, the segment begins on the left side of the edge and ends on the right side. Let's add to a HexFeatureManagerseparate method using four vertices in the corners of the edges.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { } 


Near and far side.

AddWallmay call this method with the first and last vertices of the edges. But walls should be added only when we have a connection between a fenced cell and a non-enclosed cell. It does not matter which of the cells is inside and which is outside; only the difference in their states is taken into account.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v5, far.v5); } } 

The simplest segment of the wall is one quad, standing in the middle of the edge. We will find its lower peaks, interpolating to the middle of the nearest to the far peaks.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); } 

How tall should the wall be? Let's set its height in HexMetrics. I made them the size of one level of height of cells.

  public const float wallHeight = 3f; 

HexFeatureManager.AddWallSegmentcan use this height to position the third and fourth vertex of the quad, and also add it to the mesh walls.

  Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 v1, v2, v3, v4; v1 = v3 = left; v2 = v4 = right; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); 

Now we can edit the walls and they will be displayed as quad bands. However, we will not see a continuous wall. Each quad is visible only from one side. Its face is directed towards the cell from which it was added.


One-sided quad walls.

We can quickly solve this problem by adding a second quad, pointing in the other direction.

  walls.AddQuad(v1, v2, v3, v4); walls.AddQuad(v2, v1, v4, v3); 


Double-sided walls.

Now all the walls are visible entirely, but in the corners of the cells, where there are three cells, all the same until there are holes. We will fill them in later.

Thick walls


Although the walls are already visible on both sides, they have no thickness. In fact, the walls are thin, like paper, and almost invisible at a certain angle. So let's make them solid by adding thickness. Let us set their thickness in HexMetrics. I chose the value of 0.75 units, it seemed to me suitable.

  public const float wallThickness = 0.75f; 

To make two walls thick, you need to separate the two quad-side. They must move in opposite directions. One side should move to the near edge, the other - to the far. The offset vector for this is far - near, but to keep the upper part of the wall flat, we need to assign its Y component a value of 0.

Since this is the case for both the left and the right side of the wall segment, let's add to the HexMetricsmethod to calculate this offset vector.

  public static Vector3 WallThicknessOffset (Vector3 near, Vector3 far) { Vector3 offset; offset.x = far.x - near.x; offset.y = 0f; offset.z = far.z - near.z; return offset; } 

In order for the wall to remain in the center of the edge, the present distance of movement along this vector must be equal to half the thickness for each side. And to make sure that we really moved to the desired distance, we normalize the displacement vector before scaling it.

  return offset.normalized * (wallThickness * 0.5f); 

We use this method in HexFeatureManager.AddWallSegmentto change the position of the quad. Since the displacement vector goes from the nearest to the far cell, we subtract it from the near quad and add it to the far one.

  Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 leftThicknessOffset = HexMetrics.WallThicknessOffset(nearLeft, farLeft); Vector3 rightThicknessOffset = HexMetrics.WallThicknessOffset(nearRight, farRight); Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3); 


Walls with offsets.

Quads are now shifted, although this is not entirely noticeable.

Is the wall thickness the same?
, «-» . , . . , . , . , - , . .

Wall tops


To make the wall thickness visible from above, we need to add a quad to the top of the wall. The easiest way to do this is by memorizing the top two vertices of the first quad and connecting them to the top two vertices of the second quad.

  Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3); walls.AddQuad(t1, t2, v3, v4); 


Walls with tops.

Cornering


We still have holes in the corners of the cells. To fill them, we need to add a segment to the triangular area between the cells. Each corner connects three cells. Each cell may or may not have a wall. That is, eight configurations are possible.


Angle configurations.

We place walls only between cells with different “fenced” conditions. This reduces the number of configurations to six. In each of them, one of the cells is inside the curved walls. Let's take this cell as the pivot point around which the wall is bent. From the point of view of this cell, the wall begins with an edge common to the left cell and ends with an edge common to the right cell.


Roles of cells.

That is, we need to create a method AddWallSegmentwhose parameters are the three vertices of the angle. Although we can write code to triangulate this segment, in fact it is a special case of the method AddWallSegment. The pivot point plays the role of both near vertices.

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddWallSegment(pivot, left, pivot, right); } 

Next, create a variant of the method AddWallfor the three vertices of the angle and their cells. The task of this method is to determine the angle, which is the reference point, if it exists. Therefore, he must consider all eight possible configurations and call AddWallSegmentfor six of them.

  public void AddWall ( Vector3 c1, HexCell cell1, Vector3 c2, HexCell cell2, Vector3 c3, HexCell cell3 ) { if (cell1.Walled) { if (cell2.Walled) { if (!cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } } else if (cell3.Walled) { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } else { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } } else if (cell2.Walled) { if (cell3.Walled) { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } else { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } } else if (cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } } 

To add corner segments, call this method at the end HexGridChunk.TriangulateCorner.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } 


The walls are with corners, but there are still holes.

Close the hole


There are still holes in the walls, because the height of the wall segments is not constant. While the segments along the edges have a constant height, the corner segments are between two different edges. Since each edge can have its own height, holes appear at the corners.

To fix this, change AddWallSegmentit so that it stores separately the Y coordinates of the left and right upper vertices.

  float leftTop = left.y + HexMetrics.wallHeight; float rightTop = right.y + HexMetrics.wallHeight; Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v2, v1, v4, v3); 


Enclosed walls.

The walls are now closed, but you probably still see holes in the shadows of the wall. This is caused by the Normal Bias setting of the directional shadows. When it is greater than zero, the triangles of the shadow-casting objects move along the surface normal. This avoids self-shadowing, but at the same time creates holes in cases where the triangles look in different directions. At the same time, holes can be created in the shadows of thin geometry, such as our walls.

You can get rid of these shadow artifacts by reducing normal bias to zero. Or change the Cast Shadows mesh renderer wall mode to Two Sided . This causes the shadow object to render both sides of each wall triangle for rendering, which closes all the holes.


There are no more shadow holes.

unitypackage

Walls on ledges


As long as our walls are fairly straight. For flat terrain, this is not bad at all, but it looks strange when the walls coincide with the ledges. This happens when there is a difference of one height between the cells on opposite sides of the wall.


Straight walls on ledges.

Follow edge


Instead of creating one segment for the whole edge, we will create one for each part of the edge strip. We can do this by calling four times AddWallSegmentin the AddWallrib version .

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); AddWallSegment(near.v2, far.v2, near.v3, far.v3); AddWallSegment(near.v3, far.v3, near.v4, far.v4); AddWallSegment(near.v4, far.v4, near.v5, far.v5); } } 


Curving walls.

The walls now replicate the shapes of distorted edges. In combination with the ledges it looks much better. In addition, it creates more interesting walls on flat terrain.

Placing walls on the ground


Looking at the walls on the ledges, you can find a problem. The walls are hanging above the ground! This is true for inclined flat edges, but usually not so noticeable.


Walls hanging in the air.

To solve the problem, we need to lower the walls. The easiest way is to lower the entire wall so that its top remains flat. At the same time, part of the wall on the upper side will slightly sink into the relief, but this will suit us.

To lower the wall, we need to determine which of the sides is lower - near or far. We can simply use the height of the lowest side, but we do not need to go down so low. You can interpol the Y coordinate from low to high with an offset of just under 0.5. Since the walls only occasionally become higher than the lower step of the step, we can use the vertical step of the step as an offset. Different wall thickness of the ledge configuration may require a different offset.


Lowered wall

Let's add to the HexMetricsmethod WallLerpthat will do this interpolation, in addition to averaging the X and Z coordinates of near and far vertices. It is based on method TerraceLerp.

  public const float wallElevationOffset = verticalTerraceStepSize; … public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v; return near; } 

Let's make HexFeatureManageruse of this method to define left and right vertices.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft); Vector3 right = HexMetrics.WallLerp(nearRight, farRight); … } 


Walls standing on the ground.

Change wall distortion


Now our walls fit well with the differences in heights. But they still do not fully correspond to the distorted edges, although they are close to them. It happened because we first define the tops of the walls, and then distort them. Since these vertices are somewhere between the vertices of the near and far edges, their distortion will be slightly different.

The fact that the walls inaccurately follow the ribs is not a problem. However, the distortion of the peaks of the wall otherwise changes the relatively uniform thickness. If we place walls on the basis of distorted vertices, and then add undistorted quad, their thickness should not vary much.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { nearLeft = HexMetrics.Perturb(nearLeft); farLeft = HexMetrics.Perturb(farLeft); nearRight = HexMetrics.Perturb(nearRight); farRight = HexMetrics.Perturb(farRight); … walls.AddQuadUnperturbed(v1, v2, v3, v4); … walls.AddQuadUnperturbed(v2, v1, v4, v3); walls.AddQuadUnperturbed(t1, t2, v3, v4); } 


Uncorrupted vertices of the walls.

With this approach, the walls will no longer follow the ribs as accurately as they used to. But in return, they will become less broken and will have a more constant thickness.


More constant wall thickness.

unitypackage

Holes in the walls


For the time being we have ignored the possibility that a river or a road can cross a wall. When this happens, we must make a hole in the wall through which a river or a road can pass.

To do this, add AddWalltwo Boolean parameters indicating whether a river or a road passes through the edge. Although we can handle them differently, let's in both cases simply delete the two middle segments.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) { // Leave a gap. } else { AddWallSegment(near.v2, far.v2, near.v3, far.v3); AddWallSegment(near.v3, far.v3, near.v4, far.v4); } AddWallSegment(near.v4, far.v4, near.v5, far.v5); } } 

Now HexGridChunk.TriangulateConnectionmust provide the necessary data. Since he already needed the same information, let's cache it in Boolean variables and write the calls to the corresponding methods only once.

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … bool hasRiver = cell.HasRiverThroughEdge(direction); bool hasRoad = cell.HasRoadThroughEdge(direction); if (hasRiver) { … } if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, cell.Color, e2, neighbor.Color, hasRoad); } features.AddWall(e1, cell, e2, neighbor, hasRiver, hasRoad); … } 


Holes in the walls for the passage of rivers and roads.

We cover the walls


These new openings make the walls complete. We need to close these end points with quad so that it is impossible to look through the sides of the walls. Create for this purpose in the HexFeatureManagermethod AddWallCap. It works like it AddWallSegment, but it only needs one pair of near-far peaks. Make it add a quad going from the near to the far side of the wall.

  void AddWallCap (Vector3 near, Vector3 far) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = center.y + HexMetrics.wallHeight; walls.AddQuadUnperturbed(v1, v2, v3, v4); } 

When it AddWallfinds that we need a hole, we add a lid between the second and fourth pairs of vertex edges. For the fourth pair of vertices, you need to switch the orientation, otherwise the quad face will look inward.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) { AddWallCap(near.v2, far.v2); AddWallCap(far.v4, near.v4); } … } } 


Closed holes in the walls.

What about the holes along the edges of the map?
, . . , .

unitypackage

Avoid cliffs and water


Finally, let's look at the edges that contain cliffs or water. Since the cliffs are essentially large walls, it would be illogical to place an additional wall on them. In addition, it will look bad. Underwater walls are also completely illogical, as is the restriction to the walls of the coast.


Walls on the cliffs and in the water.

We can remove walls from these unnecessary edges with additional checks in AddWall. The wall can not be under water, and the common edge with it can not be a precipice.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if ( nearCell.Walled != farCell.Walled && !nearCell.IsUnderwater && !farCell.IsUnderwater && nearCell.GetEdgeType(farCell) != HexEdgeType.Cliff ) { … } } 


Removed disturbing walls along the edges, but the corners remained in place.

Removing the corners of the walls


It takes a little more effort to remove unnecessary corner segments. The simplest case is when the supporting cell is under water. This ensures that there are no wall segments near which to connect.

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { if (pivotCell.IsUnderwater) { return; } AddWallSegment(pivot, left, pivot, right); } 


Submarine support cells no longer exist.

Now we need to look at the other two cells. If one of them is under water or connected to the supporting cell by a cliff, then there is no wall along this rib. If that is true at least for one side, then there should not be a wall segment in this corner.

Separately, we determine whether there is a left or right wall. Put the results in boolean variables to make it easier to work with them.

  if (pivotCell.IsUnderwater) { return; } bool hasLeftWall = !leftCell.IsUnderwater && pivotCell.GetEdgeType(leftCell) != HexEdgeType.Cliff; bool hasRighWall = !rightCell.IsUnderwater && pivotCell.GetEdgeType(rightCell) != HexEdgeType.Cliff; if (hasLeftWall && hasRighWall) { AddWallSegment(pivot, left, pivot, right); } 


Removed all interfering corners.

Close the corners


When there is no wall either on the left or on the right edge, then the work is finished. But if the wall is only in one direction, then this means that there is another hole in the wall. Therefore, it is necessary to close it.

  if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { AddWallCap(right, pivot); } 


Close the walls.

The connection of the walls with cliffs


In one situation, the walls look imperfect. When the wall reaches the bottom of the cliff, it ends. But since the cliffs are not completely vertical, this creates a narrow hole between the wall and the edge of the cliff. At the top of the cliff, this problem does not occur.


Holes between the walls and the edges of the cliffs.

It would be much better if the wall continued to the very edge of the cliff. We can do this by adding another wall segment between the current end of the wall and the corner top of the cliff. Since most of this segment will be hidden inside the cliff, we can manage to reduce to zero the thickness of the wall inside the cliff. Thus, it is enough for us to create a wedge: two quad-a going to a point and a triangle on top of them. Let's create a method for this purpose AddWallWedge. This can be done by copying AddWallCapand adding a wedge point.

  void AddWallWedge (Vector3 near, Vector3 far, Vector3 point) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); point = HexMetrics.Perturb(point); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; Vector3 pointTop = point; point.y = center.y; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = pointTop.y = center.y + HexMetrics.wallHeight; // walls.AddQuadUnperturbed(v1, v2, v3, v4); walls.AddQuadUnperturbed(v1, point, v3, pointTop); walls.AddQuadUnperturbed(point, v2, pointTop, v4); walls.AddTriangleUnperturbed(pointTop, v3, v4); } 

In AddWallSegmentfor the corners we will call this method when the wall goes only in one direction and this wall is at a lower height than the other side. It is under these conditions that we encounter the edge of a cliff.

  if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else if (leftCell.Elevation < rightCell.Elevation) { AddWallWedge(pivot, left, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { if (rightCell.Elevation < leftCell.Elevation) { AddWallWedge(right, pivot, left); } else { AddWallCap(right, pivot); } } 


, .

unitypackage

11:




.


In the previous section, we added support for the walls. These are simple straight wall segments with no visible differences. Now we will make the walls more interesting by adding towers to them.

In order to conform to the relief, wall segments should be created procedurally. For towers this is not required, we can use the usual prefab.

We can create a simple tower of two cubes with red material. The base of the tower has a size of 2 to 2 units and a height of 4 units, that is, it is thicker and taller than the wall. Above this cube we will place a cube of unit size denoting the top of the tower. Like all other prefabs, these cubes do not require colliders.

Since the tower model consists of several objects, we will make them children of the root object. Place them so that the local point of the origin of the root is at the base of the tower. Thanks to this, we will be able to place the towers without worrying about their height.


Prefab Tower.

Add a link to this prefab in HexFeatureManagerand connect it.

  public Transform wallTower; 


Link to the tower prefab.

Build towers


Let's start by placing the towers in the middle of each wall segment. For this we will create a tower at the end of the method AddWallSegment. Her position will be the middle of the left and right points of the segment.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { … Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; towerInstance.SetParent(container, false); } 


One tower for each wall segment.

We got a lot of towers along the wall, but their orientation does not change. We need to change their rotation so that they align with the wall. Since we have right and left points of the wall, we know which direction is right. We can use this knowledge to determine the orientation of the wall segment, and therefore the tower.

Instead of calculating the rotation on our own, we simply assign the property to a Transform.rightvector. The Unity code will change the rotation of the object so that its local direction right corresponds to the transmitted vector.

  Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false); 


The towers are lined with a wall.

How does Transform.right assignment work?
Quaternion.FromToRotation . .

 public Vector3 right { get { return rotation * Vector3.right; } set { rotation = Quaternion.FromToRotation(Vector3.right, value); } } 

Reduce the number of towers


One tower for each wall segment is too much. Let's make adding a tower optional by adding a AddWallSegmentparameter to the boolean. Give it a default value false. In this case, all the towers will disappear.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight, bool addTower = false ) { … if (addTower) { Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false); } } 

Let's place the towers only in the corners of the cells. As a result, we get a smaller number of towers with fairly constant distances between them.

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … AddWallSegment(pivot, left, pivot, right, true); … } 


The towers are only in the corners.

It looks good enough, but we may need a less periodic placement of the towers. As in the case of other objects of relief, we can use a table of hashes to decide whether to put a tower in a corner. To do this, we use the center of the angle to sample the table, and then we will compare one of the hash values ​​with the threshold value of the towers.

  HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); bool hasTower = hash.e < HexMetrics.wallTowerThreshold; AddWallSegment(pivot, left, pivot, right, hasTower); 

Threshold value refers to HexMetrics. With a value of 0.5, towers will be created in half the cases, however, we can create walls with many towers or without them at all.

  public const float wallTowerThreshold = 0.5f; 


Random towers.

Remove towers from slopes


Now we place towers, regardless of the shape of the relief. However, on the slopes of the tower look illogical. Here the walls go at an angle and can cut through the top of the tower.


Towers on the slopes.

To avoid slopes, we will check whether the right and left angle cells are at the same height. Only in this case it is possible to place the tower.

  bool hasTower = false; if (leftCell.Elevation == rightCell.Elevation) { HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); hasTower = hash.e < HexMetrics.wallTowerThreshold; } AddWallSegment(pivot, left, pivot, right, hasTower); 


More on the walls of the slopes of the towers there.

We put walls and towers on the ground


Although we avoid the walls on the slopes, the relief on both sides of the wall may still have different heights. Walls can go along ledges, and cells of the same height can have different vertical positions. Because of this, the base of the tower may be in the air.


Towers in the air.

In fact, the walls on the slopes can also hang in the air, but this is not as noticeable as for the towers.


Walls in the air.

This can be corrected by stretching the base of the walls and towers to the ground. To do this, add an Y offset for the walls in HexMetrics. One unit down will be enough. Increase the height of the towers by the same amount.

  public const float wallHeight = 4f; public const float wallYOffset = -1f; 

Let's change it HexMetrics.WallLerpso that when determining the Y coordinate it takes into account the new offset.

  public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v + wallYOffset; return near; } 

We also need to change the prefab of the tower, since the base will now be one unit below the ground. Therefore, we will increase the height of the base cube by one unit and change the local position of the cubes accordingly.



Walls and towers on the ground.

unitypackage

Bridges


At this stage, we have rivers and roads, but roads cannot cross rivers. It’s time to add bridges.

Let's start with a simple scaled cube, which will play the role of the prefab of the bridge. The width of the rivers varies, but there are approximately seven units of distance between the centers of the roads on both sides. Therefore, we give it an approximate scale (3, 1, 7). Add the red city material to the prefab and get rid of its collider. As in the case of towers, we place the cube inside the root object with the same scale. Due to this, the geometry of the bridge itself will not be important.

Add a link to the bridge's prefab HexFeatureManagerand assign it a prefab.

  public Transform wallTower, bridge; 


Assigned prefab of the bridge.

Bridge placement


To accommodate the bridge, we need a method HexFeatureManager.AddBridge. The bridge should be located between the center of the river and one of the sides of the river.

  public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.SetParent(container, false); } 

We will transmit the undistorted road centers, so we will have to distort them before placing the bridge.

  roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); 

For the correct alignment of the bridge, we can use the same approach as when turning the towers. In this case, the road centers define the forward vector of the bridge. Since we remain within the same cell, this vector will be exactly horizontal, so we do not need to reset its Y component.

  Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; instance.SetParent(container, false); 

We lay bridges over straight rivers


The only river configurations that require bridges are straight and curved. Roads can pass through the end points, and for zigzags, roads can only be near.

First, let's deal with straight rivers. Inside, the HexGridChunk.TriangulateRoadAdjacentToRiverfirst operator else ifdeals with the location of roads adjacent to such rivers. Therefore, here we will add bridges.

We are on the same side of the river. The center of the road moves away from the river, and then the center of the cell itself shifts. To find the center of the road on the opposite side, we need to move the opposite direction by the same amount. This must be done before changing the center itself.

  void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … roadCenter += corner * 0.5f; features.AddBridge(roadCenter, center - corner * 0.5f); center += corner * 0.25f; } … } 


Bridges across straight rivers.

Bridges have appeared! But now we have one instance of bridges for each direction through which the river does not flow. We need to make sure that only one instance of the bridge is generated in the cell. This can be done by choosing one direction relative to the river and on its basis generate a bridge. You can choose any direction.

  roadCenter += corner * 0.5f; if (cell.IncomingRiver == direction.Next()) { features.AddBridge(roadCenter, center - corner * 0.5f); } center += corner * 0.25f; 

Moreover, we need to add a bridge only when there is a road on both sides of the river. At the moment we are sure that there is a road on the current side. Therefore, you need to check whether there is a road on the other side of the river.

  if (cell.IncomingRiver == direction.Next() && ( cell.HasRoadThroughEdge(direction.Next2()) || cell.HasRoadThroughEdge(direction.Opposite()) )) { features.AddBridge(roadCenter, center - corner * 0.5f); } 


Bridges between the roads on both sides.

Bridges over curved rivers


Bridges above curved rivers work in a similar way, but their topology is slightly different. We will add a bridge when we are on the outside of the curve. This happens in the last block else. It uses the average direction to shift the center of the road. We will need to use this offset twice with a different scale, so we save it into a variable.

  void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; } … } 

The offset scale on the outside of the curve is 0.25, and inside HexMetrics.innerToOuter * 0.7f. Use it to house the bridge.

  Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); 


Bridges over curved rivers.

Here again, we need to avoid duplicate bridges. We can do this by adding bridges only from the middle direction.

  Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; if (direction == middle) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); } 

And again, you need to make sure that the road is on the opposite side.

  if ( direction == middle && cell.HasRoadThroughEdge(direction.Opposite()) ) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); } 


Bridges between the roads on both sides.

Scaling bridges


Since we distort the terrain, the distance between the centers of the roads and the opposite sides of the river varies. Sometimes bridges are too short, sometimes too long.


Varying distances, but a constant length of bridges.

Although we created a bridge with a length of seven units, it is possible to scale it to match the true distance between the centers of the roads. This means that the model of the bridge is deformed. Since the distances do not vary very much, the deformation may be more acceptable than bridges that are not suitable for the length.

To perform the correct scaling, we need to know the initial length of the bridge prefab. We will store this length in HexMetrics.

  public const float bridgeDesignLength = 7f; 

Now we can assign the scale of the bridge instance to the Z value of the distance between the centers of roads divided by the initial length. Since the bridge prefab root has the same scale, the bridge will stretch correctly.

  public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; float length = Vector3.Distance(roadCenter1, roadCenter2); instance.localScale = new Vector3( 1f, 1f, length * (1f / HexMetrics.bridgeDesignLength) ); instance.SetParent(container, false); } 


Variable length bridges.

Bridge construction


Instead of a simple cube, we can use a more interesting model of the bridge. For example, you can create a coarse arched bridge of three scaled and rotated cubes. Of course, you can create much more complex 3D models, including those with parts of the road. But note that the entire object will be slightly compressed and stretched.



Arched bridges of different lengths.

unitypackage

Special objects


While our cells can contain urban, rural and vegetable objects. Even though each of them has three levels, all objects are rather small compared to the cell size. What if we need a big building, like a castle?

Let's add to the relief a special type of objects. Such objects are so large that they occupy the entire cell. Each of these objects is unique and needs its own prefab. For example, a simple castle can be created from one central cube plus four corner towers. The scale (6, 4, 6) for the central cube will create a sufficiently large lock, which nevertheless will fit even in a strongly deformed cell.


Prefab castle.

Another special object could be a ziggurat, for example, built from three cubes stacked on top of one another. For the lower cube, a scale (8, 2.5, 8) will do.


Prefab ziggurat.

Special objects can be any, not necessarily architectural. For example, a group of massive trees up to ten units in height may denote a cell filled with megaflora.


Prefab megaflora.

Add to HexFeatureManagerarray to track these prefabs.

  public Transform[] special; 

First we add a lock to the array, then a ziggurat, and then a megaflora.


Setting up special objects.

Make the cells special


Now for the HexCellrequired index of special objects, which determines the type of special object, if it is located there.

  int specialIndex; 

Like other objects of relief, let's give it the property to receive and set this value.

  public int SpecialIndex { get { return specialIndex; } set { if (specialIndex != value) { specialIndex = value; RefreshSelfOnly(); } } } 

By default, the cell does not contain a special object. We denote this by the index 0. Add a property that uses this approach to determine whether a cell is special.

  public bool IsSpecial { get { return specialIndex > 0; } } 

To edit cells, add support for the index of special objects in HexMapEditor. It works similarly to the levels of urban, rural and vegetable objects.

  int activeUrbanLevel, activeFarmLevel, activePlantLevel, activeSpecialIndex; … bool applyUrbanLevel, applyFarmLevel, applyPlantLevel, applySpecialIndex; … public void SetApplySpecialIndex (bool toggle) { applySpecialIndex = toggle; } public void SetSpecialIndex (float index) { activeSpecialIndex = (int)index; } … void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applySpecialIndex) { cell.SpecialIndex = activeSpecialIndex; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } … } } 

Add a slider to the UI to control a particular object. Since we have three objects, use the 0–3 interval in the slider. Zero will mean the absence of an object, one - a lock, two - a ziggurat, three - a megaflora.


Slider special objects.

Adding special objects


Now we can assign specific objects to the cells. In order for them to appear, we need to add HexFeatureManagerone more method. It simply creates an instance of the desired special object and places it in the desired position. Since zero denotes the absence of an object, we must subtract one from the index of specific cell objects before accessing the array of prefabs.

  public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); } 

Let's give the object an arbitrary rotation using the hash table.

  public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); HexHash hash = HexMetrics.SampleHashGrid(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); } 

When a cell is triangulated into, we HexGridChunk.Triangulatecheck if the cell contains a particular object. If so, then call our new method, just like AddFeature.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } 


Special objects. They are much more common.

Avoid rivers


Since special objects are located in the centers of the cells, they are not combined with rivers, because they will hang above them.


Objects on the rivers.

In order to not create special objects on top of rivers, change the property HexCell.SpecialIndex. We will change the index only when there are no rivers in the cell.

  public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RefreshSelfOnly(); } } } 

In addition, when adding a river, we will need to get rid of all the special objects. The river must wash them away. This can be done by assigning the HexCell.SetOutgoingRivervalue 0 to the indices of special objects.

  public void SetOutgoingRiver (HexDirection direction) { … hasOutgoingRiver = true; outgoingRiver = direction; specialIndex = 0; neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.specialIndex = 0; SetRoad((int)direction, false); } 

Avoid roads


Like rivers, roads are also poorly combined with special objects, but not everything is so terrible. You can even leave the road as it is. Perhaps some objects are compatible with roads, while others are not. Therefore, you can make them dependent on the object. But we will make it easier.


Objects on the roads.

In this case, let the special objects win the roads. Therefore, if we change the index of special objects, we will also delete all roads from the cell.

  public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RemoveRoads(); RefreshSelfOnly(); } } } 

What if we remove a particular object?
0, , . .

In addition, this means that when adding roads, we will have to perform additional checks. We will add roads only when none of the cells is a cell with a particular object.

  public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && !IsSpecial && !GetNeighbor(direction).IsSpecial && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } } 

Avoid other objects.


Special objects cannot be mixed with other types of objects. If they overlap, it will look untidy. This may also depend on a particular object, but we will use the same approach.


An object that intersects with other objects.

In this case, we will suppress smaller objects, as if they were under water. This time we will perform the check in HexFeatureManager.AddFeature.

  public void AddFeature (HexCell cell, Vector3 position) { if (cell.IsSpecial) { return; } … } 

Avoid water


We also have a problem with water. Will special objects be preserved when flooded? Since we destroy small objects in flooded cells, let's do the same with particular objects.


Objects in the water.

We HexGridChunk.Triangulatewill perform the same flooding check for both special and ordinary objects.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (!cell.IsUnderwater && cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } 

Since both operators ifnow check if the cell is under water, we can transfer the test and perform it only once.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater) { if (!cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } } 

For experiments such a number of objects will be enough for us.

unitypackage

Source: https://habr.com/ru/post/425463/


All Articles