Unityの六角形マップ:霧の霧、地図調査、手続き生成

パート1〜3:グリッド、色、セルの高さ



パート4〜7:粗さ、川、および道路



パート8-11:水、地形、城壁



パート12〜15:保存と読み込み、テクスチャ、距離



パート16〜19:道を見つける、プレイヤーチーム、アニメーション



パート20-23:戦争の霧、地図調査、手続き生成



パート24〜27:水循環、侵食、バイオーム、円柱地図



パート20:戦争の霧





このパートでは、マップに戦争の霧の効果を追加します。



これで、シリーズはUnity 2017.1.0で作成されます。









今、私たちは見ることができる、見ることができないことがわかります。



シェーダーのセルデータ



多くの戦略ゲームでは、霧の概念を使用しています。 これは、プレイヤーの視界が制限されることを意味します。 彼は自分のユニットまたは管理エリアの近くにあるものしか見ることができません。 安心感はありますが、そこで何が起こっているのかわかりません。 通常、不可視の地形はより暗くレンダリングされます。 これを実現するには、セルの可視性を追跡し、それに応じてレンダリングする必要があります。



非表示のセルの外観を変更する最も簡単な方法は、メッシュデータに可視性メトリックを追加することです。 ただし、この場合、視界が変わると、新しい三角形分割を開始する必要があります。 ゲーム中に視界が絶えず変化するため、これは悪い決定です。



プレイヤーに見えないセルを部分的にマスクする、半透明の表面の地形にレンダリングする技術がよく使用されます。 この方法は、視野角が制限されているため、比較的平坦な地形に適しています。 しかし、地形にはさまざまな角度から見ることができる非常に多様な高さとオブジェクトを含めることができるため、このためには地形の形状に一致する非常に詳細なメッシュが必要です。 この方法は、上記の最も単純なアプローチよりも高価になります。



別のアプローチは、レリーフメッシュとは別にレンダリングするときに、セルのデータをシェーダーに転送することです。 これにより、三角形分割を1回だけ実行できます。 セルデータはテクスチャを使用して転送できます。 テクスチャの変更は、地形の三角測量よりもはるかに簡単なプロセスです。 さらに、いくつかの追加のテクスチャサンプルの実行は、単一の半透明レイヤーをレンダリングするよりも高速です。



シェーダー配列の使用はどうですか?
ベクトルの配列を使用して、セルデータをシェーダーに転送することもできます。 ただし、シェーダー配列には数千バイト単位のサイズ制限があり、テクスチャには数百万ピクセルを含めることができます。 大きなマップをサポートするには、テクスチャを使用します。


セルデータ管理



セルデータを含むテクスチャを制御する方法が必要です。 これを行う新しいHexCellShaderData



コンポーネントを作成しましょう。



 using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; }
      
      





新しいマップを作成またはロードするとき、正しいサイズで新しいテクスチャを作成する必要があります。 そのため、テクスチャを作成する初期化メソッドを追加します。 ミップテクスチャと線形色空間のないRGBAテクスチャを使用します。 セルデータを混在させる必要がないため、ポイントフィルタリングを使用します。 さらに、データを折りたたむべきではありません。 テクスチャの各ピクセルには、1つのセルからのデータが含まれます。



  public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; }
      
      





テクスチャサイズはマップのサイズと一致する必要がありますか?
いいえ、すべてのセルを保持するのに十分なピクセルがあれば十分です。 マップのサイズと完全に一致する場合、2のべき乗(NPOT以外)でないサイズのテクスチャが作成される可能性が高く、このテクスチャ形式は最も効果的ではありません。 2の累乗のサイズのテクスチャで動作するようにコードを構成できますが、これはセルデータへのアクセスを複雑にする小さな最適化です。


実際、新しいマップを作成するたびに新しいテクスチャを作成する必要はありません。 テクスチャが既に存在する場合、サイズを変更するだけで十分です。 Texture2D.Resize



はこれを行うのに十分賢いので、既に適切なサイズであるかどうかを確認する必要さえありません。



  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; } }
      
      





セルデータを1ピクセルずつ適用する代わりに、カラーバッファーを使用して、すべてのセルのデータを一度に適用します。 これを行うには、 Color32



配列を使用します。 必要に応じて、 Initialize



の最後に新しい配列インスタンスを作成します。 すでに正しいサイズの配列がある場合。 その内容をクリアします。



  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); } } }
      
      





color32とは何ですか?
標準の非圧縮RGBAテクスチャには4バイトのピクセルが含まれています。 4つのカラーチャネルのそれぞれが1バイトを受け取ります。つまり、256の可能な値があります。 Unity Color



構造を使用する場合、0〜1の範囲の浮動小数点コンポーネントは0〜255の範囲のバイトに変換されます。 サンプリング時に、GPUは逆変換を実行します。



Color32



構造体はバイトを直接処理するため、占有するスペースが少なく、変換の必要がないため、使用効率が向上します。 色ではなくセルデータを保存するため、 Color



ではなく生のテクスチャデータを直接操作する方が論理的です。


HexGrid



は、シェーダーでこれらのセルの作成と初期化を処理する必要があります。 したがって、それにcellShaderData



フィールドを追加し、 Awake



内にコンポーネントを作成します。



  HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); }
      
      





新しいマップを作成するときに、 cellShaderData



cellShaderData



必要があります。



  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; }
      
      





セルデータの編集



これまで、セルのプロパティを変更する場合、1つまたは複数のフラグメントを更新する必要がありましたが、現在はセルのデータを更新する必要がある場合があります。 つまり、セルにはシェーダーのセルデータへのリンクが必要です。 これを行うには、プロパティをHexCell



追加します。



  public HexCellShaderData ShaderData { get; set; }
      
      





HexGrid.CreateCell



、シェーダーデータコンポーネントをこのプロパティに割り当てます。



  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



メソッドをRefreshTerrain



に追加して、特定のセルのこのタスクを簡素化します。 ここでは、このメソッドを空のままにします。



  public void RefreshTerrain (HexCell cell) { }
      
      





HexCell.TerrainTypeIndex



変更して、このメソッドをHexCell.TerrainTypeIndex



、フラグメントの更新を命令しないようにします。



  public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; // Refresh(); ShaderData.RefreshTerrain(this); } } }
      
      





セルの地形のタイプを受け取った後、 HexCell.Load



呼び出します。



  public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … }
      
      





セルインデックス



これらのセルを変更するには、セルのインデックスを知る必要があります。 これを行う最も簡単な方法は、 Index



プロパティをHexCell



追加することHexCell



。 マップのセルのリストにあるセルのインデックスを示します。これは、シェーダーの指定されたセルのインデックスに対応します。



  public int Index { get; set; }
      
      





このインデックスはすでにHexGrid.CreateCell



にあるため、作成したセルに割り当てるだけです。



  void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … }
      
      





現在、 HexCellShaderData.RefreshTerrain



はこのインデックスを使用してセルデータを指定できます。 タイプをバイトに変換するだけで、ピクセルのアルファコンポーネントにエレベーションタイプインデックスを保存しましょう。 これにより、最大256種類の地形がサポートされます。これで十分です。



  public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; }
      
      





テクスチャにデータを適用してGPUに渡すには、 Texture2D.SetPixels32



を呼び出してからTexture2D.SetPixels32



を呼び出す必要があります。 フラグメントの場合と同様に、変更されたセルの数に関係なく、フレームごとに1回しか実行されないように、 LateUpdate



これらの操作を延期します。



  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



およびuseTerrainTypes



ます。 それらを単一のフィールドuseCellData



置き換えます。



 // public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; // public bool useTerrainTypes; public bool useCollider, useCellData, useUVCoordinates, useUV2Coordinates;
      
      





terrainTypes



リストの名前をcellIndices



ます。 また、 colors



cellWeights



リファクタリングしてみましょう-この名前の方がうまくいきます。



 // [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



変更して、これらのセルを使用するときに、別々ではなく2つのリストを取得するようにします。



  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



およびAddTriangleTerrainTypes



削除しAddTriangleTerrainTypes



。 それらを適切なAddTriangleCellData



メソッドに置き換えます。これらのメソッドは、インデックスと重みを一度に追加します。



  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



メソッドで同じことを行います。



  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リファクタリング



この段階では、 HexGridChunk



が必要なHexGridChunk



で多くのコンパイラエラーが発生します。 しかし、最初に、一貫性を保つために、静的な色を重みにリファクタリングします。



  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



を修正することから始めましょう。 以前は型が必要でしたが、現在はセルインデックスが必要です。 AddTriangleColor



およびAddTriangleTerrainTypes



コードを対応するAddTriangleCellData



コードでAddTriangleCellData



ます。



  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



です。 ここではすべてが少し複雑ですが、同じアプローチを使用しています。 また、パラメータ名c1



およびc2



w1



および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); } }
      
      





前の2つのメソッドは、更新も必要なTriangulateBoundaryTriangle



使用します。



  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



はレリーフマテリアルのプロパティHexCellShaderData



設定するHexCellShaderData



