HexCellShaderData
component that will do this. using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; }
public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; }
Texture2D.Resize
is smart enough to do it for us. public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( cellCountX, cellCountZ, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } }
Color32
array. If necessary, we will create a new instance of the array at the end of Initialize
. If we already have an array of the correct size. then we clear its contents. Texture2D cellTexture; Color32[] cellTextureData; public void Initialize () { … if (cellTextureData == null || cellTextureData.Length != x * z) { cellTextureData = new Color32[x * z]; } else { for (int i = 0; i < cellTextureData.Length; i++) { cellTextureData[i] = new Color32(0, 0, 0, 0); } } }
Color
structure, its floating point components in the interval 0–1 are converted to bytes in the interval 0–255. When sampling, the GPU performs the inverse transform.Color32
structure works directly with bytes, so they take up less space and do not require conversion, which increases the efficiency of their use. Since we store cell data instead of colors, it would be more logical to work directly with raw texture data, rather than with Color
.HexGrid
. Therefore, add the cellShaderData
field to cellShaderData
and create a component inside Awake
. HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); }
cellShaderData
should also be initiated. public bool CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; cellShaderData.Initialize(cellCountX, cellCountZ); CreateChunks(); CreateCells(); return true; }
HexCell
. public HexCellShaderData ShaderData { get; set; }
HexGrid.CreateCell
assign this property to the shader data component. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.ShaderData = cellShaderData; … }
HexCellShaderData
method to RefreshTerrain
to simplify this task for a specific cell. Let's leave this method blank for now. public void RefreshTerrain (HexCell cell) { }
HexCell.TerrainTypeIndex
so that it HexCell.TerrainTypeIndex
this method and does not order to update the fragments. public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; // Refresh(); ShaderData.RefreshTerrain(this); } } }
HexCell.Load
after getting the cell relief type. public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … }
Index
property to HexCell
. It will designate the cell index in the list of map cells, which corresponds to its index in the given cells in the shader. public int Index { get; set; }
HexGrid.CreateCell
, so just assign it to the created cell. void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … }
HexCellShaderData.RefreshTerrain
can use this index to set cell data. Let's keep the index of the relief type in the alpha component of its pixel, simply converting the type to byte. This will support up to 256 types of terrain, which will be quite enough for us. public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; }
Texture2D.SetPixels32
, and then Texture2D.Apply
. As in the case of fragments, we will postpone these operations on LateUpdate
, so that you can perform them no more than once per frame, regardless of the number of changed cells. public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; }
public void Initialize (int x, int z) { … enabled = true; }
useColors
and useTerrainTypes
. Let's replace them with one field useCellData
. // public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; // public bool useTerrainTypes; public bool useCollider, useCellData, useUVCoordinates, useUV2Coordinates;
terrainTypes
to cellIndices
. Let's also refactor-rename colors
to cellWeights
— this name will be more appropriate. // [NonSerialized] List<Vector3> vertices, terrainTypes; // [NonSerialized] List<Color> colors; [NonSerialized] List<Vector3> vertices, cellIndices; [NonSerialized] List<Color> cellWeights; [NonSerialized] List<Vector2> uvs, uv2s; [NonSerialized] List<int> triangles;
Clear
so that when using these cells, it gets two lists together, not separately. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useCellData) { cellWeights = ListPool<Color>.Get(); cellIndices = ListPool<Vector3>.Get(); } // if (useColors) { // colors = ListPool<Color>.Get(); // } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } // if (useTerrainTypes) { // terrainTypes = ListPool<Vector3>.Get(); // } triangles = ListPool<int>.Get(); }
Apply
. public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useCellData) { hexMesh.SetColors(cellWeights); ListPool<Color>.Add(cellWeights); hexMesh.SetUVs(2, cellIndices); ListPool<Vector3>.Add(cellIndices); } // if (useColors) { // hexMesh.SetColors(colors); // ListPool<Color>.Add(colors); // } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } // if (useTerrainTypes) { // hexMesh.SetUVs(2, terrainTypes); // ListPool<Vector3>.Add(terrainTypes); // } hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); if (useCollider) { meshCollider.sharedMesh = hexMesh; } }
AddTriangleColor
and AddTriangleTerrainTypes
. Replace them with the corresponding methods AddTriangleCellData
, which add indices and weights at a time. public void AddTriangleCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); } public void AddTriangleCellData (Vector3 indices, Color weights) { AddTriangleCellData(indices, weights, weights, weights); }
AddQuad
method. public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3, Color weights4 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); cellWeights.Add(weights4); } public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2 ) { AddQuadCellData(indices, weights1, weights1, weights2, weights2); } public void AddQuadCellData (Vector3 indices, Color weights) { AddQuadCellData(indices, weights, weights, weights, weights); }
HexGridChunk
that need to be HexGridChunk
. But first, for the sake of consistency, refactor-rename static colors to weights. static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f);
TriangulateEdgeFan
. He used to need a type, but now he needs a cell index. AddTriangleColor
and AddTriangleTerrainTypes
code with the corresponding AddTriangleCellData
code. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float index) { terrain.AddTriangle(center, edge.v1, edge.v2); terrain.AddTriangle(center, edge.v2, edge.v3); terrain.AddTriangle(center, edge.v3, edge.v4); terrain.AddTriangle(center, edge.v4, edge.v5); Vector3 indices; indices.x = indices.y = indices.z = index; terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = type; // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); }
TriangulateEdgeFan(center, e, cell.Index);
TriangulateEdgeStrip
. Everything is a little more complicated here, but we use the same approach. Also refactor-rename the parameter names c1
and c2
to w1
and w2
. void TriangulateEdgeStrip ( EdgeVertices e1, Color w1, float index1, EdgeVertices e2, Color w2, float index2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); Vector3 indices; indices.x = indices.z = index1; indices.y = index2; terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // Vector3 types; // types.x = types.z = type1; // types.y = type2; // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
TriangulateEdgeStrip( m, weights1, cell.Index, e, weights1, cell.Index ); … TriangulateEdgeStrip( e1, weights1, cell.Index, e2, weights2, neighbor.Index, hasRoad ); … void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color w2 = HexMetrics.TerraceLerp(weights1, weights2, 1); float i1 = beginCell.Index; float i2 = endCell.Index; TriangulateEdgeStrip(begin, weights1, i1, e2, w2, i2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color w1 = w2; e2 = EdgeVertices.TerraceLerp(begin, end, i); w2 = HexMetrics.TerraceLerp(weights1, weights2, i); TriangulateEdgeStrip(e1, w1, i1, e2, w2, i2, hasRoad); } TriangulateEdgeStrip(e2, w2, i1, end, weights2, i2, hasRoad); }
TriangulateCorner
. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); Vector3 indices; indices.x = bottomCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangleCellData(indices, weights1, weights2, weights3); // terrain.AddTriangleColor(weights1, weights2, weights3); // Vector3 types; // types.x = bottomCell.TerrainTypeIndex; // types.y = leftCell.TerrainTypeIndex; // types.z = rightCell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
TriangulateCornerTerraces
. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color w3 = HexMetrics.TerraceLerp(weights1, weights2, 1); Color w4 = HexMetrics.TerraceLerp(weights1, weights3, 1); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleCellData(indices, weights1, w3, w4); // terrain.AddTriangleColor(weights1, w3, w4); // terrain.AddTriangleTerrainTypes(indices); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color w1 = w3; Color w2 = w4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); w3 = HexMetrics.TerraceLerp(weights1, weights2, i); w4 = HexMetrics.TerraceLerp(weights1, weights3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadCellData(indices, w1, w2, w3, w4); // terrain.AddQuadColor(w1, w2, w3, w4); // terrain.AddQuadTerrainTypes(indices); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadCellData(indices, w3, w4, weights2, weights3); // terrain.AddQuadColor(w3, w4, weights2, weights3); // terrain.AddQuadTerrainTypes(indices); }
TriangulateCornerTerracesCliff
. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryWeights = Color.Lerp(weights1, weights3, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( begin, weights1, left, weights2, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryColor); // terrain.AddTriangleTerrainTypes(indices); } }
TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryWeights = Color.Lerp(weights1, weights2, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( right, weights3, begin, weights1, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryWeights); // terrain.AddTriangleTerrainTypes(indices); } }
TriangulateBoundaryTriangle
, which also requires updating. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginWeights, Vector3 left, Color leftWeights, Vector3 boundary, Color boundaryWeights, Vector3 indices ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleCellData(indices, beginWeights, w2, boundaryWeights); // terrain.AddTriangleColor(beginColor, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color w1 = w2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleCellData(indices, w1, w2, boundaryWeights); // terrain.AddTriangleColor(c1, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleCellData(indices, w2, leftWeights, boundaryWeights); // terrain.AddTriangleColor(c2, leftColor, boundaryColor); // terrain.AddTriangleTerrainTypes(types); }
TriangulateWithRiver
. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangle(centerL, m.v1, m.v2); terrain.AddQuad(centerL, center, m.v2, m.v3); terrain.AddQuad(center, centerR, m.v3, m.v4); terrain.AddTriangle(centerR, m.v4, m.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; terrain.AddTriangleCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = cell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); … }
HexCellShaderData
is required to HexCellShaderData
property of the relief material. Or we can make the texture of these cells globally visible for all shaders. This is convenient because we will need it in several shaders, so we will use this approach.Shader.SetGlobalTexture
method to make it globally visible as _HexCellData . public void Initialize (int x, int z) { … else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } … }
Shader.SetGlobalVector
after creating or changing the size of the texture. else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } Shader.SetGlobalVector( "_HexCellData_TexelSize", new Vector4(1f / x, 1f / z, x, z) );
sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 GetCellData (appdata_full v) { }
v.texcoord2
, as was the case with terrain types. Let's start with the first index - v.texcoord2.x
. Unfortunately, we cannot directly use the index to sample the cell data texture. We will have to convert it to UV coordinates._HexCellData_TexelSize.x
. float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; }
float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; }
float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = row * _HexCellData_TexelSize.y; }
float4 GetCellData (appdata_full v) { float2 uv; uv.x = (v.texcoord2.x + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; }
GetCellData
work for any index. Add an integer parameter to it index
, which we will use to access the vector component with the cell index. float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; }
_HexCellData
. Since we sample the texture in the vertex program, we need to explicitly tell the shader which mip texture to use. This can be done using a function tex2Dlod
that requires the coordinates of four textures. Since these cells do not have mip-textures, we will assign zero values to the superfluous coordinates. float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); }
float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); data.w *= 255; return data;
#include "../HexCellData.cginc" UNITY_DECLARE_TEX2DARRAY(_MainTex);
data.terrain
their relief indices. void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); // data.terrain = v.texcoord2.xyz; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; }
Input
. Since we are transmitting three separate relief indices, we will also transmit three visibility values. struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 visibility; };
void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; }
GetTerrainColor
by the corresponding vector of visibility. Thus, we individually modulate the relief color of each mixed cell. float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * (IN.color[index] * IN.visibility[index]); }
lerp
at the end of the vertex program. void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; data.visibility = lerp(0.25, 1, data.visibility); }
HexCell
variable, two methods and a property. public bool IsVisible { get { return visibility > 0; } } … int visibility; … public void IncreaseVisibility () { visibility += 1; } public void DecreaseVisibility () { visibility -= 1; }
HexCellShaderData
method RefreshVisibility
that does the same thing RefreshTerrain
as for visibility only. Save the data in the R component of the cell data. Since we are working with bytes that are converted to values of 0–1, we use to denote visibility (byte)255
. public void RefreshVisibility (HexCell cell) { cellTextureData[cell.Index].r = cell.IsVisible ? (byte)255 : (byte)0; enabled = true; }
public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { ShaderData.RefreshVisibility(this); } } public void DecreaseVisibility () { visibility -= 1; if (visibility == 0) { ShaderData.RefreshVisibility(this); } }
IncreaseVisibility
for a new squad location at the task HexUnit.Location
. Also call for the old location (if it exists) DecreaseVisibility
. public HexCell Location { get { return location; } set { if (location) { location.DecreaseVisibility(); location.Unit = null; } location = value; value.Unit = this; value.IncreaseVisibility(); transform.localPosition = value.Position; } }
public void Die () { if (location) { location.DecreaseVisibility(); } location.Unit = null; Destroy(gameObject); }
HexGrid
method for finding all the cells visible from a single cell, taking into account the distance. We can create this method by duplicating and modifying Search
. Let's change its parameters and make it return a list of cells for which a list pool can be used.PathFrom
no longer asked, because we do not need them, and we do not want to interfere with the path on the grid. List<HexCell> GetVisibleCells (HexCell fromCell, int range) { List<HexCell> visibleCells = ListPool<HexCell>.Get(); searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; visibleCells.Add(current); // if (current == toCell) { // return true; // } // int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } // … // int moveCost; // … int distance = current.Distance + 1; if (distance > range) { continue; } // int turn = (distance - 1) / speed; // if (turn > currentTurn) { // distance = turn * speed + moveCost; // } if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; // neighbor.PathFrom = current; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } } } return visibleCells; }
HexGrid
methods IncreaseVisibility
and DecreaseVisibility
. They get a cell and range, take a list of relevant cells and increase / decrease their visibility. Having finished with this, they must return the list back to its pool. public void IncreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].IncreaseVisibility(); } ListPool<HexCell>.Add(cells); } public void DecreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].DecreaseVisibility(); } ListPool<HexCell>.Add(cells); }
HexUnit
requires access to the grid, so add a property to it Grid
. public HexGrid Grid { get; set; }
HexGrid.AddUnit
. public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.Grid = this; unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; }
HexUnit
constant, which in the future can always turn into a variable. Then we will make the detachment call methods for the grid IncreaseVisibility
and DecreaseVisibility
, passing on its own visual range, and not just move to this place. const int visionRange = 3; … public HexCell Location { get { return location; } set { if (location) { // location.DecreaseVisibility(); Grid.DecreaseVisibility(location, visionRange); location.Unit = null; } location = value; value.Unit = this; // value.IncreaseVisibility(); Grid.IncreaseVisibility(value, visionRange); transform.localPosition = value.Position; } } … public void Die () { if (location) { // location.DecreaseVisibility(); Grid.DecreaseVisibility(location, visionRange); } location.Unit = null; Destroy(gameObject); }
Location
in HexUnit.Travel
. Instead, we will directly change the field location
, avoiding the property code. Therefore, we will manually clear the old location and configure the new location. Visibility will remain unchanged. public void Travel (List<HexCell> path) { // Location = path[path.Count - 1]; location.Unit = null; location = path[path.Count - 1]; location.Unit = this; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); }
TravelPath
we will reduce the visibility of the first cell only after completion LookAt
. After that, before moving to a new cell, we will increase the visibility from this cell. Having finished with this, we will again reduce the visibility from it. Finally, increase the visibility from the last cell. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; // transform.localPosition = c; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility(pathToTravel[0], visionRange); float t = Time.deltaTime * travelSpeed; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { … } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } a = c; b = location.Position; // We can simply use the destination here. c = b; Grid.IncreaseVisibility(location, visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { … } … }
HexCell location, currentTravelLocation;
IEnumerator TravelPath () { … for (int i = 1; i < pathToTravel.Count; i++) { currentTravelLocation = pathToTravel[i]; a = c; b = pathToTravel[i - 1].Position; c = (b + currentTravelLocation.Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); yield return null; } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } currentTravelLocation = null; … }
TravelPath
we can check whether the old intermediate location of the path is known. If yes, then you need to reduce the visibility in this cell, and not at the beginning of the path. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility( currentTravelLocation ? currentTravelLocation : pathToTravel[0], visionRange ); … }
void OnEnable () { if (location) { transform.localPosition = location.Position; if (currentTravelLocation) { Grid.IncreaseVisibility(location, visionRange); Grid.DecreaseVisibility(currentTravelLocation, visionRange); currentTravelLocation = null; } } }
HexGridChunk.TriangulateRoadEdge
used to create a small part of the road in the center of the cell, so it needs one cell index. Add a parameter to it and generate cell data for the triangle. void TriangulateRoadEdge ( Vector3 center, Vector3 mL, Vector3 mR, float index ) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); Vector3 indices; indices.x = indices.y = indices.z = index; roads.AddTriangleCellData(indices, weights1); }
TriangulateRoadSegment
. It is used both inside and between the cells, so it should work with two different indices. For this, it is convenient to use the index vector parameter. Since road segments can be parts of ledges, weights must also be passed through parameters. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6, Color w1, Color w2, Vector3 indices ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); roads.AddQuadCellData(indices, w1, w2); roads.AddQuadCellData(indices, w1, w2); }
TriangulateRoad
that which creates the roads inside the cells. It also needs an index parameter. He passes this data to the methods he calls for roads, and adds them to the triangles he creates himself. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge, float index ) { if (hasRoadThroughCellEdge) { Vector3 indices; indices.x = indices.y = indices.z = index; Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment( mL, mC, mR, e.v2, e.v3, e.v4, weights1, weights1, indices ); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); roads.AddTriangleCellData(indices, weights1); roads.AddTriangleCellData(indices, weights1); } else { TriangulateRoadEdge(center, mL, mR, index); } }
TriangulateRoad
, TriangulateRoadEdge
and TriangulateRoadSegment
to correct all compiler errors. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Index); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction), cell.Index ); } } … void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge, cell.Index); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL, cell.Index); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center, cell.Index); } } … void TriangulateEdgeStrip ( … ) { … if (hasRoad) { TriangulateRoadSegment( e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4, w1, w2, indices ); } }
#pragma surface surf Standard fullforwardshadows decal:blend vertex:vert #pragma target 3.0 #include "HexCellData.cginc"
struct Input { float2 uv_MainTex; float3 worldPos; float visibility; };
void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); }
void surf (Input IN, inout SurfaceOutputStandard o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility); … }
HexGridChunk.TriangulateOpenWater
. void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, c1, c2); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); if (direction <= HexDirection.SE && neighbor != null) { … water.AddQuad(c1, c2, e1, e2); indices.y = neighbor.Index; water.AddQuadCellData(indices, weights1, weights2); if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); indices.z = nextNeighbor.Index; water.AddTriangleCellData( indices, weights1, weights2, weights3 ); } } }
void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … 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); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); … }
#pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb * IN.visibility; … }
HexGridChunk.TriangulateWaterShore
. We have already created a vector of indices, but used only one cell index for open water. The coasts also need a neighbor index, so we’ll change the code. Vector3 indices; // indices.x = indices.y = indices.z = cell.Index; indices.x = indices.z = cell.Index; indices.y = neighbor.Index;
TriangulateEstuary
. if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary( e1, e2, cell.IncomingRiver == direction, indices ); } else { … waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { … waterShore.AddTriangleUV( … ); indices.z = nextNeighbor.Index; waterShore.AddTriangleCellData( indices, weights1, weights2, weights3 ); }
TriangulateEstuary
and work on the data cells for the coast and the mouth. Do not forget that the mouths are made of trapezoid with two triangles of the coast on each side. Let the weights be transmitted in the correct order. void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver, Vector3 indices ) { 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) ); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); 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(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.AddQuadCellData( indices, weights2, weights1, weights2, weights1 ); estuaries.AddTriangleCellData(indices, weights1, weights2, weights2); estuaries.AddQuadCellData(indices, weights1, weights2); … }
#pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb * IN.visibility; … }
#include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; float visibility; }; half _Glossiness; half _Metallic; fixed4 _Color; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); o.visibility = cell0.x * v.color.x + cell1.x * v.color.y; o.visibility = lerp(0.25, 1, o.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility; … }
HexGridChunk.TriangulateRiverQuad
the vector indices to the parameter and add it to the mesh so that it can maintain the visibility of the two cells. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed, Vector3 indices ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed, indices); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); }
TriangulateWithRiverBeginOrEnd
creates end points of rivers with a quad and a triangle in the center of the cell. Add for this the necessary data cells. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; Vector3 indices; indices.x = indices.y = indices.z = cell.Index; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); … rivers.AddTriangleCellData(indices, weights1); } }
TriangulateWithRiver
, so we'll just pass them on the call TriangulateRiverQuad
. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed, indices ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); } }
void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); }
TriangulateConnection
so that it passes the necessary indices to the methods of rivers and waterfalls. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (hasRiver) { e2.v3.y = neighbor.StreamBedY; Vector3 indices; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; 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, indices ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY, indices ); } } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY, indices ); } } … }
#pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); o.Albedo = c.rgb * IN.visibility; … }
GetCellData
, because the required data meshes do not exist. Instead, we have a position in the world. But for now let's keep the visibility equal to 1. Shader "Custom/Feature" { 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 [NoTilingOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 #include "../HexCellData.cginc" sampler2D _MainTex, _GridCoordinates; half _Glossiness; half _Metallic; fixed4 _Color; struct Input { float2 uv_MainTex; float visibility; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); data.visibility = 1; } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.visibility; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
tex2Dlod
a four-component vector of texture coordinates. The first two coordinates are the position of the XZ world. The other two are still zero. void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); float4 gridUV = float4(pos.xz, 0, 0); data.visibility = 1; }
float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0);
float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); float2 cellDataCoordinates = floor(gridUV.xy);
float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg;
float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; cellDataCoordinates *= 2;
float4 GetCellData (float2 cellDataCoordinates) { float2 uv = cellDataCoordinates + 0.5; uv.x *= _HexCellData_TexelSize.x; uv.y *= _HexCellData_TexelSize.y; return tex2Dlod(_HexCellData, float4(uv, 0, 0)); }
cellDataCoordinates *= 2; data.visibility = GetCellData(cellDataCoordinates).x; data.visibility = lerp(0.25, 1, data.visibility);
Shader.EnableKeyWord
and Shader.DisableKeyword
. We will call the corresponding method in HexGameUI.SetEditMode
when changing the editing mode. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); if (toggle) { Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); } else { Shader.DisableKeyword("HEX_MAP_EDIT_MODE"); } }
sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.x = 1; #endif return data; }
GetCellData
before returning it. float4 GetCellData (appdata_full v, int index) { … return FilterCellData(data); } float4 GetCellData (float2 cellDataCoordinates) { … return FilterCellData(tex2Dlod(_HexCellData, float4(uv, 0, 0))); }
#pragma multi_compile _ HEX_MAP_EDIT_MODE
HexCell
shared property IsExplored
. public bool IsExplored { get; set; }
HexCell
. To add such a restriction, we make the setter private. public bool IsExplored { get; private set; }
IsExplored
value must be assigned true
. In fact, it will be sufficient for us to simply mark the cell as examined, when the visibility increases to 1. This must be done before the call RefreshVisibility
. public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { IsExplored = true; ShaderData.RefreshVisibility(this); } }
HexCellShaderData.RefreshVisibility
stores visibility state in channel R data. Let's keep the study state in channel G data. public void RefreshVisibility (HexCell cell) { int index = cell.Index; cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; enabled = true; }
FilterCellData
so that it filters out the research data. float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.xy = 1; #endif return data; }
visibility
fourth component to the input data so that we have a place for it. struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; };
data.visibility.xyz
. void vert (inout appdata_full v, out Input data) { … data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); }
data.visibility.w
. This is done in the same way as combining visibility in other shaders, but using the Y data component of the cells. data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); data.visibility.w = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z;
IN.visibility.w
. We take it into account in the calculation of albedo. void surf (Input IN, inout SurfaceOutputStandard o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
SaveLoadMenu
constant. const int mapFileVersion = 3;
Save
and when checking the file support in Load
. void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(mapFileVersion); hexGrid.Save(writer); } } void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= mapFileVersion) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
HexCell.Save
we will record the state of the study. public void Save (BinaryWriter writer) { … writer.Write(IsExplored); }
Load
. After that we will call RefreshVisibility
in case the state of the research differs from the previous one. public void Load (BinaryReader reader) { … IsExplored = reader.ReadBoolean(); ShaderData.RefreshVisibility(this); }
Load
header data as a parameter . public void Load (BinaryReader reader, int header) { … IsExplored = header >= 3 ? reader.ReadBoolean() : false; ShaderData.RefreshVisibility(this); }
HexGrid.Load
will have to transfer to the HexCell.Load
header data. public void Load (BinaryReader reader, int header) { … for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) }
fixed3
, so let's use it. half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color;
#pragma surface surf StandardSpecular fullforwardshadows vertex:vert
surf
needs the second parameter to have a type SurfaceOutputStandardSpecular
. In addition, you now need to assign a value not o.Metallic
, but o.Specular
. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; // o.Metallic = _Metallic; o.Specular = _Specular; o.Smoothness = _Glossiness; o.Alpha = ca; }
explored
the color specular. o.Specular = _Specular * explored;
explored
the occlusion parameter, which we use as a reflection mask. float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca;
Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor;
o.Emission
done by assigning a background color value multiplied by one minus explored. o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored);
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 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [NoScaleOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} }
#pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor;
visibility
also need another component. Since Feature combines the visibility for each vertex, it only needed one float value. Now we need two. struct Input { float2 uv_MainTex; float2 visibility; };
vert
so that it explicitly uses visibility data data.visibility.x
, and then assign the data.visibility.y
value of the study data. void vert (inout appdata_full v, out Input data) { … float4 cellData = GetCellData(cellDataCoordinates); data.visibility.x = cellData.x; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cellData.y; }
surf
to use new data, like Terrain . void surf (Input IN, inout SurfaceOutputStandardSpecular o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; float explored = IN.visibility.y; o.Albedo = c.rgb * (IN.visibility.x * explored); // o.Metallic = _Metallic; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); o.Alpha = ca; }
visibility
one more component and modify it accordingly vert
. Both shaders combine these three cells. struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; }
surf
different operations, but set their surface properties in the same way. Since they are transparent, we will take into account explore
in the alpha channel, but we will not set the emission. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; }
visibility
study data. struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y; }
surf
shaders to use the new data. Both need to make the same changes. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; }
void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility.x); float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend); float explored = IN.visibility.y; o.Albedo = c.rgb; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = blend * explored; }
HexGrid
to HexUnit
. This will simplify the support of units with different rules of movement.HexUnit
general method GetMoveCost
to determine the cost of moving. He needs to know which cells are moving between, as well as the direction. Copy the corresponding code of costs of moving from HexGrid.Search
to this method and change the variable names. public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { continue; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } }
continue
, but here this approach will not work. If movement is not possible, then we will return the negative costs of moving. public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { return -1; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { return -1; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } return moveCost; }
HexGameUI.DoPathFinding
. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, selectedUnit); } else { grid.ClearPath(); } } }
HexUnit
property Speed
. While it will return a constant value of 24. public int Speed { get { return 24; } }
HexGrid
change FindPath
, and Search
so that they can work with our new approach. public void FindPath (HexCell fromCell, HexCell toCell, HexUnit unit) { ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, unit); ShowPath(unit.Speed); } bool Search (HexCell fromCell, HexCell toCell, HexUnit unit) { int speed = unit.Speed; … }
Search
old code, which determined whether it is possible to move to the next cell and what are the costs of moving. Instead, we will call HexUnit.IsValidDestination
and HexUnit.GetMoveCost
. We will skip the cell if the cost of movement turns out to be negative. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } // if (neighbor.IsUnderwater || neighbor.Unit) { // continue; // } // HexEdgeType edgeType = current.GetEdgeType(neighbor); // if (edgeType == HexEdgeType.Cliff) { // continue; // } // int moveCost; // if (current.HasRoadThroughEdge(d)) { // moveCost = 1; // } // else if (current.Walled != neighbor.Walled) { // continue; // } // else { // moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; // moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + // neighbor.PlantLevel; // } if (!unit.IsValidDestination(neighbor)) { continue; } int moveCost = unit.GetMoveCost(current, neighbor, d); if (moveCost < 0) { continue; } int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … }
HexUnit.IsValidDestination
checks whether the cell is examined. public bool IsValidDestination (HexCell cell) { return cell.IsExplored && !cell.IsUnderwater && !cell.Unit; }
HexCellShaderData
property that switches instant transitions. By default, the transitions will be smooth. public bool ImmediateMode { get; set; }
HexCellShaderData
. Give him the list of cells in which the transition is performed. Let's make it so that with each initialization it is empty. using System.Collections.Generic; using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; Color32[] cellTextureData; List<HexCell> transitioningCells = new List<HexCell>(); public bool ImmediateMode { get; set; } public void Initialize (int x, int z) { … transitioningCells.Clear(); enabled = true; } … }
RefreshVisibility
directly. This is still correct for instant transition mode, but when it is disabled, we have to add a cell to the list of transition cells. public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else { transitioningCells.Add(cell); } enabled = true; }
const float transitionSpeed = 255f;
LateUpdate
we can define the delta applied to the values. To do this, multiply the time delta by the speed. It must be an integer value, because we do not know how big it can be. A sharp drop in the frame rate can make the delta greater than 255. void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0; }
int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; }
UpdateCellData
, the parameters of which are the corresponding cell and delta. int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); }
for (int i = 0; i < transitioningCells.Count; i++) { if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells.RemoveAt(i--); } }
RemoveAt
all cells to move after it. Instead, we move the last cell to the current index, and then delete the last one. if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt(transitioningCells.Count - 1); }
UpdateCellData
. To do his job, he will need an index and data cells, so let's start by getting them. It must also determine whether to continue updating the cell. By default, we will assume that it is not necessary. After completion of the work, it is necessary to apply the changed data and return the status “update continues”. bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; }
bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data;
if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; }
int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t;
if (cell.IsExplored && data.g < 255) { … } if (cell.IsVisible && data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; }
if (cell.IsVisible) { if (data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } } else if (data.r > 0) { stillUpdating = true; int t = data.r - delta; data.r = t < 0 ? (byte)0 : (byte)t; }
UpdateCellData
ready and visibility transitions are performed correctly.RefreshVisibility
costly, especially when the transition of a set of cells. Instead, let's use another yet unused channel to indicate whether a cell is in the process of transition, for example, value B. When adding a cell to the list, we will assign it a value of 255, and add to the list only those cells whose value is not 255. public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else if (cellTextureData[index].b != 255) { cellTextureData[index].b = 255; transitioningCells.Add(cell); } enabled = true; }
bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; }
HexGrid.Load
instant transition mode. public void Load (BinaryReader reader, int header) { … cellShaderData.ImmediateMode = true; for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
public void Load (BinaryReader reader, int header) { … bool originalImmediateMode = cellShaderData.ImmediateMode; cellShaderData.ImmediateMode = true; … cellShaderData.ImmediateMode = originalImmediateMode; }
HexCell
property. public int ViewElevation { get { return elevation >= waterLevel ? elevation : waterLevel; } }
HexCellShaderData
. Suppose u HexCellShaderData
have a method for this ViewElevationChanged
. We will call it when setting HexCell.Elevation
, if necessary. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } int originalViewElevation = ViewElevation; elevation = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } … } }
WaterLevel
. public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } int originalViewElevation = ViewElevation; waterLevel = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } ValidateRivers(); Refresh(); } }
HexCellShaderData.ViewElevationChanged
. Determining the change in the overall visibility situation is a difficult task, especially when changing several cells simultaneously. Therefore, we will not invent any tricks, but simply plan to reset the visibility of all the cells. Add a boolean field to see if it needs to be done. Inside the method we will simply assign it a true and include the component. Regardless of the number of simultaneously changing cells, this will result in a single reset of values. bool needsVisibilityReset; … public void ViewElevationChanged () { needsVisibilityReset = true; enabled = true; }
HexCellShaderData
not available. So let's delegate this responsibility HexGrid
. For this you need to add a HexCellShaderData
property that allows you to reference the grid. Then we can use it in LateUpdate
to request a reset. public HexGrid Grid { get; set; } … void LateUpdate () { if (needsVisibilityReset) { needsVisibilityReset = false; Grid.ResetVisibility(); } … }
HexGrid
: set the reference to the grid in HexGrid.Awake
after creating the shader data. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); cellShaderData.Grid = this; CreateMap(cellCountX, cellCountZ); }
HexGrid
should also get a method ResetVisibility
to reset all cells. Just make it loop around all the cells and delegate the execution to itself. public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } }
HexCell
method ResetVisibilty
. It will simply zero out the visibility and launch the visibility update. This should be done when the visibility of the cell is greater than zero. public void ResetVisibility () { if (visibility > 0) { visibility = 0; ShaderData.RefreshVisibility(this); } }
HexGrid.ResetVisibility
he must again apply visibility to all units, for which he needs to know the scope of each unit. Suppose you can get it using the property VisionRange
. public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } for (int i = 0; i < units.Count; i++) { HexUnit unit = units[i]; IncreaseVisibility(unit.Location, unit.VisionRange); } }
HexUnit.visionRange
to HexUnit.VisionRange
and turn it into a property. For now it will receive a constant value of 3, but this will change in the future. public int VisionRange { get { return 3; } }
HexGrid.OnEnable
when recompilation is detected. void OnEnable () { if (!HexMetrics.noiseSource) { … ResetVisibility(); } }
HexGrid.GetVisibleCells
. In order for the field of view to be affected by height, we can simply use the height of the view fromCell
, temporarily overriding the transmitted area. So we can easily check if it works. List<HexCell> GetVisibleCells (HexCell fromCell, int range) { … range = fromCell.ViewElevation; fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); … }
int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range) { continue; }
HexCoordinates fromCoordinates = fromCell.coordinates; while (searchFrontier.Count > 0) { … for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range || distance > fromCoordinates.DistanceTo(neighbor.coordinates) ) { continue; } … } }
range += fromCell.ViewElevation;
HexCell
property Explorable
. public bool Explorable { get; set; }
IsVisible
to take this into account. public bool IsVisible { get { return visibility > 0 && Explorable; } }
IsExplored
. However, for this we investigated the standard property. We need to convert it to an explicit property in order to be able to change the logic of its getter. public bool IsExplored { get { return explored && Explorable; } private set { explored = value; } } … bool explored;
HexGrid.CreateCell
. Cells that are not on the edge are examined, all others are unexplored. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; cell.Explorable = x > 0 && z > 0 && x < cellCountX - 1 && z < cellCountZ - 1; … }
HexGrid.GetVisibleCells
to accommodate this. if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase || !neighbor.Explorable ) { continue; }
HexMapEditor.Awake
setting the edit mode to true and turning on the shader keyword of this mode. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); SetEditMode(true); }
HexGrid
. Instead, we will create a new component HexMapGenerator
, but we will HexGrid
not know about it. This will simplify the transition to another algorithm, if we need it.GenerateMap
that will work on the algorithm. Give it the dimensions of the map as parameters, and then force it to use them to create a new blank map. using System.Collections.Generic; using UnityEngine; public class HexMapGenerator : MonoBehaviour { public HexGrid grid; public void GenerateMap (int x, int z) { grid.CreateMap(x, z); } }
HexMapGenerator
and connect it to the grid.NewMapMenu
so that it can generate maps, not just create empty ones. We will manage its functionality through a boolean field generateMaps
, which by default matters true
. Let's create a common method for specifying this field, as we did for switching options HexMapEditor
. Add a corresponding switch to the menu and connect it with the method. bool generateMaps = true; public void ToggleMapGeneration (bool toggle) { generateMaps = toggle; }
GenerateMap
generator method , and not just perform CreateMap
grids. public HexMapGenerator mapGenerator; … void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z); } else { hexGrid.CreateMap(x, z); } HexMapCamera.ValidatePosition(); Close(); }
HexGrid
already have general methods GetCell
that require either a position vector, or hexagon coordinates. The generator does not need to work with either one or the other, so we will add two convenient methods HexGrid.GetCell
that will work with the offset coordinates or the cell index. public HexCell GetCell (int xOffset, int zOffset) { return cells[xOffset + zOffset * cellCountX]; } public HexCell GetCell (int cellIndex) { return cells[cellIndex]; }
HexMapGenerator
Can now receive cells directly. For example, after creating a new map, using the coordinates of displacements, it can specify grass as the relief of the middle column of cells. public void GenerateMap (int x, int z) { grid.CreateMap(x, z); for (int i = 0; i < z; i++) { grid.GetCell(x / 2, i).TerrainTypeIndex = 1; } }
RaiseTerrain
with a parameter to control the size of the area. Call this method in GenerateMap
, replacing the previous test code. Let's start with a small plot of land consisting of seven cells. public void GenerateMap (int x, int z) { grid.CreateMap(x, z); // for (int i = 0; i < z; i++) { // grid.GetCell(x / 2, i).TerrainTypeIndex = 1; // } RaiseTerrain(7); } void RaiseTerrain (int chunkSize) {}
RaiseTerrain
to take a random cell and change the type of its relief until we get the right amount of land.GetRandomCell
that determines a random cell index and gets the corresponding cell from the grid. void RaiseTerrain (int chunkSize) { for (int i = 0; i < chunkSize; i++) { GetRandomCell().TerrainTypeIndex = 1; } } HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, grid.cellCountX * grid.cellCountZ)); }
HexMapGenerator
. int cellCount; public void GenerateMap (int x, int z) { cellCount = x * z; … } … HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, cellCount)); }
HexMapGenerator
property of your own and a search border phase counter, as it was in HexGrid
. HexCellPriorityQueue searchFrontier; int searchFrontierPhase;
public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } RaiseTerrain(7); }
HexGrid
. This may disrupt the search for a unit path. To avoid this, at the end of the card generation process, we will reset the search phase of all cells to zero. RaiseTerrain(7); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; }
RaiseTerrain
have to search for the corresponding cells, and not select them by chance. This process is very similar to the search method in HexGrid
. However, we will not visit the cells more than once, so it will be enough for us to increase the phase of the search boundary by 1 instead of 2. Then we initialize the border with the first cell, which is chosen randomly. As usual, in addition to setting its search phase, we assign a zero value to its distance and heuristics. void RaiseTerrain (int chunkSize) { // for (int i = 0; i < chunkSize; i++) { // GetRandomCell().TerrainTypeIndex = 1; // } searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(); firstCell.SearchPhase = searchFrontierPhase; firstCell.Distance = 0; firstCell.SearchHeuristic = 0; searchFrontier.Enqueue(firstCell); }
searchFrontier.Enqueue(firstCell); int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = 0; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } } searchFrontier.Clear();
searchFrontier.Enqueue(firstCell); HexCoordinates center = firstCell.coordinates; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } }
RaiseTerrain(30);
Random.value
less than a certain threshold value, then the heuristic of this cell becomes not 0, but 1. Let's use the value 0.5 as the threshold, that is, it is most likely to affect half the cells. neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = Random.value < 0.5f ? 1: 0; searchFrontier.Enqueue(neighbor);
jitterProbability
with the attributeRange
limited in the range of 0–0.5. We give it a default value equal to the average of this interval, that is, 0.25. This will allow us to customize the generator in the Unity inspector window. [Range(0f, 0.5f)] public float jitterProbability = 0.25f;
neighbor.SearchHeuristic = Random.value < jitterProbability ? 1: 0;
RaiseTerrain
inside a loop to get five plots. for (int i = 0; i < 5; i++) { RaiseTerrain(30); }
[Range(20, 200)] public int chunkSizeMin = 30; [Range(20, 200)] public int chunkSizeMax = 100;
RaiseTerrain
. RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1));
[Range(5, 95)] public int landPercentage = 50;
CreateLand
. The first thing this method does is to calculate the number of cells that should become dry. This amount will be our total sushi cell. public void GenerateMap (int x, int z) { … // for (int i = 0; i < 5; i++) { // RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1)); // } CreateLand(); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; } } void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); }
CreateLand
will call RaiseTerrain
until we have spent the entire amount of cells. In order not to exceed the amount, we change it RaiseTerrain
so that it receives the amount as an additional parameter. After completing the work, he must return the remaining amount. // void RaiseTerrain (int chunkSize) { int RaiseTerrain (int chunkSize, int budget) { … return budget; }
while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current.TerrainTypeIndex == 0) { current.TerrainTypeIndex = 1; if (--budget == 0) { break; } } size += 1; … }
CreateLand
can raise land until it spends the entire amount of cells. void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { landBudget = RaiseTerrain( Random.Range(chunkSizeMin, chunkSizeMax + 1), landBudget ); } }
RaiseTerrain
and see what happens. HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.TerrainTypeIndex == 0) { … }
SetTerrainType
to specify all types of the relief only once. void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); cell.TerrainTypeIndex = cell.Elevation; } }
public void GenerateMap (int x, int z) { … CreateLand(); SetTerrainType(); … }
RaiseTerrain
can not deal with the type of relief, and focus on the heights. To do this, change its logic. If the new height of the current cell is 1, then it has just become dry, so the sum of the cells has decreased, which may lead to the completion of the growth of the section. HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == 1 && --budget == 0) { break; } // if (current.TerrainTypeIndex == 0) { // current.TerrainTypeIndex = 1; // if (--budget == 0) { // break; // } // }
GenerateMap
before creating sushi. public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = 1; } CreateLand(); … }
void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (!cell.IsUnderwater) { cell.TerrainTypeIndex = cell.Elevation - cell.WaterLevel; } } }
[Range(1, 5)] public int waterLevel = 3; … public void GenerateMap (int x, int z) { … for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = waterLevel; } … }
RaiseTerrain
still believes that the water level is 1. Let's fix it. HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == waterLevel && --budget == 0) { break; }
[Range(0f, 1f)] public float highRiseProbability = 0.25f;
int rise = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation + rise; if ( originalElevation < waterLevel && current.Elevation >= waterLevel && --budget == 0 ) { break; } size += 1; … }
[Range(0f, 0.4f)] public float sinkProbability = 0.2f;
RaiseTerrain
and change its name to SinkTerrain
. Instead of determining the magnitude of the lift, we need the magnitude of the lowering, which can use the same logic. At the same time, comparisons to check whether we passed through the surface of the water, you need to turn. In addition, when lowering the relief, we are not limited to the sum of the cells. Instead, each lost land cell returns the amount spent on it, so we increase it and continue to work. int SinkTerrain (int chunkSize, int budget) { … int sink = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation - sink; if ( originalElevation >= waterLevel && current.Elevation < waterLevel // && --budget == 0 ) { // break; budget += 1; } size += 1; … } searchFrontier.Clear(); return budget; }
CreateLand
we must either lower or raise the land, depending on the probability of lowering. void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (Random.value < sinkProbability) { landBudget = SinkTerrain(chunkSize, landBudget); } else { landBudget = RaiseTerrain(chunkSize, landBudget); } } }
[Range(-4, 0)] public int elevationMinimum = -2; [Range(6, 10)] public int elevationMaximum = 8;
RaiseTerrain
we must make sure that the height does not exceed the permissible maximum. This can be done by checking if the current cells are too high. If so, then we skip them without changing the height or adding their neighbors. This will lead to the fact that land areas will avoid areas that have reached a maximum height, and grow around them. HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = originalElevation + rise; if (newElevation > elevationMaximum) { continue; } current.Elevation = newElevation; if ( originalElevation < waterLevel && newElevation >= waterLevel && --budget == 0 ) { break; } size += 1;
SinkTerrain
, but for the minimum height. HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = current.Elevation - sink; if (newElevation < elevationMinimum) { continue; } current.Elevation = newElevation; if ( originalElevation >= waterLevel && newElevation < waterLevel ) { budget += 1; } size += 1;
HexCell.Save
accordingly. public void Save (BinaryWriter writer) { writer.Write((byte)terrainTypeIndex); writer.Write((byte)(elevation + 127)); … }
SaveLoadMenu.mapFileVersion
to 4. const int mapFileVersion = 4;
HexCell.Load
so that it subtracts 127 from the heights loaded from version 4 files. public void Load (BinaryReader reader, int header) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); if (header >= 4) { elevation -= 127; } … }
Random.Range
and Random.value
. To get the same pseudo-random number sequence again, you need to use the same seed value. We have already applied a similar approach before, in HexMetrics.InitializeHashGrid
. It first saves the current state of the number generator initialized with a certain seed value, and then restores its original state. The same approach we can use for HexMapGenerator.GenerateMap
. Again, we can memorize the old state and restore it upon completion, so as not to interfere with anything else that uses Random
. public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; … Random.state = originalRandomState; }
public int seed;
Random
. To create random maps, you need to use a random seed. The simplest approach is to use an arbitrary seed to generate Random.Range
. So that it does not affect the initial random state, we need to do this after saving it. public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … }
seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.unscaledTime; Random.InitState(seed);
seed ^= (int)Time.unscaledTime; seed &= int.MaxValue; Random.InitState(seed);
public bool useFixedSeed;
GenerateMap
. If we do not change the seed field manually, then the same map will be generated again. Random.State originalRandomState = Random.state; if (!useFixedSeed) { seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.time; seed &= int.MaxValue; } Random.InitState(seed);
Source: https://habr.com/ru/post/427003/
All Articles