あります。 または、これらのセルのテクスチャをすべてのシェーダーにグローバルに表示させることができます。 これはいくつかのシェーダーで必要なので便利です。そのため、このアプローチを使用します。



セルテクスチャを作成した後、静的なShader.SetGlobalTexture



メソッドを呼び出して、 Shader.SetGlobalTexture



としてグローバルに表示します



  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); } … }
      
      





シェーダープロパティを使用して、UnityはtextureName_TexelSize変数を介してシェーダーでテクスチャサイズを使用できるようにします。 これは、幅と高さ、および幅と高さ自体に反する値を含む4コンポーネントベクトライザーです。 ただし、グローバルテクスチャを設定する場合、これは実行されません。 したがって、テクスチャを作成またはサイズ変更した後、 Shader.SetGlobalVector



を使用して自分で実行します。



  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) );
      
      





シェーダーデータアクセス



HexCellDataという名前のマテリアルフォルダーに新しいシェーダーインクルードファイルを作成します。 その中で、これらのセルのテクスチャとサイズに関する情報の変数を定義します。 また、指定された頂点メッシュデータのセルデータを取得する関数を作成します。



 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 GetCellData (appdata_full v) { }
      
      











新しいインクルードファイル。



テレインタイプの場合と同様に、セルインデックスはv.texcoord2



に格納されます。 最初のインデックスv.texcoord2.x



から始めましょう。 残念ながら、インデックスを直接使用してこれらのセルのテクスチャをサンプリングすることはできません。 UV座標に変換する必要があります。



U座標を作成する最初のステップは、セルインデックスをテクスチャの幅で除算することです。 これを行うには、 _HexCellData_TexelSize.x



乗算し_HexCellData_TexelSize.x







 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; }
      
      





結果はZU形式の数値になります。Zは行インデックスで、UはUセルの座標です。文字列を抽出するには、数値を切り捨ててから数値から減算してU座標を取得します。



 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; }
      
      





V座標は、ラインをテクスチャの高さで割っています。



 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; }
      
      





これにより、頂点データに保存されている最初のセルのインデックスの正しいUV座標が得られます。ただし、上部には最大3つの異なるインデックスを設定できます。したがって、GetCellData



どのインデックスでも機能するようにします。整数パラメータを追加します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



頂点プログラムでテクスチャをサンプリングしているため、使用するミップテクスチャをシェーダーに明示的に指示する必要があります。これはtex2Dlod



、4つのテクスチャの座標を必要とする関数使用して実行できます。これらのセルにはミップテクスチャがないため、追加の座標にゼロ値を割り当てます。



 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)); }
      
      





4番目のデータコンポーネントには、レリーフタイプインデックスが含まれ、これをバイトとして直接保存します。ただし、GPUは0〜1の範囲の浮動小数点値に自動的に変換しました。正しい値に戻すには、255を掛けます。その後、データを返すことができます。



  float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); data.w *= 255; return data;
      
      





この機能を使用するには、テレインシェーダーでHexCellData有効にします。このシェーダーをマテリアル/地形に配置したため、相対パス../HexCellData.cgincを使用する必要があります



  #include "../HexCellData.cginc" UNITY_DECLARE_TEX2DARRAY(_MainTex);
      
      





頂点プログラムでは、頂点データに格納されている3つのセルインデックスすべてのセルデータを取得します。次にdata.terrain



、標高インデックスを割り当てます。



  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



ます。3つの個別の標高インデックスを渡すため、3つの可視性値も渡します。



  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; }
      
      





0の可視性は、セルが現在非表示であることを意味します。可視であれば、可視性1の値になります。したがって、結果GetTerrainColor



に可視性の対応するベクトルを掛けることで、地形を暗くすることができます。したがって、各混合セルのレリーフ色を個別に調整します。



  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]); }
      
      











細胞が黒くなった。



代わりに、頂点プログラムで可視性を組み合わせることはできませんか?
, . . . , . , .


完全な暗闇は、一時的に見えない細胞にとってはやり過ぎです。浮き彫りが見えるようにするには、非表示のセルに使用するインジケーターを増やす必要があります。0–1から¼– 1に移動しましょうlerp



。これは、頂点プログラムの最後にある関数使用して実行できます



  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); }
      
      











影付きセル。



セル可視性追跡



可視性が機能するには、セルが可視性を追跡する必要があります。しかし、セルはそれが見えるかどうかをどのように決定しますか?これを行うエンティティの数を追跡することでこれを行うことができます。誰かがセルを見始めたら、このセルを報告しなければなりません。そして、誰かがセルを見ることをやめたら、それについても彼女に通知しなければなりません。セルは、エンティティが何であれ、ウォッチャーの数を単純に追跡します。セルの可視性の値が少なくとも1の場合、セルは表示され、そうでない場合は非表示になります。この動作を実装するには、HexCell



2つのメソッドとプロパティ変数に追加します。



  public bool IsVisible { get { return visibility > 0; } } … int visibility; … public void IncreaseVisibility () { visibility += 1; } public void DecreaseVisibility () { visibility -= 1; }
      
      





次に、HexCellShaderData



メソッドRefreshVisibility



にを追加します。これは、RefreshTerrain



可視性のためだけと同じことを行います。データをデータセルのコンポーネントRに保存します。値0〜1に変換されるバイトを処理するため、可視性を示すために使用します(byte)255







  public void RefreshVisibility (HexCell cell) { cellTextureData[cell.Index].r = cell.IsVisible ? (byte)255 : (byte)0; enabled = true; }
      
      





0から1の間で値を変更して、可視性を増減してこのメ​​ソッドを呼び出します。



  public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { ShaderData.RefreshVisibility(this); } } public void DecreaseVisibility () { visibility -= 1; if (visibility == 0) { ShaderData.RefreshVisibility(this); } }
      
      





分隊の可視性の作成



ユニットが占有しているセルを見ることができるようにしましょう。これはIncreaseVisibility



、タスク中のユニットの新しい場所への呼び出し使用して達成されますHexUnit.Location



また、古い場所(存在する場合)も呼び出します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



、範囲を考慮して1つのセルから見えるすべてのセルを見つけましょう。を複製して変更することで、このメソッドを作成できますSearch



。パラメータを変更し、リストプールを使用できるセルのリストを返すようにします。



各反復で、現在のセルがリストに追加されます。最終的なセルはないため、このポイントに到達しても検索は終了しません。また、移動のロジックと移動のコストも取り除きます。プロパティを作成するPathFrom



私たちはそれらを必要とせず、グリッドに沿ったパスに干渉したくないので、もはや尋ねられませんでした。



各ステップで、距離は1だけ増加します。範囲を超える場合、このセルはスキップされます。また、検索ヒューリスティックは必要ないため、値0で初期化します。つまり、実際にはダイクストラアルゴリズムに戻りました。



  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



メソッドIncreaseVisibility



とを追加しますDecreaseVisibility



彼らはセルと範囲を取得し、対応するセルのリストを取得し、それらの可視性を増減します。完了したら、リストをプールに戻す必要があります。



  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



するには、グリッドにアクセスする必要があるため、それにプロパティを追加します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; }
      
      





まず、3つのセルの可視性の範囲で十分です。これを行うには、HexUnit



定数に追加します。これは将来、常に変数になります。次に、分隊がグリッドIncreaseVisibility



とのメソッドを呼び出すようにし、DecreaseVisibility



この場所に移動するだけでなく、その可視範囲も送信します。



  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



cを設定しないことHexUnit.Travel



です。代わりに、location



プロパティコードを回避して、フィールドを直接変更します。したがって、古い場所を手動でクリアし、新しい場所を構成します。可視性は変更されません。



  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



は、完了後にのみ最初のセルの可視性を低下させLookAt



ます。その後、新しいセルに移動する前に、このセルからの可視性を高めます。これを終えると、再び可視性が低下します。最後に、最後のセルの可視性を高めます。



  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



、パスの古い中間位置がわかっているかどうかを確認できます。はいの場合は、パスの先頭ではなく、このセルの可視性を下げる必要があります。



  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



、セルの中心に道路の小さな部分を作成するために使用されるため、1つのセルインデックスが必要です。パラメーターを追加して、三角形のセルデータを生成します。



  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); }
      
      





道路を作成するもう1つの簡単な方法はTriangulateRoadSegment



です。セルの内部とセルの両方で使用されるため、2つの異なるインデックスで動作するはずです。このためには、インデックスベクトルパラメーターを使用すると便利です。道路セグメントは棚の一部である可能性があるため、重みもパラメータに渡す必要があります。



  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



では、セル内に道路を作成するに進みましょうインデックスパラメータも必要です。彼は、このデータを呼び出した道路メソッドに渡し、作成した三角形に追加します。



  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



およびに追加して、TriangulateRoadSegment



すべてのコンパイラエラーを修正します。



  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 ); } }
      
      





これでメッシュデータが正しいので、Roadシェーダーに進みます。頂点プログラムが必要で、HexCellDataが含まれている必要があります



  #pragma surface surf Standard fullforwardshadows decal:blend vertex:vert #pragma target 3.0 #include "HexCellData.cginc"
      
      





複数のマテリアルを混在させていないため、フラグメントプログラムに可視性のインジケータを1つ渡すだけで十分です。



  struct Input { float2 uv_MainTex; float3 worldPos; float visibility; };
      
      





新しい頂点プログラムが2つのセルからデータを受信するのに十分です。すぐに可視性をミックスし、調整して出力に追加します。



  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); … }
      
      





Waterシェーダーは、Roadシェーダーと同じ方法で変更する必要がありますが、2つではなく3つのセルの可視性を組み合わせる必要があります。



  #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



ます。既にインデックスベクトルを作成しましたが、オープンウォーターには1つのセルインデックスのみを使用しました。コーストにはネイバーインデックスも必要なので、コードを変更します。



  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



し、海岸と口のこれらのセルを処理します。口が台形でできており、両側に海岸の2つの三角形があることを忘れないでください。重量が正しい順序で転送されることを確認してください。



  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); … }
      
      





3つのセルの可視性を混合して、WaterShoreシェーダーWaterシェーダーに同じ変更を加える必要があります。



  #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; … }
      
      





シャイダー河口ミックス2つのセルの可視性だけでなく、シェーダ道路一般的な説明川のUV座標を送信する必要があるため、彼にはすでに頂点プログラムがあります。



  #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



2つのセルの可視性を維持できるように、インデックスベクトルパラメーターに追加し、メッシュに追加します。



  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



セルの中心に四角形と三角形のある川の終点を作成します。これに必要なセルデータを追加します。



  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



ので、呼び出しでそれらを渡すだけ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



必要な指標を川や滝の方法に渡すように変更します。



  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 ); } } … }
      
      





Riverシェーダーは、Roadシェーダーと同じ変更を行う必要があります



  #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; … }
      
      











可視性のある川。



ユニティパッケージ



オブジェクトと可視性



現在、可視性は手順で生成されたテレイン全体で機能しますが、これまでのところテレインフィーチャには影響しません。建物、農場、木は、手続き型ジオメトリからではなく、プレハブから作成されるため、セルインデックスを追加したり、頂点に重みを混ぜることはできません。これらのオブジェクトはそれぞれ1つのセルにのみ属しているため、どのセルに属しているかを判断する必要があります。これを実行できる場合、対応するセルのデータにアクセスし、可視性を適用します。



すでに世界のXZ位置をセルインデックスに変換できます。この変換は、地形の編集と分隊の管理に使用されました。ただし、対応するコードは重要です。整数演算を使用し、エッジを操作するロジックが必要です。これはシェーダーには実用的ではないため、ロジックの大部分をテクスチャでベイク処理して使用できます。



すでに六角形パターンのテクスチャを使用して、地形にメッシュを投影しています。このテクスチャは、2×2のセル領域を定義します。したがって、どの領域にいるかを簡単に計算できます。その後、この領域のセルにXオフセットとZオフセットを含むテクスチャを適用し、このデータを使用して自分がいるセルを計算できます。



これは同様のテクスチャです。 Xオフセットは赤チャンネルに保存され、Zオフセットは緑チャンネルに保存されます。 2×2セルの領域をカバーするため、0と2からのオフセットが必要です。そのようなデータはカラーチャンネルに保存できないため、オフセットは半分に削減されます。セルの明確なエッジは必要ないため、小さなテクスチャで十分です。









グリッド座標のテクスチャ。



プロジェクトにテクスチャを追加します。他のメッシュテクスチャと同様に、Wrap ModeRepeatに設定しますミキシングは必要ないので、ブレンドモードでは値Pointを選択しますまた、データが歪まないように、圧縮をオフにします。sRGBモードをオフにして、線形モードでレンダリングするときに色空間変換が実行されないようにします。最後に、ミップテクスチャは必要ありません。









テクスチャのインポートオプション。



可視性のあるオブジェクトシェーダー



新しい機能シェーダー作成して、オブジェクトに可視性サポートを追加します。これは、頂点プログラムを備えたシンプルなサーフェスシェーダーです。HexCellDataを追加して、可視性インジケータをフラグメントプログラムに渡し、通常どおり、色で検討します。ここでの違いはGetCellData



、必要なメッシュデータが存在しないため使用できないことです。代わりに、私たちには世界での地位があります。ただし、現時点では、可視性を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



、4コンポーネントのテクスチャ座標ベクトルが再び必要です。最初の2つの座標は、世界のXZ位置です。他の2つは以前と同様にゼロです。



  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; }
      
      





テレインシェーダーのように、UV座標をストレッチして、テクスチャが六角形のグリッドに対応する正しいアスペクト比になるようにします。



  float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0);
      
      





UV座標値を切り捨てることにより、2×2セルのどの部分にいるかがわかります。これは、セル座標の基礎を形成します。



  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;
      
      





グリッドの一部のサイズは2×2であり、オフセットは半分になるため、結果を2倍にして最終的な座標を取得する必要があります。



  float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; cellDataCoordinates *= 2;
      
      





これで、これらのセルのUV座標に変換する必要があるセルグリッドのXZ座標が得られました。これは、ピクセルの中心に移動し、それらをテクスチャサイズに分割するだけで実行できます。したがって、サンプリングを処理するHexCellDataインクルードファイルにこのための関数を追加しましょう



 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);
      
      











可視性を持つオブジェクト。



最後に、可視性は常に表示されるユニットを除き、マップ全体に影響します。各頂点のオブジェクトの可視性を決定するため、セルの境界を横切るオブジェクトの場合、それが閉じるセルの可視性は混合されます。しかし、オブジェクトは非常に小さいため、位置の歪みを考慮しても、常にセル内に残ります。ただし、一部は別のセルの頂点の一部である場合があります。したがって、私たちのアプローチは安価ですが、不完全です。これは、壁の場合に最も顕著であり、その視認性は隣接するセルの可視性の間で異なります。









視界が変化する壁。



壁セグメントは手続き的に生成されるため、セルデータをメッシュに追加し、レリーフに使用したアプローチを使用できます。残念ながら、タワーはプレハブであるため、依然として矛盾が生じます。一般的に言えば、既存のアプローチは単純なジオメトリには十分に見えます。将来的には、より詳細なモデルと壁を検討するため、それらの可視性を混合する方法を改善します。



ユニティパッケージ



パート21:地図調査





前のパートでは、戦争の霧を追加しました。これを改良して、地図研究を実装します。









私たちは世界を探索する準備ができています。



編集モードでマップ全体を表示します



この研究の意味は、これまで見られなかった細胞は未知であると見なされ、したがって目に見えないということです。それらを不明瞭にすることはできませんが、表示することはできません。したがって、調査サポートを追加する前に、編集モードでの可視性を有効にします。



可視性の切り替え



グリッド上のオーバーレイで行われたように、キーワードを使用してシェーダーが可視性を使用するかどうかを制御できます。HEX_MAP_EDIT_MODEキーワードを使用して、編集モードの状態示しましょういくつかのシェーダーはこのキーワードを知っている必要があるため、静的メソッドShader.EnableKeyWord



を使用してグローバルに定義しますShader.DisableKeyword



HexGameUI.SetEditMode



編集モードを変更するときに適切なメソッドを呼び出します。



  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"); } }
      
      





編集モードシェーダー



HEX_MAP_EDIT_MODEが定義されている場合、シェーダーは可視性を無視します。これは、セルの可視性が常に1と見なされるという事実に要約されます。HexCellData include-fileの先頭にあるキーワードに応じて、セルのデータをフィルターする関数を追加しましょう



 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.x = 1; #endif return data; }
      
      





両方の関数の結果をGetCellData



返す前に、この関数を通過させます。



 float4 GetCellData (appdata_full v, int index) { … return FilterCellData(data); } float4 GetCellData (float2 cellDataCoordinates) { … return FilterCellData(tex2Dlod(_HexCellData, float4(uv, 0, 0))); }
      
      





すべてが機能するためには、HEX_MAP_EDIT_MODEキーワードが定義されている場合に、関連するすべてのシェーダーがmulti_compileディレクティブを受け取ってオプションを作成する必要がありますターゲットディレクティブと最初のincludeディレクティブの間のシェーダー河口フィーチャー道路地形、および水辺に適切なラインを追加します



  #pragma multi_compile _ HEX_MAP_EDIT_MODE
      
      





これで、マップ編集モードに切り替えると、戦争の霧が消えます。



ユニティパッケージ



細胞研究



デフォルトでは、セルは未探索と見なされる必要があります。部隊が彼らを見ると、彼らは探検されます。その後、分遣隊がそれらを見ることができる場合、彼らは調査され続けます。



追跡調査ステータス



研究のステータスを監視するためのサポートを追加するために、HexCell



一般プロパティに追加しますIsExplored







  public bool IsExplored { get; set; }
      
      





研究の状態は、セル自体によって決定されます。したがって、このプロパティはのみ設定する必要がありますHexCell



このような制限を追加するには、セッターをプライベートに設定します。



  public bool IsExplored { get; private set; }
      
      





セルの可視性が初めてゼロより大きくなると、セルは調査対象と見なされるようになるためIsExplored



、値を割り当てる必要がありますtrue



実際、可視性が1に増加したときに、セルを検査済みとしてマークするだけで十分です。これは、呼び出しの前に行う必要がありますRefreshVisibility







  public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { IsExplored = true; ShaderData.RefreshVisibility(this); } }
      
      





研究状態をシェーダーに転送する



セルの可視性の場合と同様に、シェーダーデータを通じてシェーダーに研究状態を渡します。結局、これは単なる別のタイプの可視性です。HexCellShaderData.RefreshVisibility



データチャネルRに可視性状態を保存します。チャンネルGデータで調査の状態を保持しましょう。



  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



ために、研究データを除外するように変更します。



 float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.xy = 1; #endif return data; }
      
      





テレインシェーダーは、3つのセルすべての可視性データをフラグメントプログラムに渡します。研究状態の場合、頂点プログラムでそれらを組み合わせて、唯一の値をフラグメントプログラムに転送します。visibility



この場所を確保するために、4番目のコンポーネントを入力に追加します。



  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



ます。これは、他のシェーダーで可視性を組み合わせることと同様ですが、これらのセルのコンポーネントYを使用します。



  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



アルベドの計算で考慮してください。



  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; }
      
      











未探索の地形は黒になりました。



未探索の細胞の浮き彫りは黒色になりました。しかし、これはまだオブジェクト、道路、水に影響していません。ただし、これは調査が機能することを確認するのに十分です。



研究ステータスの保存と読み込み



調査サポートを追加したので、マップを保存およびロードするときに調査ステータスが考慮されるようにする必要があります。したがって、マップファイルのバージョンを3に増やす必要があります。これらの変更をより便利にするSaveLoadMenu



ために、この定数を追加しましょう



  const int mapFileVersion = 3;
      
      





にファイルバージョンを書き込むとき、Save



およびファイルサポートをチェックするときに、この定数を使用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



、調査のステータスを記録します。



  public void Save (BinaryWriter writer) { … writer.Write(IsExplored); }
      
      





そして、最後にそれを読みますLoad



その後RefreshVisibility



、調査の状態が前のものと異なる場合に呼び出します



  public void Load (BinaryReader reader) { … IsExplored = reader.ReadBoolean(); ShaderData.RefreshVisibility(this); }
      
      





古い保存ファイルとの後方互換性を維持するために、ファイルバージョンが3未満の場合、保存状態の読み取りをスキップする必要があります。この場合、デフォルトでは、セルの状態は「未探索」になります。これを行うには、パラメーターとしてLoad



ヘッダーデータを追加する必要があります



  public void Load (BinaryReader reader, int header) { … IsExplored = header >= 3 ? reader.ReadBoolean() : false; ShaderData.RefreshVisibility(this); }
      
      





次にHexGrid.Load



HexCell.Load



ヘッダーデータを渡す必要があります。



  public void Load (BinaryReader reader, int header) { … for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
      
      





これで、マップを保存およびロードするときに、セルの探索状態が考慮されます。



ユニティパッケージ



不明なセルを非表示



現在の段階では、未探索のセルは黒い浮き彫りで視覚的に示されます。しかし、実際には、これらのセルは不明であるため目に見えないようにします。不透明なジオメトリを透明にして、表示されないようにすることができます。ただし、Unityサーフェスシェーダーフレームワークは、この可能性を考慮せずに設計されました。真の透明度を使用する代わりに、シェーダーを背景に合わせて変更します。これにより、シェーダーも非表示になります。



レリーフを本当に黒にする



調査したレリーフは黒ですが、まだ鏡面反射光があるため、認識できます。照明を取り除くには、完全にマットな黒にする必要があります。他の表面特性に影響を与えないために、鏡面反射色を黒に変更するのが最も簡単です。これは、鏡面反射で動作するサーフェスシェーダーを使用する場合に可能ですが、現在は標準のメタリックを使用しています。それでは、テレインシェーダーをスペキュラーに切り替えてみましょう



カラープロパティに置き換え_Metallicをプロパティに_Specular。デフォルトでは、色の値は(0.2、0.2、0.2)に等しくなければなりません。したがって、メタリックバージョンの外観と一致することを保証します。



  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



ので、それを使用しましょう。



  half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color;
      
      





変化プラグマ表面サーフ標準StandardSpecularこれにより、Unityはスペキュラーを使用してシェーダーを生成します。



  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert
      
      





ここで、関数surf



は2番目のパラメーターの型が必要SurfaceOutputStandardSpecular



です。さらに、ではなくo.Metallic



、ではなく値を割り当てる必要があり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



鏡面反射色を考慮することにより、ハイライトを不明瞭にすることができます。



  o.Specular = _Specular * explored;
      
      











反射照明のない未踏の地形。



写真でわかるように、未探索のレリーフはくすんだ黒に見えます。ただし、接線角度で見ると、表面は鏡に変わります。そのため、レリーフは環境、つまりスカイボックスを反映し始めます。



表面が鏡になるのはなぜですか?
. . Rendering .








未開拓の地域はまだ環境を反映しています。



これらの反射を取り除くために、未探索の浮き彫りが完全に陰影付けされていることを考慮します。これはexplored



、反射マスクとして使用するオクルージョンパラメーターに値を割り当てることで実現されます。



  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;
      
      











反射のない未踏。



一致する背景



未探索の地形はすべての照明を無視するようになったので、背景と一致させる必要があります。カメラは常に上から見えるため、背景は常に灰色です。使用する色をテレインシェーダーに指示するには、_BackgroundColorプロパティを追加します。デフォルトは黒です。



  Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor;
      
      





この色を使用するには、発光色として追加します。これはo.Emission



、探索されたマイナス1を掛けた背景色の値を割り当てることによって実現されます。



  o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored);
      
      





デフォルトのスカイボックスを使用しているため、表示される背景色は実際には同じではありません。一般に、わずかに赤みがかった灰色が最適な色になります。レリーフマテリアルを設定するときは、Hex Colorにコード68615BFF を使用できます









灰色の背景色のレリーフ素材。



一般的には動作しますが、どこを見るべきか知っていれば、非常に弱いシルエットに気づくでしょう。プレーヤーがそれらを見ることができないように、スカイボックスの代わりに68615BFFの均一な背景色をカメラに割り当てることができます。









均一な背景色のカメラ。



スカイボックスを削除してみませんか?
, , environmental lighting . , .


これで、背景のセルと未探索のセルを区別できなくなりました。高い未探査の地形は、低いカメラアングルでの低い調査地形を依然として不明瞭にする可能性があります。さらに、未探索のパーツはまだ探索された部分に影を落とします。しかし、これらの最小限の手がかりは無視できます。









未探索のセルは表示されなくなりました。



均一な背景色を使用しない場合はどうなりますか?
, , . . , . , , , UV- .


レリーフオブジェクトを隠す



これで、レリーフのメッシュのみが非表示になりました。研究の残りの状態はまだ影響を受けていません。









これまでのところ、安reliefだけが隠されています。 Terrainのような不透明なシェーダーである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 _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



もう1つのコンポーネントも必要です。ので機能は各頂点のための可視性を兼ね備えた、彼は一つの値だけフロートを必要としていました。ここで2つ必要です。



  struct Input { float2 uv_MainTex; float2 visibility; };
      
      





vert



可視性データを明示的に使用するように変更してからdata.visibility.x



data.visibility.y



値をスタディデータに割り当てます。



  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; }
      
      





Terrainのsurf



ような新しいデータを使用するように変更します。



  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



もう1つのコンポーネントを追加し、それに応じて変更しvert



ます。両方のシェーダーは、3つのセルからのデータを結合します。



  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; }
      
      





WaterWater Shoresurf



異なる操作を実行しますが、表面のプロパティを同じように設定します。透明なのでexplore



、アルファチャンネルを考慮し、放出を設定しません。



  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; }
      
      











隠された水。



河口、川、道路を隠す



私たちは、シェーダ滞在河口河川道路の一般的な説明を3つはすべて透過的であり、2つのセルのデータを結合します。それらをすべて鏡面反射に切り替えてから、visibility



研究データに追加します。



  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



シェーダーの機能を変更します。どちらも同じ変更を行う必要があります。



  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; }
      
      





Shader Roadは、追加の混合メトリックを使用するため少し異なります。



  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



ではHexUnit



これにより、異なる移動ルールを持つユニットのサポートが簡素化されます。一般的な方法



追加して、移動のコストを決定します。彼はどの細胞が細胞間を移動しているか、また方向を知る必要があります。このメソッドに移行するコストの対応するコードをコピーし、変数の名前を変更します。HexUnit



GetMoveCost



HexGrid.Search







  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



、このアプローチはここでは機能しません。移動が不可能な場合は、移動の負のコストを返します。



  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



プロパティに追加しますSpeed



一方、24の定数値を返します。



  public int Speed { get { return 24; } }
      
      





HexGrid



変更FindPath



、そしてSearch



、彼らは私たちの新しいアプローチで作業できるようにします。



  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



、次のセルに移動できるかどうか、および移動のコストを決定した古いコードから削除します。代わりに、それが呼び出されますHexUnit.IsValidDestination



HexUnit.GetMoveCost



移動コストが負の場合、セルをスキップします。



  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



、セルが検査されたかどうか確認することだけです



  public bool IsValidDestination (HexCell cell) { return cell.IsExplored && !cell.IsUnderwater && !cell.Unit; }
      
      











より多くのユニットが未探索のセルに到達することはできません。



未探索のセルは有効なエンドポイントではないため、分隊はエンドポイントに移動するときにそれらを回避します。つまり、未開拓のエリアは障壁として機能し、経路を延長したり、不可能にしたりすることさえあります。最初にエリアを探索するには、ユニットを未知の地形に近づける必要があります。



移動中に短いパスが表示されたらどうなりますか?
. , . .



, , . , .


ユニティパッケージ



パート22:可視性の強化





地図探索のサポートを追加することにより、計算と可視性の遷移を改善します。









さらに見るには、高く登ります。



可視性の遷移



セルは、切り離しの範囲内にあるかどうかによって、表示または非表示になります。ユニットがセル間を移動するのに時間が必要と思われる場合でも、その視野はセルからセルへ瞬時にジャンプします。その結果、周囲のセルの可視性が劇的に変化します。分隊の動きはスムーズに見えますが、視界の変化は突然です。



理想的には、可視性もスムーズに変化するはずです。いったん視野に入ると、細胞は徐々に照らされ、それを残して、徐々に暗くなります。それとも、インスタントトランジションを好むでしょうか?HexCellShaderData



インスタントトランジションを切り替えるプロパティに追加しましょうデフォルトでは、移行はスムーズになります。



  public bool ImmediateMode { get; set; }
      
      





遷移セルの追跡



スムーズな遷移を表示する場合でも、真の可視性データは依然としてバイナリのままです。つまり、効果は視覚的なものにすぎません。これは、可視性の遷移を処理する必要があることを意味しHexCellShaderData



ます。遷移が実行されるセルのリストを提供します。各初期化で空であることを確認してください。



 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



直接設定しています。これは、インスタントトランジションでは依然として正しいですが、無効になっている場合は、トランジションセルのリストにセルを追加する必要があります。



  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; }
      
      





現時点では、リスト内のセルに対して何も実行していないため、可視性はもう機能していないようです。



ループ内のセルをループする



対応する値を即座に255または0に設定する代わりに、これらの値を徐々に増減します。遷移の滑らかさは、変化率に依存します。非常に高速でも低速でもないはずです。美しいトランジションとゲームの利便性の間の適切な妥協点は、1秒以内に変更することです。変更しやすくするために、これに定数を設定しましょう。



  const float transitionSpeed = 255f;
      
      





これでLateUpdate



、値に適用されるデルタを定義できます。これを行うには、時間差に速度を掛けます。整数である必要があります。どのくらいの大きさになるかわからないからです。フレームレートが急激に低下すると、デルタが255を超える可能



性があります。さらに、遷移セルがある間に更新する必要があります。そのため、リストに何かがある間はコードを含める必要があります。



  void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0; }
      
      





非常に理論的に可能な非常に高いフレームレート。低い遷移速度と組み合わせて、これは0のデルタを与えることができます。変更を行うために、デルタの最小値を強制的に1にします。



  int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; }
      
      





デルタを受け取ったら、すべての遷移セルをループしてデータを更新できます。このためのメソッドがありUpdateCellData



、そのパラメーターは対応するセルとデルタであるとします。



  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



すべてのセルが強制的に移動されます。代わりに、最後のセルを現在のインデックスに移動してから、最後のセルを削除します。



  if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt(transitioningCells.Count - 1); }
      
      





次に、メソッドを作成する必要がありますUpdateCellData



彼の仕事をするために、彼はインデックスとセルデータを必要とするので、それらを取得することから始めましょう。また、セルの更新を続行するかどうかも決定する必要があります。デフォルトでは、それは必要ないと仮定します。作業の完了後、変更されたデータを適用し、ステータス「更新が継続中」を返す必要があります。



  bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; }
      
      





セルデータの更新



この段階で、移行中または既に完了したセルがあります。まず、セルプローブの状態を確認しましょう。セルが調べられたが、そのG値がまだ255に等しくない場合、遷移のプロセスにあるため、これを監視します。



  bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data;
      
      





遷移を実行するには、セルのG値にデルタを追加します。算術演算はバイトでは機能せず、最初に整数に変換されます。したがって、合計は整数形式になり、バイトに変換する必要があります。



  if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; }
      
      





ただし、変換の前に、値が255を超えないようにする必要があります。



  int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t;
      
      





次に、Rの値を使用する可視性についても同じことを行う必要があります。



  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; }
      
      





セルが再び不可視になる可能性があるため、Rの値を減らす必要があるかどうかを確認する必要があります。これは、セルが不可視であるがRがゼロより大きい場合に発生します。



  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



準備が整い、可視性の移行が正しく実行されます。





可視性の遷移。



重複する遷移要素に対する保護



トランジションは機能しますが、重複したアイテムがリストに表示される場合があります。これは、移行中のセルの表示状態が変化した場合に発生します。たとえば、分隊の移動中に短時間だけセルが表示される場合。



重複した要素の出現の結果として、セル遷移はフレームごとに数回更新され、遷移の高速化と余分な作業につながります。これを防ぐには、セルを追加する前に、すでにリストにあるかどうかを確認します。ただし、すべての呼び出しでリスト検索RefreshVisibility



特に、複数のセル遷移が実行される場合、コストがかかります。代わりに、セルが移行中かどうかを示すためにまだ使用されていない別のチャネル、たとえば値Bを使用します。セルをリストに追加するとき、値255を割り当て、値が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; }
      
      





これが機能するには、セル遷移の完了後にBの値をリセットする必要があります。



  bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; }
      
      







重複のない遷移。



可視性の即時読み込み



可視性の変更は、マップをロードしているときでも常に緩やかです。マップはセルがすでに表示されている状態を表しているため、これは非論理的です。したがって、遷移はここでは不適切です。さらに、大きなマップの多くの可視セルに対してトランジションを実行すると、ロード後にゲームが遅くなる可能性があります。したがって、セルと分隊をロードする前HexGrid.Load



に、インスタント遷移モードに切り替えましょう



  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; }
      
      





ユニティパッケージ



高さ依存スコープ



これまで、すべてのユニットに3つの一定のスコープを使用しましたが、実際にはもっと複雑です。一般的な場合、次の2つの理由でオブジェクトを見ることができません。何らかの障害によりオブジェクトが見えないか、オブジェクトが小さすぎるか遠すぎます。このゲームでは、スコープ制限のみを実装します。



地球は私たちの視野を閉じているため、地球の反対側にあるものを見ることができません。地平線しか見えません。惑星はほぼ球体と見なすことができるため、視点が高いほど、より多くの表面を見ることができます。つまり、地平線は高さに依存します。









地平線は視点の高さに依存します。



私たちのユニットの限られた可視性は、地球の曲率によって作成された地平線効果を模倣しています。レビューの範囲は、惑星のサイズと地図の縮尺によって異なります。少なくともそれは論理的な説明です。しかし、範囲を縮小する主な理由はゲームプレイであり、これは戦争の霧と呼ばれる制限です。ただし、視野の基礎となる物理学を理解すると、地平線から遠ざかり、より低い障害物を見ることができるため、高い視点には戦略的価値があると結論付けることができます。しかし、これまでのところ、私たちはそれを実現していません。



レビューの高さ



スコープを決定するときに高さを考慮するには、高さを知る必要があります。これは、陸上のセルか水かによって、通常の高さまたは水のレベルになります。これをHexCell



プロパティに追加しましょう



  public int ViewElevation { get { return elevation >= waterLevel ? elevation : waterLevel; } }
      
      





ただし、高さがスコープに影響する場合、セルの表示高さが変わると、可視性の状況も変わる可能性があります。セルが複数のユニットの範囲をブロックしているか、現在ブロックしているため、変更が必要なものを判断するのはそれほど簡単ではありません。セル自体はこの問題を解決できないため、状況の変化を報告させHexCellShaderData



ます。HexCellShaderData



このためのメソッドがあるとしますViewElevationChanged



HexCell.Elevation



必要に応じて、割り当て時に呼び出します



  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



一般的な可視性の状況の変化を判断することは、特に複数のセルを同時に変更する場合は難しい作業です。したがって、トリックを思い付くのではなく、すべてのセルの可視性をリセットすることを計画します。これを行う必要があるかどうかを追跡するブールフィールドを追加します。メソッド内では、単純にtrueに設定し、コンポーネントを含めます。同時に変更されるセルの数に関係なく、これは単一のリセットにつながります。



  bool needsVisibilityReset; … public void ViewElevationChanged () { needsVisibilityReset = true; enabled = true; }
      
      





すべてのセルの可視性の値をリセットするには、それらにアクセスできる必要がありますがHexCellShaderData



、持ってません。それでは、この責任を委任しましょうHexGrid



これを行うには、HexCellShaderData



プロパティを追加する必要があります。これにより、グリッドを参照できます。次に、それを使用してLateUpdate



リセットを要求できます



  public HexGrid Grid { get; set; } … void LateUpdate () { if (needsVisibilityReset) { needsVisibilityReset = false; Grid.ResetVisibility(); } … }
      
      





次に進みましょう。シェーダーデータを作成した後HexGrid



、グリッドへのリンクを設定しHexGrid.Awake



ます。



  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); cellShaderData.Grid = this; CreateMap(cellCountX, cellCountZ); }
      
      





HexGrid



ResetVisibility



すべてのセルをリセットするメソッドも取得する必要がありますループ内のすべてのセルを巡回させ、リセットを自分自身に委任するだけです。



  public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } }
      
      





次に、HexCell



メソッドに追加する必要がありResetVisibilty



ます。単に可視性をゼロにし、可視性の更新をトリガーします。これは、セルの可視性がゼロより大きい場合に実行する必要があります。



  public void ResetVisibility () { if (visibility > 0) { visibility = 0; ShaderData.RefreshVisibility(this); } }
      
      





すべての可視性データをリセットした後HexGrid.ResetVisibility



、すべてのユニットに可視性を再度適用する必要があります。そのためには、各ユニットのスコープを知る必要があります。プロパティを使用して取得できるとします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



リファクタリングHexUnit.VisionRange



してプロパティに変換します。3の定数値を受け取りますが、将来的には変更されます。



  public int VisionRange { get { return 3; } }
      
      





このため、セルの表示高さを変更しても、可視性データはリセットされ、正しいままになります。ただし、スコープを決定するルールを変更し、再生モードで再コンパイルを実行する可能性があります。スコープを独立して変更HexGrid.OnEnable



するには、再コンパイルが検出されたときにリセットを開始しましょう



  void OnEnable () { if (!HexMetrics.noiseSource) { … ResetVisibility(); } }
      
      





これで、プレイモードのままで、スコープコードを変更して結果を確認できます。



地平線を広げる



スコープの計算が決定されHexGrid.GetVisibleCells



ます。高さがスコープに影響するようにfromCell



、透過領域を一時的に再定義することにより、単に表示の高さを使用できます。したがって、これが機能するかどうかを簡単に確認できます。



  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



プロパティに追加しますExplorable







  public bool Explorable { get; set; }
      
      





これで、調査対象のセルが表示されるようになるため、IsVisible



これを考慮してプロパティ変更します。



  public bool IsVisible { get { return visibility > 0 && Explorable; } }
      
      





同じことが適用されIsExplored



ます。ただし、このために標準プロパティを調査しました。ゲッターのロジックを変更できるように、明示的なプロパティに変換する必要があります。



  public bool IsExplored { get { return explored && Explorable; } private set { explored = value; } } … bool explored;
      
      





地図の端を隠す



メソッドで長方形マップのエッジを非表示にできHexGrid.CreateCell



ます。端にないセルは調査され、残りはすべて未探索です。



  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; … }
      
      





今、カードは端の周りで暗くなり、それらの後ろに巨大な未踏のスペースを隠します。その結果、マップの調査対象エリアのサイズは、各次元で2ずつ減少します。









マップの未探索のエッジ。



研究状態を編集可能にすることは可能ですか?
, , . .


未探索のセルは可視性を妨げます



最後に、セルを検査できない場合は、可視性を妨げるはずです。HexGrid.GetVisibleCells



これを考慮して変更してください。



  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase || !neighbor.Explorable ) { continue; }
      
      





ユニティパッケージ



パート23:土地の生成





チュートリアルのこの部分は、手順マップ生成のシリーズの始まりです。



この部分はUnity 2017.1.0で作成されました。









生成された多数のマップの1つ。



カード生成



任意のマップを作成できますが、時間がかかります。アプリケーションがデザイナーのためにカードを生成し、好みに合わせて変更できるようにしておくと便利です。別の手順を実行して、設計を手動で完全に作成し、完成したマップを生成する責任をアプリケーションに完全に移すことができます。このため、ゲームは毎回新しいマップでプレイでき、各ゲームセッションは異なります。これらすべてを可能にするには、マップ生成アルゴリズムを作成する必要があります。



必要な生成アルゴリズムのタイプは、必要なカードのタイプによって異なります。正しいアプローチはありません。信頼性とプレイアビリティの間の妥協点を常に探す必要があります。



カードが信じられるためには、プレイヤーにとって非常に可能性があり現実的である必要があります。これは、地図が地球の一部のように見えることを意味するものではありません。それは、異なる惑星または完全に異なる現実かもしれません。しかし、もしそれが地球の安reliefを示すなら、それは少なくとも部分的にそれに似ていなければなりません。



プレイアビリティは、カードがゲームプレイにどのように対応するかに関連しています。信念と矛盾することもあります。たとえば、山脈は美しいように見えますが、同時にユニットの移動とビューを大きく制限します。これが望ましくない場合は、山なしで行う必要があり、信頼性が低下し、ゲームの表現力が制限されます。または、山を救うことができますが、ゲームプレイへの影響を減らすことができ、信頼性も低下します。



さらに、実現可能性を考慮する必要があります。たとえば、構造プレート、侵食、雨、火山噴火、met石や月の影響などをシミュレートすることにより、非常にリアルな地球のような惑星を作成できます。しかし、そのようなシステムの開発には多くの時間が必要です。さらに、そのような惑星を生成するのに長い時間がかかる可能性があり、プレイヤーは新しいゲームを開始する前に数分待つ必要はありません。つまり、シミュレーションは強力なツールですが、価格がかかります。



ゲームはしばしば、真正性、プレイ可能性、および実現可能性の間のトレードオフを使用します。そのような妥協は目に見えず、完全に正常に見えることもあれば、開発プロセス中に下された決定に応じて、ランダム、一貫性のない、または無秩序に見えることもあります。これは、カード生成だけでなく、手続き型カードジェネレーターを開発する際には、これに特別な注意を払う必要があります。あなたが作成しているゲームにとって役に立たないことが判明した美しいカードを生成するアルゴリズムを作成するのに多くの時間を費やすことができます。



このチュートリアルシリーズでは、土地のようなレリーフを作成します。面白く、大きな変動性と大きな均一な領域の欠如が見られるはずです。レリーフスケールは大きく、マップは1つ以上の大陸、海洋の領域、または惑星全体をカバーします。土地の質量、気候、地域の数、地形の隆起など、地理を管理する必要があります。このパートでは、寿司作りの基礎を築きます。



編集モードで開始する



ゲームプレイではなくマップに焦点を当てるので、編集モードでアプリケーションを起動する方が便利です。これにより、すぐにカードを見ることができます。したがって、HexMapEditor.Awake



編集モードをtrueに設定し、このモードのシェーダーキーワードをオンにして変更します。



  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); SetEditMode(true); }
      
      





カードジェネレーター



手続き型マップを生成するには非常に多くのコードが必要なため、に直接追加しませんHexGrid



代わりに、私たちは、新しいコンポーネントを作成しHexMapGenerator



、そしてHexGrid



それはないだろう知っています。これにより、必要に応じて別のアルゴリズムへの移行が簡単になります。



ジェネレーターにはグリッドへのリンクが必要なので、一般的なフィールドを追加します。さらにGenerateMap



、アルゴリズムを処理する一般的なメソッドを追加します。マップの寸法をパラメーターとして指定し、それを使用して新しい空のマップを作成します。



 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



それをグリッドに接続します。









マップジェネレーターオブジェクト。



新しいマップのメニューを変更する



NewMapMenu



空のカードを作成するだけでなく、カードを生成できるように変更します。generateMaps



デフォルトで値を持つブール型フィールドを介して機能を制御しますtrue



optionsを切り替えるために行ったように、このフィールドを設定する一般的な方法を作成しましょうHexMapEditor



適切なスイッチをメニューに追加し、メソッドに接続します。



  bool generateMaps = true; public void ToggleMapGeneration (bool toggle) { generateMaps = toggle; }
      
      











スイッチ付きの新しいカードのメニュー。



メニューにマップジェネレーターへのリンクを指定します。次に、必要に応じて、グリッドGenerateMap



を実行するだけでなくジェネレーターメソッドを呼び出しますCreateMap







  public HexMapGenerator mapGenerator; … void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z); } else { hexGrid.CreateMap(x, z); } HexMapCamera.ValidatePosition(); Close(); }
      
      











ジェネレーターへの接続。



セルアクセス



ジェネレーターが機能するためには、セルにアクセスする必要があります。私たちはHexGrid



、すでに一般的な方法持っているGetCell



必要や位置ベクトル、または六角形の座標を。ジェネレータはどちらか一方を使用する必要がないためHexGrid.GetCell



、セルのオフセットまたはインデックスの座標を使用する2つの便利なメソッドを追加します。



  public HexCell GetCell (int xOffset, int zOffset) { return cells[xOffset + zOffset * cellCountX]; } public HexCell GetCell (int cellIndex) { return cells[cellIndex]; }
      
      





これでHexMapGenerator



、セルを直接受信できます。たとえば、新しいマップを作成した後、草の座標を使用して、セルの中央の列の起伏として草を設定できます。



  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; } }
      
      











小さな地図上の草の列。



ユニティパッケージ



寿司作り



マップを生成するとき、土地なしで完全に開始します。全世界が1つの巨大な海であふれていると想像できます。陸地は、海底の一部が押し上げられて水面上に浮かんだときに作成されます。この方法でどのくらいの土地を作成するか、どこに表示するか、どのような形状にするかを決定する必要があります。



レリーフを上げる



小さく始めましょう-私たちは水の上に一枚の土地を上げます。このためRaiseTerrain



に、プロットのサイズを制御するパラメーターを持つメソッドを作成します。このメソッドをで呼び出しGenerateMap



、以前のテストコードを置き換えます。7つのセルで構成される小さな土地から始めましょう。



  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



我々は土地の適切な量を得るまで、ランダムなセルを取り、その救済の種類を変更します。



ランダムセルを取得するにはGetRandomCell



、ランダムセルインデックスを決定し、グリッドから対応するセルを取得するメソッドを追加します。



  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)); }
      
      











7つのランダムな寿司セル。



最終的には多くのランダムなセルが必要になるか、すべてのセルを数回ループする必要があるため、セル自体のセルの数を追跡しましょうHexMapGenerator







  int cellCount; public void GenerateMap (int x, int z) { cellCount = x * z; … } … HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, cellCount)); }
      
      





1つのサイトの作成



これまでのところ、7つのランダムなセルを土地に変えていますが、どこにでも配置できます。ほとんどの場合、単一の土地エリアを形成していません。さらに、同じセルを複数回選択できるため、土地が少なくなります。両方の問題を無制限に解決するには、最初のセルのみを選択します。その後、以前に選択したセルの隣にあるセルのみを選択する必要があります。これらの制限はパス検索の制限に似ているため、ここでも同じアプローチを使用します。



にあるようにHexMapGenerator



、独自のプロパティと検索境界のフェーズのカウンターを追加します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



。これは、ユニットパスの検索を妨げる可能性があります。これを回避するために、マップ生成プロセスの最後に、すべてのセルの検索フェーズをゼロにリセットします。



  RaiseTerrain(7); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; }
      
      





次にRaiseTerrain



、適切なセルを探して、ランダムに選択する必要はありません。このプロセスは、の検索方法に非常に似ていますHexGrid



。ただし、セルに複数回アクセスすることはないため、検索境界の位相を2ではなく1増やすだけで十分です。その後、ランダムに選択された最初のセルで境界を初期化します。通常どおり、検索フェーズの指定に加えて、距離とヒューリスティックにゼロの値を割り当てます。



  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); } } }
      
      











細胞の蓄積。



実際、中央のセルがマップの端に表示されない場合、7つのセルはコンパクトな六角形の領域に美しく詰め込まれています。30のプロットサイズを使用してみましょう。



  RaiseTerrain(30);
      
      











30セルの寿司マス。



正しい六角形を得るのに十分なセルがありませんでしたが、再び同じ形状になりました。プロットの半径が大きいため、マップの端に近くなる可能性が高くなり、マップを別の形状に強制します。



寿司のランダム化



すべての領域が同じように見えることを望まないため、セルの優先度をわずかに変更します。境界に隣接するセルを追加するたびに、次の数値がRandom.value



特定のしきい値よりも小さい場合、このセルのヒューリスティックは0ではなく1になります。しきい値として値0.5を使用します。



  neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = Random.value < 0.5f ? 1: 0; searchFrontier.Enqueue(neighbor);
      
      











歪んだ領域。



セルの検索ヒューリスティックを増やすことで、予想よりも遅くアクセスしました。同時に、ヒューリスティックを増加させない限り、中心から1ステップ離れた場所にある他のセルも早くアクセスされます。つまり、すべてのセルのヒューリスティックを1つの値だけ増やしても、マップにはまったく影響しません。つまり、しきい値0のようなしきい値1の効果はありません。また、しきい値0.8は0.2に相当します。つまり、確率が0.5の場合、検索プロセスが最も「震えている」ことになります。



振動の適切な量は、希望する地形のタイプによって異なりますので、カスタマイズ可能にしましょう。jitterProbability



属性持つ一般的なフロートフィールドジェネレーターに追加しますRange



0〜0.5の範囲で制限されます。この間隔の平均、つまり0.25に等しいデフォルト値を設定しましょう。これにより、Unityインスペクターウィンドウでジェネレーターを構成できます。



  [Range(0f, 0.5f)] public float jitterProbability = 0.25f;
      
      











変動の確率。



ゲームUIでカスタマイズ可能にできますか?
, . UI, . , UI. , . , .


ここで、ヒューリスティックが1になるタイミングを決定するために、定数値の代わりに確率を使用します。



  neighbor.SearchHeuristic = Random.value < jitterProbability ? 1: 0;
      
      





ヒューリスティック値0と1を使用します。より大きな値を使用できますが、これによりセクションの変形が大幅に悪化し、ほとんどの場合、ストライプの束になります。



いくつかの土地を上げる



1つの土地の生成に限定されません。たとえばRaiseTerrain



、ループ内に呼び出しを配置し​​て、5つのセクションを取得します。



  for (int i = 0; i < 5; i++) { RaiseTerrain(30); }
      
      











土地の5つの区画。



現在はそれぞれ30セルの5つのプロットを生成していますが、必ずしも150セルの土地を取得しているわけではありません。各サイトは個別に作成されるため、互いを認識していないため、交差する可能性があります。これは通常のことです。孤立したセクションのセットよりも興味深いランドスケープを作成できるためです。



土地の変動性を高めるために、各プロットのサイズを変更することもできます。2つの整数フィールドを追加して、プロットの最小サイズと最大サイズを制御します。それらに十分な間隔、たとえば20〜200を割り当てます。標準の最小値を30、標準の最大値を100にします。



  [Range(20, 200)] public int chunkSizeMin = 30; [Range(20, 200)] public int chunkSizeMax = 100;
      
      











サイジング間隔。



これらのフィールドを使用して、呼び出されRaiseTerrain



ときに領域のサイズをランダムに決定します



  RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1));
      
      











中央のマップ上のランダムにサイズ設定された5つのセクション。



十分な寿司を作る



生成される土地の量を特に制御することはできませんが。プロット数の構成オプションを追加できますが、プロット自体のサイズはランダムであり、わずかにまたは強く重なる場合があります。したがって、サイトの数は、必要な土地の地図上の領収書を保証するものではありません。整数として表される土地の割合を直接制御するオプションを追加しましょう。100%の土地や水はあまりおもしろくないので、5〜95の間隔に制限します。デフォルトでは50です。



  [Range(5, 95)] public int landPercentage = 50;
      
      











寿司の割合。



適切な量​​の土地の作成を保証するために、十分な量が得られるまで地形のエリアを上げ続ける必要があります。これを行うには、土地の生成を複雑にするプロセスを制御する必要があります。したがって、新しいメソッドを呼び出して、サイトを上げる既存のサイクルを置き換えましょうCreateLand



このメソッドが最初に行うことは、土地になるセルの数を計算することです。この量は、寿司細胞の合計になります。



  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



RaiseTerrain



細胞全体を使い果たすまで 金額を超えないようにするためRaiseTerrain



、追加のパラメータとして金額を受け取るように変更します。仕事を終えた後、彼は残りの金額を返さなければなりません。



 // 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



は、細胞の全量を使うまで土地を上げることができます。



  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { landBudget = RaiseTerrain( Random.Range(chunkSizeMin, chunkSizeMax + 1), landBudget ); } }
      
      











マップのちょうど半分が土地になりました。



ユニティパッケージ



高さを考慮に入れる



土地は単なる海岸線ではなく、海岸線によって制限されています。彼女は、丘、山、谷、湖などを含む、変化する高さを持っています。ゆっくりと移動する構造プレートの相互作用により、高さに大きな違いがあります。シミュレートしませんが、私たちの土地は何らかの形でそのようなプレートに似ているはずです。サイトは移動しませんが、交差する場合があります。そして、これを利用できます。



土地を押し上げる



各セクションは、海底から押し出された土地の一部を示しています。したがって、現在のセルの高さを常に増やして、RaiseTerrain



何が起こるかを見てみましょう



  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.TerrainTypeIndex == 0) { … }
      
      











高さのある土地。



高さはわかりましたが、見にくいです。地理的な階層化など、各高さレベルに独自の地形タイプを使用すると、それらをより読みやすくすることができます。これは、高さをより目立たせるためにのみ行うので、単に高さレベルを標高インデックスとして使用できます。



高さが地形タイプの数を超えるとどうなりますか?
. , .


高さを変更するたびにセルの地形タイプを更新する代わりに、SetTerrainType



すべての地形タイプを1回だけ設定する別のメソッド作成しましょう



  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



はタイプの救済に対処することができず、高さに集中することができません。これを行うには、そのロジックを変更する必要があります。現在のセルの新しい高さが1である場合、そのセルはより乾燥しているため、セルの合計が減少し、サイトの成長の完了につながる可能性があります。



  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; // } // }
      
      











層の層化。



水を加える



すべてのセルの水位を1に設定して、どのセルが水または土地であるかを明示的に示しましょうGenerateMap



。土地を作成する前にこれを実行します。



  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; } } }
      
      











土地と水。



水位を上げる



1つの水位に限定されません。間隔が1〜5でデフォルト値が3の共通フィールドを使用して、カスタマイズ可能にしましょう。このレベルを使用して、セルを初期化します。



  [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; } … }
      
      

















水位3。水位が3の



場合、予想よりも少ない土地が得られます。これは、RaiseTerrain



水位が1であるまだ信じているためです。それを修正しましょう。



  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == waterLevel && --budget == 0) { break; }
      
      





より高い水位を使用すると、それにつながります。細胞がすぐに土地にならないこと。水位が2の場合、最初のセクションは水面下に残ります。海の底は上昇していますが、まだ水面下にあります。土地は、少なくとも2つのセクションの交差点でのみ形成されます。水位が高いほど、土地を作成するためにより多くのサイトを横断する必要があります。したがって、水位が上昇すると、土地はより混oticとします。さらに、より多くのプロットが必要な場合、既存の土地で交差する可能性が高くなります。そのため、より小さなプロットを使用する場合のように、山がより一般的であり、平坦な土地はあまりありません。



























水位は2〜5で、寿司は常に50%です。



ユニティパッケージ



上下動



ここまでで、プロットを一度に1レベル上げましたが、これに限定する必要はありません。



高いサイト



各セクションはセルの高さを1レベル増やしますが、クリッピングが発生する場合があります。これは、2つのセクションのエッジが接触したときに発生します。これにより孤立した崖ができますが、長い崖線はまれです。プロットの高さを1ステップ以上増やすことで、出現頻度を増やすことができます。ただし、これは特定の割合のサイトに対してのみ行う必要があります。すべてのエリアが高くなると、地形に沿って移動することが非常に困難になります。0.25のデフォルト値を持つ確率フィールドを使用して、このパラメーターをカスタマイズ可能にしましょう。



  [Range(0f, 1f)] public float highRiseProbability = 0.25f;
      
      











細胞の強い上昇の可能性。



高い領域では高さの増加を使用できますが、これはすぐに手に負えなくなります。高さの差2はすでに崖を作っているので、これで十分です。水位に等しい高さをスキップできるため、セルが陸地になったかどうかを判断する方法を変更する必要があります。水位を下回り、同じレベル以上になった場合、新しい土地セルを作成しました。



  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; … }
      
      





























高さが大幅に増加する確率は、0.25、0.50、0.75、および1です。



土地を下げる



土地は常に上昇するわけではなく、時には下降することもあります。土地が十分に低くなると、水がいっぱいになり、失われます。これまでのところ、これを行っていません。私たちはエリアを押し上げるだけなので、通常、土地はまとまりのない丸いエリアのセットのように見えます。時々エリアを下げると、より多様なフォームが得られます。









沈んだ寿司のない大きな地図。



別の確率フィールドを使用して、地盤沈下の頻度を制御できます。低下は土地を破壊する可能性があるため、低下の確率は常に上昇の確率より低くなければなりません。そうしないと、適切な割合の土地を取得するのに非常に時間がかかる場合があります。したがって、0.4の最大低下確率と0.2のデフォルト値を使用しましょう。



  [Range(0f, 0.4f)] public float sinkProbability = 0.2f;
      
      











低下の確率。



サイトを下げることは上げることと似ていますが、いくつかの違いがあります。したがって、メソッドを複製し、RaiseTerrain



その名前をに変更しSinkTerrain



ます。上昇の大きさを判断する代わりに、同じロジックを使用できる低下値が必要です。同時に、水面を通過したかどうかを確認するための比較を裏返す必要があります。さらに、レリーフを下げるとき、セルの合計に限定されません。代わりに、失われた寿司のセルごとに消費した金額が返されるため、それを増やして作業を続けます。



  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



低下する確率に応じて、土地を低下または上昇させる必要があります。



  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); } } }
      
      





























ドロップの確率は、0.1、0.2、0.3、および0.4です。



高さを制限する



現在の段階では、潜在的に多くのセクションをオーバーラップさせることができます。場合によっては、いくつかの高さの増加があり、一部は下がってから再び上昇することがあります。同時に、特に高い割合の土地が必要な場合には、非常に高い、場合によっては非常に低い高さを作成できます。









90%の土地での巨大な高さ。



高さを制限するには、カスタムの最小値と最大値を追加しましょう。合理的な最小値は-4〜0の間で、許容される最大値は6〜10の範囲です。デフォルト値を-2と8にします。マップを手動で編集する場合、許容範囲外になるため、UIエディターのスライダーを変更するか、そのままにしておくことができます。



  [Range(-4, 0)] public int elevationMinimum = -2; [Range(6, 10)] public int elevationMaximum = 8;
      
      











最小および最大の高さ。



ここRaiseTerrain



で、高さが許容最大値を超えないようにする必要があります。これは、現在のセルが高すぎるかどうかを確認することで実行できます。その場合、高さを変更せずに、それらの隣人を追加せずにそれらをスキップします。これは、陸地が最大の高さに達した地域を避け、その周辺で成長するという事実につながります。



  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



。ただし、最小の高さについてです。



  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;
      
      











土地が90%の限られた高さ。



負の高度の保存



この時点で、保存およびロードコードは負の高さを処理できません。これは、高さをバイトとして保存するためです。負の数は、大きな正の値に保存されるときに変換されます。そのため、生成されたマップを保存およびロードすると、元の水中セルの代わりに非常に高いマップが表示される場合があります。



負の高さのサポートを追加するには、バイトではなく整数として保存します。ただし、複数レベルの高さをサポートする必要はありません。さらに、127を追加することで、格納された値をシフトできます。これにより、1バイト内で-127〜128の範囲の高さを正しく格納できます。HexCell.Save



それに応じて変更します。



  public void Save (BinaryWriter writer) { writer.Write((byte)terrainTypeIndex); writer.Write((byte)(elevation + 127)); … }
      
      





マップデータの保存方法を変更したためSaveLoadMenu.mapFileVersion



、4に増やしました。



  const int mapFileVersion = 4;
      
      





そして最後に、HexCell.Load



バージョン4ファイルからロードされた高さから127を引くように変更します。



  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



ます。同じ擬似乱数列を再度取得するには、同じシード値を使用する必要があります。以前に、同様のアプローチをすでに採用していHexMetrics.InitializeHashGrid



ます。最初に、特定のシード値で初期化された数値ジェネレーターの現在の状態を保存してから、元の状態を復元します。にも同じアプローチを使用できますHexMapGenerator.GenerateMap



を使用する他の要素に干渉しないように、古い状態を再度記憶し、完了後に復元できますRandom







  public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; … Random.state = originalRandomState; }
      
      





次に、最後のカードの生成に使用したシードを利用可能にする必要があります。これは、一般的な整数フィールドを使用して行われます。



  public int seed;
      
      











シードを表示します。



ここで、初期化するシード値が必要Random



です。ランダムカードを作成するには、ランダムシードを使用する必要があります。最も簡単なアプローチは、任意のシード値を使用してを生成することRandom.Range



です。初期ランダム状態に影響を与えないように、保存後にこれを行う必要があります。



  public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … }
      
      





完了後、ランダムな状態を復元するため、すぐに別のカードを生成すると、結果として同じシード値が取得されます。さらに、初期ランダム状態がどのように初期化されたかはわかりません。したがって、それは任意の開始点として機能しますが、呼び出しごとにランダム化するためにさらに何かが必要です。



乱数ジェネレーターを初期化するにはさまざまな方法があります。この場合、広い範囲で変化する複数の任意の値を単純に組み合わせることができます。つまり、同じカードを再生成する可能性は低くなります。たとえば、サイクルで表されるシステム時間の下位32ビットと、アプリケーションの現在のランタイムを使用します。結果がそれほど大きくならないように、ビットごとの排他的OR演算を使用してこれらの値を組み合わせます。



  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



ます。シードフィールドを手動で変更しない場合、結果は再び同じマップになります。



  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);
      
      





これで、将来再び生成するために、好きなマップのシード値をコピーしてどこかに保存できます。まったく同じジェネレーターパラメーター、つまり同じカードサイズ、および他のすべての構成オプションを使用する場合にのみ、同じカードを受け取ることを忘れないでください。これらの確率のわずかな変化でさえ、まったく異なるマップを作成できます。したがって、シードに加えて、すべての設定を記憶する必要があります。















シード値0および929396788の大きなカード、標準パラメーター。



ユニティパッケージ



All Articles