Hexagon-Karten in Unity: Nebel des Krieges, Kartenforschung, prozedurale Generierung

Teile 1-3: Netz, Farben und Zellenhöhen

Teile 4-7: Unebenheiten, Flüsse und Straßen

Teile 8-11: Wasser, Landformen und Wälle

Teile 12-15: Speichern und Laden, Texturen, Entfernungen

Teile 16-19: Weg finden, Spielerkader, Animationen

Teile 20-23: Nebel des Krieges, Kartenforschung, Verfahrensgenerierung

Teile 24-27: Wasserkreislauf, Erosion, Biomes, zylindrische Karte

Teil 20: Der Nebel des Krieges


  • Speichern Sie die Zellendaten in der Textur.
  • Reliefarten ohne Triangulation ändern.
  • Wir verfolgen die Sichtbarkeit.
  • Verdunkeln Sie alles Unsichtbare.

In diesem Teil werden wir der Karte den Nebel des Kriegseffekts hinzufügen.

Jetzt wird die Serie auf Unity 2017.1.0 erstellt.


Jetzt sehen wir, dass wir sehen können und nicht sehen können.

Zellendaten im Shader


Viele Strategiespiele verwenden das Konzept des Nebel des Krieges. Dies bedeutet, dass die Sicht des Spielers eingeschränkt ist. Er kann nur sehen, was sich in der Nähe seiner Einheiten oder seines kontrollierten Bereichs befindet. Obwohl wir die Erleichterung sehen können, wissen wir nicht, was dort passiert. Normalerweise wird das unsichtbare Gelände dunkler. Um dies zu realisieren, müssen wir die Sichtbarkeit der Zelle verfolgen und entsprechend rendern.

Die einfachste Möglichkeit, das Erscheinungsbild ausgeblendeter Zellen zu ändern, besteht darin, den Netzdaten eine Sichtbarkeitsmetrik hinzuzufügen. Gleichzeitig müssen wir jedoch eine neue Relieftriangulation mit einer Änderung der Sichtbarkeit beginnen. Dies ist eine schlechte Entscheidung, da sich die Sichtbarkeit während des Spiels ständig ändert.

Oft wird die Technik des Renderns über der Topographie einer durchscheinenden Oberfläche verwendet, bei der für den Spieler unsichtbare Zellen teilweise maskiert werden. Diese Methode eignet sich für relativ flaches Gelände in Kombination mit einem begrenzten Betrachtungswinkel. Da unser Gelände jedoch sehr unterschiedliche Höhen und Objekte enthalten kann, die aus verschiedenen Winkeln betrachtet werden können, benötigen wir hierfür ein sehr detailliertes Netz, das der Form des Geländes entspricht. Diese Methode ist teurer als der oben erwähnte einfachste Ansatz.

Ein anderer Ansatz besteht darin, die Daten der Zellen beim Rendern getrennt vom Reliefnetz an den Shader zu übertragen. Dadurch können wir die Triangulation nur einmal durchführen. Zellendaten können mit Textur übertragen werden. Das Ändern der Textur ist viel einfacher als das Triangulieren des Geländes. Darüber hinaus ist das Ausführen mehrerer zusätzlicher Texturmuster schneller als das Rendern einer einzelnen durchscheinenden Ebene.

Was ist mit Shader-Arrays?
Sie können Zellendaten auch mithilfe eines Vektorarrays an den Shader übertragen. Shader-Arrays haben jedoch eine Größenbeschränkung, die in Tausenden von Bytes gemessen wird, und Texturen können Millionen von Pixeln enthalten. Zur Unterstützung großer Karten verwenden wir Texturen.

Zelldatenverwaltung


Wir brauchen eine Möglichkeit, die Textur zu steuern, die die Zelldaten enthält. Erstellen wir eine neue HexCellShaderData Komponente, die dies HexCellShaderData .

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

Beim Erstellen oder Laden einer neuen Karte müssen wir eine neue Textur mit der richtigen Größe erstellen. Daher fügen wir eine Initialisierungsmethode hinzu, die eine Textur erstellt. Wir verwenden eine RGBA-Textur ohne Mip-Texturen und linearen Farbraum. Wir müssen keine Zelldaten mischen, daher verwenden wir die Punktfilterung. Außerdem sollten Daten nicht reduziert werden. Jedes Pixel in der Textur enthält Daten aus einer Zelle.

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

Sollte die Texturgröße mit der Größe der Karte übereinstimmen?
Nein, es müssen nur genügend Pixel vorhanden sein, um alle Zellen aufzunehmen. Bei exakter Übereinstimmung mit der Größe der Karte wird höchstwahrscheinlich eine Textur mit Größen erstellt, die keine Zweierpotenzen sind (Nicht-Zweierpotenzen, NPOT), und dieses Texturformat ist nicht das effektivste. Obwohl wir den Code so konfigurieren können, dass er mit Texturen der Größe einer Zweierpotenz arbeitet, ist dies eine geringfügige Optimierung, die den Zugriff auf Zellendaten erschwert.

Tatsächlich müssen wir nicht jedes Mal eine neue Textur erstellen, wenn wir eine neue Karte erstellen. Es reicht aus, die Größe der Textur zu ändern, wenn sie bereits vorhanden ist. Wir müssen nicht einmal überprüfen, ob wir bereits die richtige Größe haben, da Texture2D.Resize intelligent genug ist, um dies für uns zu tun.

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

Anstatt die Zelldaten pixelweise anzuwenden, verwenden wir einen Farbpuffer und wenden die Daten aller Zellen gleichzeitig an. Dazu verwenden wir das Color32 Array. Bei Bedarf erstellen wir am Ende von Initialize eine neue Array-Instanz. Wenn wir bereits ein Array mit der richtigen Größe haben. dann löschen wir seinen Inhalt.

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

Was ist color32?
Standardmäßige unkomprimierte RGBA-Texturen enthalten 4-Byte-Pixel. Jeder der vier Farbkanäle empfängt ein Byte, dh sie haben 256 mögliche Werte. Bei Verwendung der Unity Color Struktur werden die Gleitkommakomponenten im Intervall 0–1 im Intervall 0–255 in Bytes konvertiert. Beim Abtasten führt die GPU die inverse Transformation durch.

Die Color32 Struktur arbeitet direkt mit Bytes, sodass sie weniger Platz beanspruchen und keine Konvertierung erfordern, was die Effizienz ihrer Verwendung erhöht. Da wir Zellendaten anstelle von Farben speichern, ist es logischer, direkt mit Rohtexturdaten und nicht mit Color .

HexGrid sollte sich mit der Erstellung und Initialisierung dieser Zellen im Shader befassen. Daher fügen wir ihm ein cellShaderData Feld hinzu und erstellen eine Komponente in Awake .

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

Beim Erstellen einer neuen Karte sollte auch 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; } 

Zellendaten bearbeiten


Bisher war es beim Ändern der Eigenschaften einer Zelle erforderlich, ein oder mehrere Fragmente zu aktualisieren. Jetzt müssen möglicherweise die Daten der Zellen aktualisiert werden. Dies bedeutet, dass Zellen eine Verknüpfung zu den Zellendaten im Shader haben müssen. HexCell dazu HexCell eine Eigenschaft HexCell .

  public HexCellShaderData ShaderData { get; set; } 

In HexGrid.CreateCell weisen wir dieser Eigenschaft eine Shader-Datenkomponente zu.

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

Jetzt können wir Zellen dazu bringen, ihre Shader-Daten zu aktualisieren. Während wir die Sichtbarkeit nicht verfolgen, können wir Shader-Daten für etwas anderes verwenden. Der Relieftyp der Zelle bestimmt die Textur, mit der sie gerendert wird. Dies hat keinen Einfluss auf die Geometrie der Zelle, sodass wir den Elevationstypindex in den Zellendaten und nicht in den Netzdaten speichern können. Auf diese Weise können wir die Notwendigkeit einer Triangulation beseitigen, wenn wir die Art des Reliefs der Zelle ändern.

Fügen Sie HexCellShaderData eine HexCellShaderData Methode RefreshTerrain , um diese Aufgabe für eine bestimmte Zelle zu vereinfachen. Lassen wir diese Methode vorerst leer.

  public void RefreshTerrain (HexCell cell) { } 

Ändern Sie HexCell.TerrainTypeIndex so, dass diese Methode HexCell.TerrainTypeIndex wird und die Fragmente nicht aktualisiert werden sollen.

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

Wir werden es auch in HexCell.Load nachdem wir den Typ der HexCell.Load erhalten haben.

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

Zellindex


Um diese Zellen zu ändern, müssen wir den Index der Zelle kennen. Der einfachste Weg, dies zu tun, besteht darin, die Index Eigenschaft zu HexCell . Es zeigt den Index der Zelle in der Liste der Zellen in der Karte an, der dem Index in den angegebenen Zellen im Shader entspricht.

  public int Index { get; set; } 

Dieser Index befindet sich bereits in HexGrid.CreateCell also einfach der erstellten Zelle zu.

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

Jetzt kann HexCellShaderData.RefreshTerrain diesen Index verwenden, um HexCellShaderData.RefreshTerrain anzugeben. Speichern Sie den Elevationstypindex in der Alpha-Komponente des Pixels, indem Sie den Typ einfach in Byte konvertieren. Dies wird bis zu 256 Geländetypen unterstützen, was für uns ausreichen wird.

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

Um Daten auf eine Textur anzuwenden und an die GPU zu übergeben, müssen Sie Texture2D.SetPixels32 und dann Texture2D.Apply . Wie bei Fragmenten werden wir diese Vorgänge auf LateUpdate sodass sie unabhängig von der Anzahl der geänderten Zellen nicht öfter als einmal pro Frame ausgeführt werden können.

  public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; } 

Aktivieren Sie die Komponente nach der Initialisierung, um sicherzustellen, dass die Daten nach dem Erstellen einer neuen Karte aktualisiert werden.

  public void Initialize (int x, int z) { … enabled = true; } 

Triangulation von Zellindizes


Da wir jetzt den Elevationstypindex in diesen Zellen speichern, müssen wir sie nicht mehr in den Triangulationsprozess einbeziehen. Um jedoch Zellendaten verwenden zu können, muss der Shader wissen, welche Indizes verwendet werden sollen. Daher müssen Sie Zellindizes in den Netzdaten speichern und die Höhenindizes ersetzen. Außerdem benötigen wir noch den Farbkanal des Netzes, um Zellen zu mischen, wenn diese Zellen verwendet werden.

Wir HexMesh veralteten allgemeinen Felder useColors und useTerrainTypes . Ersetzen Sie sie durch ein useCellData Feld.

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

Wir überarbeiten die Umbenennung der cellIndices Liste in cellIndices . Lassen Sie uns auch colors in cellWeights umbenennen - dieser Name ist besser 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; 

Ändern Sie Clear damit bei Verwendung dieser Zellen zwei Listen zusammen und nicht getrennt angezeigt werden.

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

Führen Sie dieselbe Gruppierung in 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; } } 

Entfernen AddTriangleColor AddTriangleTerrainTypes AddTriangleColor und AddTriangleTerrainTypes . Ersetzen Sie sie durch die entsprechenden AddTriangleCellData Methoden, die jeweils Indizes und Gewichte hinzufügen.

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

Machen Sie dasselbe in der entsprechenden AddQuad Methode.

  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 Refactoring


Zu diesem Zeitpunkt werden in HexGridChunk viele Compilerfehler HexGridChunk , die HexGridChunk . Aus Gründen der Konsistenz werden statische Farben zunächst in Gewichte umbenannt.

  static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f); 

Beginnen wir mit der Korrektur von TriangulateEdgeFan . Früher brauchte er einen Typ, jetzt braucht er einen Zellindex. AddTriangleColor Code AddTriangleColor und AddTriangleTerrainTypes durch den entsprechenden Code 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); } 

Diese Methode wird an mehreren Stellen aufgerufen. Lassen Sie uns sie durchgehen und sicherstellen, dass der Index der Zelle dorthin übertragen wird und nicht die Art des Geländes.

  TriangulateEdgeFan(center, e, cell.Index); 

Als nächstes kommt TriangulateEdgeStrip . Hier ist alles etwas komplizierter, aber wir verwenden den gleichen Ansatz. w1 w2 die Parameternamen c1 und c2 in w1 und 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); } } 

Ändern Sie die Aufrufe dieser Methode so, dass der Zellenindex an sie übergeben wird. Wir halten auch die Variablennamen konsistent.

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

Nun kommen wir zu den Winkelmethoden. Diese Änderungen sind einfach, müssen jedoch in einer großen Menge Code vorgenommen werden. Zuerst bei 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); } 

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

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

Und etwas anders in 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); } } 

Die beiden vorherigen Methoden verwenden das TriangulateBoundaryTriangle , das ebenfalls aktualisiert werden muss.

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

Die letzte Methode, die geändert werden muss, ist 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); … } 

Damit alles funktioniert, müssen wir angeben, dass wir die Zelldaten für das untergeordnete Element des Reliefs des vorgefertigten Fragments verwenden.


Das Relief verwendet Zelldaten.

Zu diesem Zeitpunkt enthält das Netz Zellindizes anstelle von Höhenindizes. Da der Elevation Shader sie immer noch als Elevation-Indizes interpretiert, werden wir sehen, dass die erste Zelle mit der ersten Textur usw. gerendert wird, bis die letzte Relief-Textur erreicht ist.


Verwenden von Zellindizes als Höhentexturindizes.

Ich kann den überarbeiteten Code nicht zum Laufen bringen. Was mache ich falsch?
Zu einer Zeit haben wir eine große Menge an Triangulationscode geändert, sodass eine hohe Wahrscheinlichkeit für Fehler oder Versehen besteht. Wenn Sie den Fehler nicht finden können, laden Sie das Paket aus diesem Abschnitt herunter und extrahieren Sie die entsprechenden Dateien. Sie können sie in ein separates Projekt importieren und mit Ihrem eigenen Code vergleichen.

Übertragen Sie Zellendaten an einen Shader


Um diese Zellen verwenden zu können, muss der Terrain Shader Zugriff darauf haben. Dies kann über die Shader-Eigenschaft implementiert werden. Dazu muss HexCellShaderData die Materialeigenschaft des Reliefs HexCellShaderData . Oder wir können die Textur dieser Zellen für alle Shader global sichtbar machen. Dies ist praktisch, da wir es in mehreren Shadern benötigen, sodass wir diesen Ansatz verwenden werden.

Rufen Sie nach dem Erstellen der Shader.SetGlobalTexture die statische Shader.SetGlobalTexture Methode auf, um sie global als _HexCellData sichtbar zu machen .

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

Bei Verwendung der Shader-Eigenschaft stellt Unity dem Shader die Texturgröße über die Variable texturName_TexelSize zur Verfügung . Dies ist ein Vier-Komponenten-Vektorisierer, der Werte enthält, die invers zur Breite und Höhe sowie zur Breite und Höhe selbst sind. Beim Festlegen der globalen Textur wird dies jedoch nicht ausgeführt. Daher werden wir es selbst mit dem Shader.SetGlobalVector nachdem wir die Shader.SetGlobalVector erstellt oder ihre 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) ); 

Shader-Datenzugriff


Erstellen Sie eine neue Shader-Include-Datei im Materialordner mit dem Namen HexCellData . Darin definieren wir Variablen für Informationen über die Textur und Größe dieser Zellen. Wir erstellen auch eine Funktion, um die Zellendaten für die angegebenen Scheitelpunktnetzdaten abzurufen.

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


Neue Include-Datei.

v.texcoord2 werden in v.texcoord2 gespeichert, wie dies bei Geländetypen der Fall war. Beginnen wir mit dem ersten Index - v.texcoord2.x . Leider können wir den Index nicht direkt verwenden, um die Textur dieser Zellen abzutasten. Wir müssen es in UV-Koordinaten umwandeln.

Der erste Schritt beim Erstellen der U-Koordinate besteht darin, den Zellenindex durch die Breite der Textur zu teilen. Wir können dies tun, indem wir es mit _HexCellData_TexelSize.x multiplizieren.

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

Das Ergebnis ist eine Zahl in der Form ZU, wobei Z der Zeilenindex und U die Koordinate der U-Zelle ist. Wir können die Zeichenfolge extrahieren, indem wir die Zahl abrunden und dann von der Zahl subtrahieren, um die U-Koordinate zu erhalten.

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

Die V-Koordinate teilt die Linie durch die Höhe der Textur.

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

Da wir die Textur abtasten, müssen wir die Koordinaten in der Mitte der Pixel verwenden, nicht an ihren Kanten. Auf diese Weise garantieren wir, dass die richtigen Pixel abgetastet werden. Fügen Sie daher nach dem Teilen durch die Größe der Textur ½ hinzu.

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

Dies gibt uns die richtigen UV-Koordinaten für den Index der ersten Zelle, die in den Scheitelpunktdaten gespeichert ist. Aber obendrein können wir bis zu drei verschiedene Indizes haben. Daher werden wir dafür sorgen, dass es GetCellDatafür jeden Index funktioniert. Fügen Sie einen ganzzahligen Parameter hinzu index, mit dem wir auf die Vektorkomponente mit dem Zellenindex zugreifen.

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

Nachdem wir alle erforderlichen Koordinaten für diese Zellen haben, können wir eine Stichprobe erstellen _HexCellData. Da wir die Textur im Vertex-Programm abtasten, müssen wir dem Shader explizit mitteilen, welche Mip-Textur verwendet werden soll. Dies kann mit einer Funktion erfolgen tex2Dlod, die die Koordinaten von vier Texturen erfordert. Da diese Zellen keine Mip-Texturen haben, weisen wir den zusätzlichen Koordinaten Nullwerte zu.

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

Die vierte Datenkomponente enthält einen Höhenindex, den wir direkt als Bytes speichern. Die GPU konvertierte sie jedoch automatisch in einen Gleitkommawert im Bereich von 0 bis 1. Um es wieder in den richtigen Wert zu konvertieren, multiplizieren Sie es mit 255. Danach können Sie die Daten zurückgeben.

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

Um diese Funktionalität zu nutzen, aktivieren Sie HexCellData im Terrain- Shader . Da ich diesen Shader in Materials / Terrain platziert habe , muss ich den relativen Pfad ../HexCellData.cginc verwenden .

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

Im Vertex-Programm erhalten wir Zelldaten für alle drei in den Vertex-Daten gespeicherten Zellindizes. Weisen Sie dann data.terrainihre Höhenindizes zu.

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

Zu diesem Zeitpunkt zeigte die Karte wieder das richtige Gelände an. Der große Unterschied besteht darin, dass das Bearbeiten nur von Geländetypen nicht mehr zu neuen Triangulationen führt. Wenn während der Bearbeitung andere Zellendaten geändert werden, wird die Triangulation wie gewohnt durchgeführt.

Einheitspaket

Sichtbarkeit


Nachdem wir die Basis dieser Zellen erstellt haben, können wir fortfahren, um die Sichtbarkeit zu unterstützen. Dazu verwenden wir den Shader, die Zellen selbst und die Objekte, die die Sichtbarkeit bestimmen. Beachten Sie, dass der Triangulationsprozess absolut nichts darüber weiß.

Shader


Beginnen wir damit, dem Terrain- Shader die Sichtbarkeit zu erläutern. Es empfängt Sichtbarkeitsdaten vom Scheitelpunktprogramm und übergibt sie unter Verwendung der Struktur an das Fragmentprogramm Input. Da wir drei separate Höhenindizes übergeben, übergeben wir auch drei Sichtbarkeitswerte.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 visibility; }; 

Um die Sichtbarkeit zu speichern, verwenden wir die erste Komponente dieser Zellen.

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

Eine Sichtbarkeit von 0 bedeutet, dass die Zelle derzeit unsichtbar ist. Wenn es sichtbar wäre, hätte es den Wert von Sichtbarkeit 1. Daher können wir das Gelände abdunkeln, indem wir das Ergebnis GetTerrainColormit dem entsprechenden Sichtbarkeitsvektor multiplizieren . Daher modulieren wir die Relieffarbe jeder gemischten Zelle individuell.

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


Die Zellen wurden schwarz.

Können wir nicht stattdessen die Sichtbarkeit in einem Vertex-Programm kombinieren?
, . . . , . , .

Vollständige Dunkelheit ist eine Büste für vorübergehend unsichtbare Zellen. Damit wir das Relief noch sehen können, müssen wir den Indikator für versteckte Zellen erhöhen. Wechseln wir von 0–1 zu ¼ - 1, was mit der Funktion lerpam Ende des Scheitelpunktprogramms erfolgen kann.

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


Schattierte Zellen.

Verfolgung der Sichtbarkeit von Zellen


Damit die Sichtbarkeit funktioniert, müssen die Zellen ihre Sichtbarkeit verfolgen. Aber wie bestimmt eine Zelle, ob sie sichtbar ist? Wir können dies tun, indem wir die Anzahl der Entitäten verfolgen, die es sehen. Wenn jemand eine Zelle sieht, muss er diese Zelle melden. Und wenn jemand aufhört, die Zelle zu sehen, muss er sie auch darüber informieren. Die Zelle verfolgt einfach die Anzahl der Beobachter, unabhängig davon, um welche Entitäten es sich handelt. Wenn eine Zelle einen Sichtbarkeitswert von mindestens 1 hat, ist sie sichtbar, andernfalls ist sie unsichtbar. Um dieses Verhalten zu implementieren, fügen wir der HexCellVariablen zwei Methoden und eine Eigenschaft hinzu.

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

Fügen Sie als Nächstes der HexCellShaderDataMethode hinzu RefreshVisibility, die genau das Gleiche wie RefreshTerrainaus Gründen der Sichtbarkeit tut . Speichern Sie die Daten in der Komponente R der Datenzellen. Da wir mit Bytes arbeiten, die in die Werte 0–1 konvertiert werden, geben wir die Sichtbarkeit an (byte)255.

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

Wir werden diese Methode mit zunehmender und abnehmender Sichtbarkeit aufrufen und den Wert zwischen 0 und 1 ändern.

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

Squad-Sichtbarkeit schaffen


Machen wir es so, dass die Einheiten die Zelle sehen können, die sie besetzen. Dies wird durch einen Anruf IncreaseVisibilityan den neuen Standort des Geräts während der Aufgabe erreicht HexUnit.Location. Wir rufen auch nach dem alten Standort (falls vorhanden) 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; } } 


Einheiten können sehen, wo sie sind.

Endlich haben wir Sichtbarkeit genutzt! Beim Hinzufügen zu einer Karte machen Einheiten ihre Zelle sichtbar. Außerdem wird ihr Umfang teleportiert, wenn sie an ihren neuen Standort umziehen. Ihr Umfang bleibt jedoch aktiv, wenn Einheiten von der Karte entfernt werden. Um dies zu beheben, reduzieren wir die Sichtbarkeit ihres Standorts, wenn Einheiten zerstört werden.

  public void Die () { if (location) { location.DecreaseVisibility(); } location.Unit = null; Destroy(gameObject); } 

Sichtbereich


Bisher sehen wir nur die Zelle, in der sich die Ablösung befindet, und dies schränkt die Möglichkeiten ein. Zumindest müssen wir benachbarte Zellen sehen. Im allgemeinen Fall können Einheiten alle Zellen in einem bestimmten Abstand sehen, der von der Einheit abhängt.

Fügen wir der HexGridMethode hinzu, um alle von einer Zelle aus sichtbaren Zellen unter Berücksichtigung des Bereichs zu finden. Wir können diese Methode durch Duplizieren und Ändern erstellen Search. Ändern Sie die Parameter und geben Sie eine Liste der Zellen zurück, für die Sie den Listenpool verwenden können.

Bei jeder Iteration wird die aktuelle Zelle zur Liste hinzugefügt. Es gibt keine endgültige Zelle mehr, sodass die Suche niemals endet, wenn sie diesen Punkt erreicht. Wir werden auch die Logik der Umzüge und die Kosten des Umzugs los. Machen Sie die EigenschaftenPathFromSie wurden nicht mehr gefragt, weil wir sie nicht brauchen und wir den Weg entlang des Gitters nicht stören wollen.

Bei jedem Schritt erhöht sich der Abstand einfach um 1. Wenn er den Bereich überschreitet, wird diese Zelle übersprungen. Und wir brauchen keine Suchheuristik, also initialisieren wir sie mit einem Wert von 0. Das heißt, wir sind im Wesentlichen zum Dijkstra-Algorithmus zurückgekehrt.

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

Können wir nicht einen einfacheren Algorithmus verwenden, um alle Zellen in Reichweite zu finden?
, , .

Fügen Sie auch HexGridMethoden IncreaseVisibilityund hinzu DecreaseVisibility. Sie erhalten die Zelle und den Bereich, nehmen eine Liste der entsprechenden Zellen und erhöhen / verringern ihre Sichtbarkeit. Wenn Sie fertig sind, sollten Sie die Liste wieder in ihren Pool zurückbringen.

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

Um diese Methoden verwenden zu können, HexUnitmuss auf das Raster zugegriffen werden. Fügen Sie ihm daher eine Eigenschaft hinzu Grid.

  public HexGrid Grid { get; set; } 

Wenn Sie einem Raster einen Trupp hinzufügen, wird dieser Eigenschaft ein Raster zugewiesen 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; } 

Zunächst reicht ein Sichtbarkeitsbereich von drei Zellen aus. Dazu fügen wir die HexUnitKonstante hinzu, die sich in Zukunft immer in eine Variable verwandeln kann. Dann werden wir den Trupp dazu bringen, Methoden für das Gitter aufzurufen IncreaseVisibilityund DecreaseVisibilityauch seinen Sichtbarkeitsbereich zu übertragen, und nicht nur an diesen Ort gehen.

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


Einheiten mit Sichtbereich, die sich überlappen können.

Sichtbarkeit beim Bewegen


Im Moment wird der Sichtbereich des Trupps nach dem Bewegungsbefehl sofort zum Endpunkt teleportiert. Es hätte besser ausgesehen, wenn sich das Gerät und sein Sichtfeld zusammen bewegt hätten. Der erste Schritt dazu ist, dass wir die Eigenschaft Locationc nicht mehr festlegen HexUnit.Travel. Stattdessen ändern wir das Feld direkt locationund vermeiden den Eigenschaftscode. Daher werden wir den alten Standort manuell löschen und einen neuen Standort konfigurieren. Die Sicht bleibt unverändert.

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

Innerhalb von Coroutinen TravelPathreduzieren wir die Sichtbarkeit der ersten Zelle erst nach Fertigstellung LookAt. Danach, bevor wir zu einer neuen Zelle wechseln, erhöhen wir die Sichtbarkeit dieser Zelle. Nachdem wir damit fertig sind, reduzieren wir erneut die Sichtbarkeit. Erhöhen Sie schließlich die Sichtbarkeit der letzten Zelle.

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


Sichtbarkeit in Bewegung.

All dies funktioniert, außer wenn in dem Moment, in dem sich die Abteilung bewegt, ein neuer Befehl erteilt wird. Dies führt zu einer Teleportation, die auch für die Sichtbarkeit gelten sollte. Um dies zu realisieren, müssen wir den aktuellen Standort des Teams verfolgen, während wir uns bewegen.

  HexCell location, currentTravelLocation; 

Wir werden diesen Ort jedes Mal aktualisieren, wenn wir während der Bewegung eine neue Zelle treffen, bis der Trupp die letzte Zelle erreicht. Dann muss es zurückgesetzt werden.

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

Nach Abschluss der Abzweigung können TravelPathwir nun überprüfen, ob der alte Zwischenort des Pfades bekannt ist. Wenn ja, müssen Sie die Sichtbarkeit in dieser Zelle und nicht am Anfang des Pfads verringern.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility( currentTravelLocation ? currentTravelLocation : pathToTravel[0], visionRange ); … } 

Wir müssen auch die Sichtbarkeit nach der Neukompilierung korrigieren, die während der Bewegung des Trupps aufgetreten ist. Wenn der Zwischenort noch bekannt ist, verringern Sie die Sichtbarkeit darin und erhöhen Sie die Sichtbarkeit am Endpunkt. Setzen Sie dann den Zwischenort zurück.

  void OnEnable () { if (location) { transform.localPosition = location.Position; if (currentTravelLocation) { Grid.IncreaseVisibility(location, visionRange); Grid.DecreaseVisibility(currentTravelLocation, visionRange); currentTravelLocation = null; } } } 

Einheitspaket

Sichtbarkeit von Straßen und Wasser


Obwohl Änderungen der Relieffarbe auf der Sichtbarkeit basieren, hat dies keine Auswirkungen auf Straßen und Wasser. Sie sehen für unsichtbare Zellen zu hell aus. Um die Sichtbarkeit auf Straßen und Wasser anzuwenden, müssen wir Zellindizes hinzufügen und Gewichte zu ihren Netzdaten mischen. Daher werden wir die Kinder der Use Cell Data für die Flüsse , Straßen , Wasser , Water Shore und Mündungen des Fertighausfragments überprüfen.

Straßen


Wir werden von den Straßen starten. Die Methode wird HexGridChunk.TriangulateRoadEdgeverwendet, um einen kleinen Teil der Straße in der Mitte der Zelle zu erstellen, sodass ein Zellenindex erforderlich ist. Fügen Sie einen Parameter hinzu und generieren Sie Zellendaten für das Dreieck.

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

Eine andere einfache Möglichkeit, Straßen zu erstellen, ist TriangulateRoadSegment. Es wird sowohl innerhalb als auch zwischen Zellen verwendet, daher sollte es mit zwei verschiedenen Indizes funktionieren. Hierzu ist es zweckmäßig, den Indexvektorparameter zu verwenden. Da Straßensegmente Teile von Leisten sein können, müssen auch Gewichte durch Parameter geführt werden.

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

Fahren wir nun mit fort TriangulateRoad, wodurch Straßen innerhalb der Zellen erstellt werden. Es benötigt auch einen Indexparameter. Er übergibt diese Daten an die von ihm aufgerufenen Straßenmethoden und fügt sie den von ihm erstellten Dreiecken hinzu.

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

Es bleibt die erforderliche Verfahren Argumente hinzuzufügen TriangulateRoad, TriangulateRoadEdgeund TriangulateRoadSegmentall Compiler - Fehler zu korrigieren.

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

Jetzt sind die Netzdaten korrekt und wir werden mit dem Road Shader fortfahren . Es benötigt ein Vertex-Programm und muss HexCellData enthalten .

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

Da wir nicht mehrere Materialien mischen, reicht es aus, einen Indikator für die Sichtbarkeit in das Fragmentprogramm zu übergeben.

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

Es reicht aus, wenn ein neues Vertex-Programm Daten von zwei Zellen empfängt. Wir mischen sofort ihre Sichtbarkeit, passen sie an und ergänzen die Ausgabe.

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

Im Fragmentprogramm müssen wir der Farbe nur Sichtbarkeit hinzufügen.

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


Straßen mit Sichtbarkeit.

Offenes Wasser


Es mag den Anschein haben, dass die Sicht das Wasser bereits beeinträchtigt hat, aber dies ist nur die Oberfläche eines in Wasser getauchten Geländes. Beginnen wir mit der Sichtbarkeit auf offenes Wasser. Dafür müssen wir uns ändern 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 ); } } } 

Wir müssen auch den Fächern der Dreiecke in Küstennähe Zelldaten hinzufügen.

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

Der Wasser- Shader muss auf die gleiche Weise wie der Road- Shader geändert werden , muss jedoch die Sichtbarkeit von nicht zwei, sondern drei Zellen kombinieren.

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


Offenes Wasser mit Sicht.

Küste und Mündung


Um die Küste zu unterstützen, müssen wir uns wieder ändern HexGridChunk.TriangulateWaterShore. Wir haben bereits einen Indexvektor erstellt, aber nur einen Zellindex für offenes Wasser verwendet. Die Küste benötigt auch einen Nachbarindex, ändern Sie also den Code.

  Vector3 indices; // indices.x = indices.y = indices.z = cell.Index; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; 

Fügen Sie die Zellendaten zu den Quads und dem Dreieck der Küste hinzu. Wir übergeben auch die Indizes beim Aufruf 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 ); } 

Fügen Sie TriangulateEstuarydiese Zellen für die Küste und den Mund hinzu und kümmern Sie sich um sie. Vergessen Sie nicht, dass der Mund aus Trapez mit zwei Dreiecken der Küste an den Seiten besteht. Wir sorgen dafür, dass die Gewichte in der richtigen Reihenfolge übertragen werden.

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

Im WaterShore- Shader müssen Sie dieselben Änderungen wie im Water Shader vornehmen und die Sichtbarkeit der drei Zellen mischen.

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

Der Estuary- Shader mischt die Sichtbarkeit von zwei Zellen, genau wie der Road- Shader . Er hat bereits ein Scheitelpunktprogramm, weil wir ihn brauchen, um die UV-Koordinaten der Flüsse zu übertragen.

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


Küste und Mündung mit Sichtbarkeit.

Flüsse


Die letzten Wasserregionen, mit denen gearbeitet wird, sind die Flüsse. Fügen Sie dem HexGridChunk.TriangulateRiverQuadParameter einen Indexvektor hinzu und fügen Sie ihn dem Netz hinzu, damit die Sichtbarkeit von zwei Zellen erhalten bleibt.

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

TriangulateWithRiverBeginOrEndErstellt Flussendpunkte mit einem Quad und einem Dreieck in der Mitte der Zelle. Fügen Sie dazu die notwendigen Zellendaten hinzu.

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

Wir haben diese Zellindizes bereits in TriangulateWithRiver, also geben wir sie einfach beim Anruf weiter 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 ); } } 

Wir fügen auch Wasserfällen, die in tiefes Wasser münden, Indexunterstützung hinzu.

  void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); } 

Und schließlich ändern Sie es TriangulateConnectionso, dass es die erforderlichen Indizes an die Methoden von Flüssen und Wasserfällen weitergibt.

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

Der River- Shader muss dieselben Änderungen vornehmen wie der Road- Shader .

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


Flüsse mit Sichtbarkeit.

Einheitspaket

Objekte und Sichtbarkeit


Jetzt funktioniert die Sichtbarkeit für das gesamte prozedural generierte Gelände, hat jedoch bisher keine Auswirkungen auf die Geländemerkmale. Gebäude, Farmen und Bäume werden aus Fertighäusern und nicht aus prozeduraler Geometrie erstellt. Daher können wir keine Zellindizes hinzufügen und Gewichte mit ihren Scheitelpunkten mischen. Da jedes dieser Objekte nur zu einer Zelle gehört, müssen wir bestimmen, in welcher Zelle sie sich befinden. Wenn wir dies tun können, erhalten wir Zugriff auf die Daten der entsprechenden Zellen und wenden die Sichtbarkeit an.

Wir können die XZ-Positionen der Welt bereits in Zellindizes umwandeln. Diese Transformation wurde verwendet, um das Gelände zu bearbeiten und Trupps zu verwalten. Der entsprechende Code ist jedoch nicht trivial. Es verwendet ganzzahlige Operationen und erfordert Logik, um mit Kanten zu arbeiten. Dies ist für einen Shader unpraktisch, daher können wir den Großteil der Logik in einer Textur backen und verwenden.

Wir verwenden bereits eine Textur mit einem sechseckigen Muster, um das Gitter über die Topographie zu projizieren. Diese Textur definiert eine Zellfläche von 2 × 2. Daher können wir leicht berechnen, in welchem ​​Gebiet wir uns befinden. Danach können Sie eine Textur mit X- und Z-Offsets für die Zellen in diesem Bereich anwenden und diese Daten verwenden, um die Zelle zu berechnen, in der wir uns befinden.

Hier ist eine ähnliche Textur. Der X-Offset wird in seinem roten Kanal und der Z-Offset in seinem grünen Kanal gespeichert. Da es den Bereich von 2 × 2 Zellen abdeckt, benötigen wir Offsets von 0 und 2. Solche Daten können nicht im Farbkanal gespeichert werden, sodass die Offsets um die Hälfte reduziert werden. Wir brauchen keine klaren Kanten der Zellen, daher reicht eine kleine Textur aus.


Die Textur der Gitterkoordinaten.

Fügen Sie dem Projekt eine Textur hinzu. Stellen Sie den Wrap-Modus wie bei der anderen Netzstruktur auf Wiederholen . Wir brauchen keine Vermischung, so dass der Mischmodus den Wert wählen Punkt . Deaktivieren Sie auch die Komprimierung, damit die Daten nicht verzerrt werden. Deaktivieren Sie den sRGB- Modus, damit beim Rendern im linearen Modus keine Farbraumkonvertierungen durchgeführt werden. Und schließlich brauchen wir keine Mip-Texturen.


Optionen zum Importieren von Texturen.

Object Shader mit Sichtbarkeit


Erstellen Sie einen neuen Feature- Shader , um Objekten Sichtbarkeitsunterstützung hinzuzufügen. Dies ist ein einfacher Oberflächen-Shader mit einem Vertex-Programm. Fügen Sie HexCellData hinzu und übergeben Sie die Sichtbarkeitsanzeige an das Fragmentprogramm. Betrachten Sie sie wie gewohnt in Farbe. Der Unterschied besteht darin, dass wir es nicht verwenden GetCellDatakönnen, da die erforderlichen Netzdaten nicht vorhanden sind. Stattdessen haben wir eine Position in der Welt. Lassen Sie die Sichtbarkeit jedoch vorerst gleich 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" } 

Ändern Sie alle Materialien der Objekte so, dass sie den neuen Shader verwenden, und weisen Sie ihnen die Textur der Gitterkoordinaten zu.


Urban mit Mesh-Textur.

Zugriff auf Zellendaten


Um die Textur der Gitterkoordinaten im Scheitelpunktprogramm abzutasten, benötigen wir erneut tex2Dlodeinen vierkomponentigen Texturkoordinatenvektor. Die ersten beiden Koordinaten sind die Position der XZ-Welt. Die anderen beiden sind wie zuvor gleich Null.

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

Wie im Terrain- Shader strecken wir die UV-Koordinaten so, dass die Textur das richtige Seitenverhältnis aufweist, das dem Sechseckgitter entspricht.

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

Wir können herausfinden, in welchem ​​Teil der 2 × 2-Zellen wir uns befinden, indem wir den Wert der abgerundeten UV-Koordinaten nehmen. Dies bildet die Basis für die Koordinaten der Zellen.

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

Um die Koordinaten der Zelle zu finden, in der wir uns befinden, fügen wir die in der Textur gespeicherten Verschiebungen hinzu.

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

Da ein Teil des Gitters 2 × 2 groß ist und die Offsets halbiert sind, müssen wir das Ergebnis verdoppeln, um die endgültigen Koordinaten zu erhalten.

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

Jetzt haben wir die XZ-Koordinaten des Zellgitters, die wir in die UV-Koordinaten dieser Zellen umwandeln müssen. Dies kann erreicht werden, indem einfach in die Mitte der Pixel verschoben und diese dann in Texturgrößen unterteilt werden. Fügen wir also der HexCellData- Include-Datei eine Funktion hinzu , die auch das Sampling übernimmt.

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

Jetzt können wir dies in dem Vertex - Shader - Programm verwenden die Eigenschaft .

  cellDataCoordinates *= 2; data.visibility = GetCellData(cellDataCoordinates).x; data.visibility = lerp(0.25, 1, data.visibility); 


Objekte mit Sichtbarkeit.

Schließlich wirkt sich die Sichtbarkeit auf die gesamte Karte aus, mit Ausnahme der Einheiten, die immer sichtbar sind. Da wir die Sichtbarkeit von Objekten für jeden Scheitelpunkt bestimmen, wird für das Objekt, das die Zellgrenze überschreitet, die Sichtbarkeit der Zellen, die es schließt, gemischt. Die Objekte sind jedoch so klein, dass sie ständig in ihrer Zelle verbleiben, selbst unter Berücksichtigung von Positionsverzerrungen. Einige können jedoch Teil der Eckpunkte in einer anderen Zelle sein. Daher ist unser Ansatz billig, aber unvollkommen. Dies macht sich vor allem bei Wänden bemerkbar, deren Sichtbarkeit zwischen den Sichtbarkeiten benachbarter Zellen variiert.


Wände mit wechselnder Sichtbarkeit.

Da Wandsegmente prozedural generiert werden, können wir ihrem Netz Zelldaten hinzufügen und den Ansatz verwenden, den wir für das Relief verwendet haben. Leider sind die Türme vorgefertigt, so dass wir immer noch Inkonsistenzen haben werden. Im Allgemeinen sieht der vorhandene Ansatz für die von uns verwendete einfache Geometrie gut genug aus. In Zukunft werden wir detailliertere Modelle und Wände betrachten, daher werden wir die Methode zum Mischen ihrer Sichtbarkeit verbessern.

Einheitspaket

Teil 21: Kartenforschung


  • Wir zeigen alles während der Bearbeitung an.
  • Wir verfolgen die untersuchten Zellen.
  • Wir verstecken, was noch unbekannt ist.
  • Wir zwingen Einheiten, unerforschte Gebiete zu meiden.

Im vorherigen Teil haben wir den Nebel des Krieges hinzugefügt, den wir nun verfeinern werden, um Kartenforschung durchzuführen.


Wir sind bereit, die Welt zu erkunden.

Zeigen Sie die gesamte Karte im Bearbeitungsmodus an


Die Bedeutung der Studie ist, dass bis die Zellen nicht gesehen werden, sie als unbekannt und daher unsichtbar angesehen werden. Sie sollten nicht verdeckt, aber überhaupt nicht angezeigt werden. Daher werden wir vor dem Hinzufügen von Forschungsunterstützung die Sichtbarkeit im Bearbeitungsmodus aktivieren.

Sichtbarkeitsumschaltung


Mit dem Schlüsselwort können wir steuern, ob die Shader die Sichtbarkeit verwenden, wie dies mit der Überlagerung im Raster geschehen ist. Verwenden Sie das Schlüsselwort HEX_MAP_EDIT_MODE , um den Status des Bearbeitungsmodus anzuzeigen. Da mehrere Shader über dieses Schlüsselwort Bescheid wissen sollten, definieren wir es global mit statischen Methoden Shader.EnableKeyWordund Shader.DisableKeyword. Wir werden die entsprechende Methode aufrufen, HexGameUI.SetEditModewenn wir den Bearbeitungsmodus ändern.

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

Bearbeitungsmodus-Shader


Wenn HEX_MAP_EDIT_MODE definiert ist, ignorieren Shader die Sichtbarkeit. Dies läuft darauf hinaus, dass die Sichtbarkeit von Zellen immer als gleich 1 betrachtet wird. Fügen wir eine Funktion hinzu, um die Daten von Zellen abhängig vom Schlüsselwort am Anfang der HexCellData- Include-Datei zu filtern .

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

Wir durchlaufen diese Funktion als Ergebnis beider Funktionen, GetCellDatabevor wir sie zurückgeben.

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

Damit alles funktioniert, müssen alle relevanten Shader die Anweisung multi_compile erhalten, um Optionen zu erstellen, falls das Schlüsselwort HEX_MAP_EDIT_MODE definiert ist. Fügen Sie den Shadern von Estuary , Feature , River , Road , Terrain , Water und Water Shore zwischen der Zielrichtlinie und der ersten Include-Direktive die entsprechende Linie hinzu .

  #pragma multi_compile _ HEX_MAP_EDIT_MODE 

Wenn Sie jetzt in den Kartenbearbeitungsmodus wechseln, verschwindet der Nebel des Krieges.

Einheitspaket

Zellforschung


Standardmäßig sollten Zellen als unerforscht betrachtet werden. Sie werden erforscht, wenn ein Trupp sie sieht. Danach bleiben sie weiterhin untersucht, ob eine Abteilung sie sehen kann.

Verfolgung des Studienstatus


Um die Überwachung des Status von Studien zu unterstützen, fügen wir die HexCellallgemeine Eigenschaft hinzu IsExplored.

  public bool IsExplored { get; set; } 

Der Stand der Studie wird von der Zelle selbst bestimmt. Daher sollte diese Eigenschaft nur festgelegt werden HexCell. Um diese Einschränkung hinzuzufügen, setzen wir den Setter auf privat.

  public bool IsExplored { get; private set; } 

Wenn die Sichtbarkeit der Zelle zum ersten Mal größer als Null wird, wird die Zelle als untersucht betrachtet. Daher sollte IsExploredein Wert zugewiesen werden true. Tatsächlich reicht es aus, die Zelle einfach als geprüft zu markieren, wenn die Sichtbarkeit auf 1 steigt. Dies muss vor dem Anruf erfolgen RefreshVisibility.

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

Übertragung des Forschungsstatus auf Shader


Wie bei der Sichtbarkeit von Zellen übertragen wir ihren Forschungsstatus über die Shader-Daten auf die Shader. Am Ende ist es nur eine andere Art der Sichtbarkeit. HexCellShaderData.RefreshVisibilityspeichert den Sichtbarkeitsstatus im Datenkanal R. Lassen Sie uns den Stand der Studie in Kanal G-Daten beibehalten.

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

Schwarzes unerforschtes Relief


Jetzt können wir Shader verwenden, um den Stand der Zellforschung zu visualisieren. Um sicherzustellen, dass alles so funktioniert, wie es sollte, machen wir das unerforschte Gelände nur schwarz. Um den Bearbeitungsmodus zu aktivieren, ändern Sie ihn zunächst FilterCellDataso, dass die Forschungsdaten herausgefiltert werden.

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

Der Terrain- Shader übergibt die Sichtbarkeitsdaten aller drei möglichen Zellen an das Fragmentprogramm. Im Falle des Forschungszustands kombinieren wir sie im Vertex-Programm und übertragen den einzigen Wert in das Fragment-Programm. Fügen Sie der Eingabe die visibilityvierte Komponente hinzu, damit wir einen Platz dafür haben.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; }; 

Wenn wir jetzt im Vertex-Programm den Sichtbarkeitsindex ändern, müssen wir explizit darauf zugreifen data.visibility.xyz.

  void vert (inout appdata_full v, out Input data) { … data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); } 

Danach kombinieren wir die Zustände der Studie und schreiben das Ergebnis in data.visibility.w. Dies ähnelt dem Kombinieren der Sichtbarkeit in anderen Shadern, verwendet jedoch die Komponente Y dieser Zellen.

  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; 

Der Forschungsstatus ist jetzt im Fragmentprogramm durch verfügbar IN.visibility.w. Berücksichtigen Sie dies bei der Berechnung der Albedo.

  void surf (Input IN, inout SurfaceOutputStandard o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


Die unerforschte Topographie ist jetzt schwarz.

Das Relief von unerforschten Zellen hat jetzt eine schwarze Farbe. Objekte, Straßen und Wasser sind davon jedoch noch nicht betroffen. Dies reicht jedoch aus, um sicherzustellen, dass die Studie funktioniert.

Forschungsstatus speichern und laden


Nachdem wir die Forschungsunterstützung hinzugefügt haben, müssen wir sicherstellen, dass der Forschungsstatus beim Speichern und Laden von Karten berücksichtigt wird. Daher müssen wir die Version der Kartendateien auf 3 erhöhen. Um diese Änderungen bequemer zu gestalten, fügen wir hierfür eine SaveLoadMenuKonstante hinzu.

  const int mapFileVersion = 3; 

Wir werden diese Konstante beim Schreiben der Dateiversion in Saveund beim Einchecken der Dateisupport verwenden 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); } } } 

Als letzten Schritt HexCell.Savezeichnen wir den Status der Studie auf.

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

Und wir werden es am Ende lesen Load. Danach rufen wir RefreshVisibilityan, falls sich der Stand der Studie vom vorherigen unterscheidet.

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

Um die Abwärtskompatibilität mit alten Sicherungsdateien aufrechtzuerhalten, müssen wir das Lesen des Sicherungsstatus überspringen, wenn die Dateiversion kleiner als 3 ist. In diesem Fall haben die Zellen standardmäßig den Status "unerforscht". Dazu müssen wir LoadHeader-Daten als Parameter hinzufügen .

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

Jetzt HexGrid.Loadmüssen die HexCell.LoadHeader-Daten übergeben werden.

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

Beim Speichern und Laden von Karten wird nun der Erkundungsstatus der Zellen berücksichtigt.

Einheitspaket

Unbekannte Zellen verstecken


Gegenwärtig sind unerforschte Zellen visuell durch ein schwarzes Relief gekennzeichnet. In Wirklichkeit wollen wir, dass diese Zellen unsichtbar sind, weil sie unbekannt sind. Wir können die undurchsichtige Geometrie transparent machen, damit sie nicht sichtbar ist. Das Unity Surface Shader Framework wurde jedoch ohne diese Möglichkeit entwickelt. Anstatt echte Transparenz zu verwenden, werden wir die Shader so ändern, dass sie dem Hintergrund entsprechen, wodurch sie auch unsichtbar werden.

Das Relief wirklich schwarz machen


Obwohl das untersuchte Relief schwarz ist, können wir es immer noch erkennen, da es immer noch spiegelnde Beleuchtung hat. Um die Beleuchtung loszuwerden, müssen wir sie perfekt mattschwarz machen. Um andere Oberflächeneigenschaften nicht zu beeinträchtigen, ist es am einfachsten, die Spiegelfarbe in Schwarz zu ändern. Dies ist möglich, wenn Sie einen Oberflächen-Shader verwenden, der mit Specular funktioniert. Jetzt verwenden wir jedoch das Standard-Metallic. Beginnen wir also damit, den Terrain- Shader auf Specular umzustellen.

Ersetzen Sie die Farbeigenschaft _Metallic auf Eigenschaft _Specular . Standardmäßig sollte der Farbwert gleich (0,2, 0,2, 0,2) sein. Wir garantieren also, dass es zum Erscheinungsbild der Metallic-Version passt.

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

Ändern Sie auch die entsprechenden Shader-Variablen. Die Farbe von spiegelnden Oberflächen-Shadern ist definiert als fixed3, also verwenden wir sie.

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

Ändern Sie die Pragma-Oberflächenbrandung von Standard in StandardSpecular . Dadurch wird Unity gezwungen, Shader mithilfe von Specular zu generieren.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert 

Jetzt benötigt die Funktion surfden zweiten Parameter vom Typ SurfaceOutputStandardSpecular. Außerdem müssen Sie jetzt den Wert nicht zuweisen o.Metallic, sondern 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; } 

Jetzt können wir die Glanzlichter verdecken, indem wir die Spiegelfarbe berücksichtigen explored.

  o.Specular = _Specular * explored; 


Unerforschtes Gelände ohne reflektierte Beleuchtung.

Wie Sie auf dem Bild sehen können, sieht das unerforschte Relief jetzt mattschwarz aus. Bei Betrachtung in einem Tangentenwinkel verwandeln sich die Oberflächen jedoch in einen Spiegel, wodurch das Relief beginnt, die Umgebung, dh die Skybox, zu reflektieren.

Warum werden Oberflächen zu Spiegeln?
. . Rendering .


Unerforschte Gebiete spiegeln immer noch die Umwelt wider.

Um diese Reflexionen loszuwerden, betrachten wir das unerforschte Relief als vollständig schattiert. Dies wird erreicht, indem exploreddem Okklusionsparameter ein Wert zugewiesen wird, den wir als Reflexionsmaske verwenden.

  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; 


Ohne Reflexionen unerforscht.

Passender Hintergrund


Jetzt, da das unerforschte Gelände die gesamte Beleuchtung ignoriert, müssen Sie es an den Hintergrund anpassen. Da unsere Kamera immer von oben schaut, ist der Hintergrund immer grau. Fügen Sie die Eigenschaft _BackgroundColor hinzu , die standardmäßig Schwarz ist, um dem Terrain- Shader mitzuteilen, welche Farbe verwendet werden soll .

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

Um diese Farbe zu verwenden, fügen wir sie als emittierendes Licht hinzu. Dies wird o.Emissionerreicht, indem ein Hintergrundfarbwert multipliziert mit einem minus erkundet zugewiesen wird.

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

Da wir die Standard-Skybox verwenden, ist die sichtbare Hintergrundfarbe tatsächlich nicht dieselbe. Im Allgemeinen wäre ein leicht rötliches Grau die beste Farbe. Beim Einrichten des Reliefmaterials können Sie den Code 68615BFF für Hex Color verwenden .


Reliefmaterial mit grauer Hintergrundfarbe.

Im Allgemeinen funktioniert dies, obwohl Sie, wenn Sie wissen, wo Sie suchen müssen, sehr schwache Silhouetten bemerken werden. Damit der Player sie nicht sehen kann, können Sie der Kamera anstelle der Skybox eine einheitliche Hintergrundfarbe von 68615BFF zuweisen.


Kamera mit einheitlicher Hintergrundfarbe.

Warum nicht die Skybox entfernen?
, , environmental lighting . , .

Jetzt können wir den Unterschied zwischen dem Hintergrund und unerforschten Zellen nicht finden. Eine hohe unerforschte Topographie kann eine niedrige erforschte Topographie bei niedrigen Kamerawinkeln immer noch verdecken. Darüber hinaus werfen unerforschte Teile immer noch Schatten auf die Erkundeten. Diese minimalen Hinweise können jedoch vernachlässigt werden.


Nicht erforschte Zellen sind nicht mehr sichtbar.

Was ist, wenn Sie keine einheitliche Hintergrundfarbe verwenden?
, , . . , . , , , UV- .

Reliefobjekte verstecken


Jetzt haben wir nur noch das Netz des Reliefs versteckt. Der Rest der Studie ist noch nicht betroffen.


Bisher ist nur das Relief verborgen.

Lassen Sie uns den Feature- Shader ändern , der ein undurchsichtiger Shader wie Terrain ist . Verwandeln Sie es in einen Spiegel-Shader und fügen Sie die Hintergrundfarbe hinzu. Beginnen wir mit den Eigenschaften.

  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" {} } 

Weitere Pragma-Oberfläche und Variablen wie zuvor.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; 

visibilityEine weitere Komponente ist ebenfalls erforderlich. Da Feature die Sichtbarkeit für jeden Scheitelpunkt kombiniert, wurde nur ein Gleitkommawert benötigt. Jetzt brauchen wir zwei.

  struct Input { float2 uv_MainTex; float2 visibility; }; 

Ändern Sie es vertso, dass es explizit für die Sichtbarkeitsdaten verwendet wird data.visibility.x, und weisen Sie dann den data.visibility.yWert der Studiendaten zu.

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

Ändern Sie es surfso, dass es die neuen Daten wie Terrain verwendet .

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


Versteckte Reliefobjekte.

Wasser verstecken


Als nächstes kommen die Water- und Water Shore- Shader . Beginnen wir mit der Konvertierung in Spiegel-Shader. Sie benötigen jedoch keine Hintergrundfarbe, da es sich um transparente Shader handelt. Fügen Sie nach

der Konvertierung visibilityeine weitere Komponente hinzu und ändern Sie sie entsprechend vert. Beide Shader kombinieren Daten aus drei Zellen.

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

Wasser und Wasserufer führen surfunterschiedliche Vorgänge aus, stellen jedoch ihre Oberflächeneigenschaften auf dieselbe Weise ein. Da sie transparent sind, werden wir sie exploreim Alphakanal berücksichtigen und keine Emissionen festlegen.

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


Verstecktes Wasser.

Wir verstecken Flussmündungen, Flüsse und Straßen


Wir haben immer noch die Shader für Mündung , Fluss und Straße . Alle drei sind transparent und kombinieren die Daten zweier Zellen. Schalten Sie sie alle auf spiegelnd um und fügen Sie sie dann den visibilityForschungsdaten hinzu.

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

Ändern Sie die Funktion der Flussmündungs- und Fluss-surf Shader so, dass die neuen Daten verwendet werden. Beide müssen die gleichen Änderungen vornehmen.

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

Die Shader Road ist etwas anders, da sie eine zusätzliche Mischmetrik verwendet.

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


Alles ist versteckt.

Einheitspaket

Vermeiden Sie unerforschte Zellen


Obwohl alles Unbekannte visuell verborgen ist, wird der Stand der Studie bei der Suche nach einem Pfad nicht berücksichtigt. Infolgedessen können Einheiten angewiesen werden, sich durch und durch unerforschte Zellen zu bewegen, wodurch auf magische Weise bestimmt wird, in welche Richtung sie sich bewegen sollen. Wir müssen Einheiten zwingen, um unerforschte Zellen zu vermeiden.


Navigieren Sie durch unerforschte Zellen.

Trupps bestimmen die Umzugskosten


Bevor wir unerforschte Zellen in Angriff nehmen, wiederholen wir den Code, um die Kosten für den Wechsel von HexGridnach zu übertragen HexUnit. Dies vereinfacht die Unterstützung für Einheiten mit unterschiedlichen Bewegungsregeln.

Fügen Sie der HexUnitallgemeinen Methode hinzu GetMoveCost, um die Umzugskosten zu bestimmen. Er muss wissen, welche Zellen sich zwischen ihnen bewegen und in welche Richtung. Wir kopieren den entsprechenden Code für die Kosten der Umstellung HexGrid.Searchauf diese Methode und ändern die Variablennamen.

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

Die Methode sollte die Umzugskosten zurückgeben. Ich habe den alten Code verwendet, um ungültige Züge zu überspringen continue, aber dieser Ansatz funktioniert hier nicht. Wenn eine Bewegung nicht möglich ist, erstatten wir die negativen Umzugskosten.

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

Jetzt müssen wir beim Finden des Pfades nicht nur die Geschwindigkeit, sondern auch die ausgewählte Einheit kennen. Ändern Sie entsprechend HexGameUI.DoPathFinding.

  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, selectedUnit); } else { grid.ClearPath(); } } } 

Da wir weiterhin Zugriff auf die Geschwindigkeit des Trupps benötigen, werden wir die HexUnitEigenschaft erweitern Speed. Während es einen konstanten Wert von 24 zurückgibt.

  public int Speed { get { return 24; } } 

Die HexGridVeränderung FindPath, und Searchso , dass sie mit unserem neuen Ansatz arbeiten können.

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

Jetzt werden wir aus dem Searchalten Code entfernen , der bestimmt hat, ob es möglich ist, zur nächsten Zelle zu wechseln, und was die Kosten für den Umzug sind. Stattdessen werden wir anrufen HexUnit.IsValidDestinationund HexUnit.GetMoveCost. Wir werden die Zelle überspringen, wenn die Umzugskosten negativ sind.

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

Umgehen Sie unerforschte Gebiete


Um unerforschte Zellen zu vermeiden, müssen wir sicherstellen, dass wir HexUnit.IsValidDestinationprüfen, ob die Zelle untersucht wird.

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


Weitere Einheiten können nicht zu unerforschten Zellen gelangen.

Da unerforschte Zellen keine gültigen Endpunkte mehr sind, werden sie von Trupps beim Verschieben zum Endpunkt vermieden. Das heißt, unerforschte Gebiete wirken als Barrieren, die den Weg verlängern oder sogar unmöglich machen. Wir müssen die Einheiten näher an ein unbekanntes Gelände bringen, um zuerst die Gegend zu erkunden.

Was ist, wenn während des Umzugs ein kürzerer Pfad angezeigt wird?
. , . .

, , . , .

Einheitspaket

Teil 22: Verbesserte Sichtbarkeit


  • Ändern Sie die Sichtbarkeit reibungslos.
  • Verwenden Sie die Höhe der Zelle, um den Bereich zu bestimmen.
  • Verstecke den Rand der Karte.

Durch Hinzufügen von Unterstützung für die Kartenerkundung werden wir die Berechnungen und Übergänge des Bereichs verbessern.


Um weiter zu sehen, klettere höher.

Sichtbarkeitsübergänge


Die Zelle ist entweder sichtbar oder unsichtbar, da sie sich entweder im Bereich der Ablösung befindet oder nicht. Selbst wenn es so aussieht, als würde eine Einheit einige Zeit brauchen, um sich zwischen Zellen zu bewegen, springt ihr Bereich sofort von Zelle zu Zelle. Infolgedessen ändert sich die Sichtbarkeit der umgebenden Zellen dramatisch. Die Bewegung des Trupps scheint reibungslos zu sein, aber die Sichtbarkeit ändert sich plötzlich.

Idealerweise sollte sich auch die Sichtbarkeit reibungslos ändern. Sobald Sie sich im Sichtfeld befinden, sollten die Zellen allmählich beleuchtet und allmählich dunkel werden. Oder bevorzugen Sie sofortige Übergänge? Fügen wir der HexCellShaderDataEigenschaft hinzu, die sofortige Übergänge wechselt. Standardmäßig sind Übergänge glatt.

  public bool ImmediateMode { get; set; } 

Transition Cell Tracking


Selbst wenn glatte Übergänge angezeigt werden, bleiben die tatsächlichen Sichtbarkeitsdaten binär, dh der Effekt ist nur visuell. Dies bedeutet, dass Sichtbarkeitsübergänge behandelt werden müssen HexCellShaderData. Wir werden ihm eine Liste von Zellen geben, in denen der Übergang durchgeführt wird. Stellen Sie sicher, dass es bei jeder Initialisierung leer ist.

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

Im Moment setzen wir Zellendaten RefreshVisibilitydirekt. Dies ist für den Sofortübergangsmodus immer noch korrekt, aber wenn er deaktiviert ist, müssen wir der Liste der Übergangszellen eine Zelle hinzufügen.

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

Die Sichtbarkeit scheint nicht mehr zu funktionieren, da wir derzeit nichts mit den Zellen in der Liste tun.

Durchlaufen Sie Zellen in einer Schleife


Anstatt die entsprechenden Werte sofort auf 255 oder 0 einzustellen, werden diese Werte schrittweise erhöht / verringert. Die Glätte des Übergangs hängt von der Änderungsrate ab. Es sollte nicht sehr schnell und nicht sehr langsam sein. Ein guter Kompromiss zwischen schönen Übergängen und der Bequemlichkeit des Spiels besteht darin, sich innerhalb einer Sekunde zu ändern. Stellen wir hierfür eine Konstante ein, um das Ändern zu erleichtern.

  const float transitionSpeed = 255f; 

Jetzt LateUpdatekönnen wir das Delta definieren, das auf die Werte angewendet wird. Multiplizieren Sie dazu das Zeitdelta mit der Geschwindigkeit. Es muss eine ganze Zahl sein, da wir nicht wissen, wie groß sie sein kann. Ein starker Abfall der Bildrate kann dazu führen, dass das Delta mehr als 255 beträgt.

Außerdem müssen wir aktualisieren, solange Übergangszellen vorhanden sind. Daher sollte der Code enthalten sein, solange sich etwas in der Liste befindet.

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

Auch theoretisch möglich sehr hohe Bildraten. In Kombination mit einer niedrigen Übergangsgeschwindigkeit kann dies ein Delta von 0 ergeben. Damit die Änderung stattfindet, erzwingen wir, dass das Delta-Minimum 1 ist.

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

Nachdem wir das Delta erhalten haben, können wir alle Übergangszellen durchlaufen und ihre Daten aktualisieren. Nehmen wir an, wir haben eine Methode dafür UpdateCellData, deren Parameter die entsprechende Zelle und das Delta sind.

  int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); } 

Irgendwann sollte der Zellübergang abgeschlossen sein. Angenommen, die Methode gibt Informationen darüber zurück, ob der Übergang noch läuft. Wenn es nicht mehr läuft, können wir die Zelle aus der Liste entfernen. Danach müssen wir den Iterator dekrementieren, um die Zellen nicht zu überspringen.

  for (int i = 0; i < transitioningCells.Count; i++) { if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells.RemoveAt(i--); } } 

Die Reihenfolge, in der die Übergangszellen verarbeitet werden, ist nicht wichtig. Daher müssen wir die Zelle nicht am aktuellen Index löschen, wodurch RemoveAtalle Zellen gezwungen würden, sich danach zu bewegen. Stattdessen verschieben wir die letzte Zelle in den aktuellen Index und löschen dann die letzte.

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

Jetzt müssen wir eine Methode erstellen UpdateCellData. Um seine Arbeit zu erledigen, benötigt er einen Index und Zellendaten. Beginnen wir also damit, diese abzurufen. Es sollte auch festgelegt werden, ob die Aktualisierung der Zelle fortgesetzt werden soll. Standardmäßig gehen wir davon aus, dass dies nicht erforderlich ist. Nach Abschluss der Arbeiten müssen die geänderten Daten übernommen und der Status "Das Update wird fortgesetzt" zurückgegeben.

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

Zellendaten aktualisieren


Zu diesem Zeitpunkt haben wir eine Zelle, die sich im Übergang befindet oder diese bereits fertiggestellt hat. Lassen Sie uns zunächst den Status der Zellsonde überprüfen. Wenn die Zelle untersucht wird, ihr G-Wert jedoch noch nicht 255 beträgt, befindet sie sich im Übergangsprozess, sodass wir dies überwachen werden.

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

Um den Übergang durchzuführen, fügen wir dem G-Wert der Zelle ein Delta hinzu. Arithmetische Operationen funktionieren nicht mit Bytes, sondern werden zuerst in Ganzzahlen konvertiert. Daher hat die Summe das Ganzzahlformat, das in Byte konvertiert werden muss.

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

Vor der Konvertierung müssen Sie jedoch sicherstellen, dass der Wert 255 nicht überschreitet.

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

Als nächstes müssen wir dasselbe für die Sichtbarkeit tun, die den Wert von R verwendet.

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

Da die Zelle wieder unsichtbar werden kann, müssen wir prüfen, ob der Wert von R verringert werden muss. Dies geschieht, wenn die Zelle unsichtbar ist, R jedoch größer als Null ist.

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

Jetzt ist es UpdateCellDatafertig und die Sichtbarkeitsübergänge werden korrekt ausgeführt.


Sichtbarkeitsübergänge.

Schutz vor doppelten Übergangselementen


Übergänge funktionieren, aber möglicherweise werden doppelte Elemente in der Liste angezeigt. Dies geschieht, wenn sich der Sichtbarkeitsstatus der Zelle ändert, während sie sich noch im Übergang befindet. Zum Beispiel, wenn die Zelle während der Bewegung des Trupps nur für kurze Zeit sichtbar ist.

Aufgrund des Auftretens doppelter Elemente wird der Zellenübergang mehrmals pro Frame aktualisiert, was zu schnelleren Übergängen und zusätzlicher Arbeit führt. Wir können dies verhindern, indem wir vor dem Hinzufügen einer Zelle prüfen, ob sie bereits in der Liste enthalten ist. Eine Listensuche bei jedem AnrufRefreshVisibilitykostspielig, insbesondere wenn mehrere Zellübergänge durchgeführt werden. Verwenden wir stattdessen einen anderen Kanal, der noch nicht verwendet wurde, um anzugeben, ob sich die Zelle im Übergangsprozess befindet, z. B. Wert B. Wenn wir der Liste eine Zelle hinzufügen, weisen wir ihr den Wert 255 zu und fügen nur die Zellen hinzu, deren Wert nicht 255 entspricht.

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

Damit dies funktioniert, müssen wir den Wert von B nach Abschluss des Zellübergangs zurücksetzen.

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


Übergänge ohne Duplikate.

Sichtbarkeit sofort laden


Änderungen der Sichtbarkeit sind jetzt immer schrittweise, auch beim Laden einer Karte. Dies ist unlogisch, da die Karte den Zustand beschreibt, in dem die Zellen bereits sichtbar sind, sodass der Übergang hier unangemessen ist. Darüber hinaus kann das Durchführen von Übergängen für die vielen sichtbaren Zellen einer großen Karte das Spiel nach dem Laden verlangsamen. HexGrid.LoadWechseln wir daher vor dem Laden von Zellen und Trupps in den Sofortübergangsmodus.

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

Daher definieren wir die anfängliche Einstellung des Sofortübergangsmodus neu, unabhängig davon, wie er aussieht. Möglicherweise ist es bereits ausgeschaltet oder es wurde eine Konfigurationsoption vorgenommen, sodass wir uns an den Anfangsmodus erinnern und nach Abschluss der Arbeiten darauf umschalten.

  public void Load (BinaryReader reader, int header) { … bool originalImmediateMode = cellShaderData.ImmediateMode; cellShaderData.ImmediateMode = true; … cellShaderData.ImmediateMode = originalImmediateMode; } 

Einheitspaket

Höhenabhängiger Bereich


Bisher haben wir für alle Einheiten einen konstanten Umfang von drei verwendet, aber in Wirklichkeit ist dies komplizierter. Im allgemeinen Fall können wir das Objekt aus zwei Gründen nicht sehen: Entweder hindert uns ein Hindernis daran, es zu sehen, oder das Objekt ist zu klein oder zu weit. In unserem Spiel implementieren wir nur die Bereichsbeschränkung.

Wir können nicht sehen, was sich auf der gegenüberliegenden Seite der Erde befindet, weil der Planet uns verdeckt. Wir können nur bis zum Horizont sehen. Da der Planet ungefähr als Kugel betrachtet werden kann, gilt: Je höher der Blickwinkel, desto mehr Oberfläche können wir sehen, dh der Horizont hängt von der Höhe ab.


Der Horizont hängt von der Höhe des Aussichtspunkts ab.

Die eingeschränkte Sichtbarkeit unserer Einheiten ahmt den Horizonteffekt nach, der durch die Krümmung der Erde erzeugt wird. Die Reichweite ihrer Überprüfung hängt von der Größe des Planeten und dem Maßstab der Karte ab. Zumindest ist das die logische Erklärung. Der Hauptgrund für die Reduzierung des Umfangs ist jedoch das Gameplay. Dies ist eine Einschränkung, die als Nebel des Krieges bezeichnet wird. Wenn wir jedoch die dem Sichtfeld zugrunde liegende Physik verstehen, können wir daraus schließen, dass ein hoher Standpunkt einen strategischen Wert haben sollte, da er sich vom Horizont entfernt und es Ihnen ermöglicht, niedrigere Hindernisse zu betrachten. Aber bisher haben wir es nicht umgesetzt.

Höhe zur Überprüfung


Um die Höhe bei der Bestimmung des Umfangs zu berücksichtigen, müssen wir die Höhe kennen. Dies ist die übliche Höhe oder der übliche Wasserstand, je nachdem, ob es sich um eine Landzelle oder um Wasser handelt. Fügen wir dies der HexCellEigenschaft hinzu.

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

Wenn sich die Höhe jedoch auf den Bereich auswirkt, kann sich bei einer Änderung der Betrachtungshöhe der Zelle auch die Sichtbarkeitssituation ändern. Da die Zelle den Umfang mehrerer Einheiten blockiert hat oder jetzt blockiert, ist es nicht so einfach zu bestimmen, was geändert werden muss. Die Zelle selbst kann dieses Problem nicht lösen. Lassen Sie sie daher eine Änderung der Situation melden HexCellShaderData. Angenommen, Sie HexCellShaderDatahaben eine Methode dafür ViewElevationChanged. Wir werden es bei HexCell.ElevationBedarf bei Abtretung anrufen .

  public int Elevation { get { return elevation; } set { if (elevation == value) { return; } int originalViewElevation = ViewElevation; elevation = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } … } } 

Das gilt auch für WaterLevel.

  public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } int originalViewElevation = ViewElevation; waterLevel = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } ValidateRivers(); Refresh(); } } 

Sichtbarkeit zurücksetzen


Jetzt müssen wir eine Methode erstellen HexCellShaderData.ViewElevationChanged. Das Ermitteln, wie sich eine allgemeine Sichtbarkeitssituation ändert, ist eine komplexe Aufgabe, insbesondere wenn mehrere Zellen gleichzeitig geändert werden. Daher werden wir keine Tricks finden, sondern einfach planen, die Sichtbarkeit aller Zellen zurückzusetzen. Fügen Sie ein boolesches Feld hinzu, um zu verfolgen, ob dies geschehen soll. Innerhalb der Methode setzen wir sie einfach auf true und schließen die Komponente ein. Unabhängig von der Anzahl der Zellen, die sich gleichzeitig geändert haben, führt dies zu einem einzelnen Reset.

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

Um die Sichtbarkeitswerte aller Zellen zurückzusetzen, müssen Sie Zugriff auf diese haben, über die Sie HexCellShaderDatanicht verfügen. Lassen Sie uns diese Verantwortung delegieren HexGrid. Dazu müssen Sie der HexCellShaderDataEigenschaft etwas hinzufügen, damit Sie auf das Raster verweisen können. Dann können wir es verwenden LateUpdate, um einen Reset anzufordern.

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

Fahren wir fort mit HexGrid: Setzen Sie den Link zum Raster, HexGrid.Awakenachdem Sie die Shader-Daten erstellt haben.

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

HexGridsollte auch eine Methode erhalten ResetVisibility, um alle Zellen zu verwerfen. Lassen Sie es einfach alle Zellen in der Schleife umgehen und delegieren Sie das Zurücksetzen an sich selbst.

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

Jetzt müssen wir die HexCellMethode ergänzen ResetVisibilty. Die Sichtbarkeit wird einfach auf Null gesetzt und die Sichtbarkeitsaktualisierung ausgelöst. Dies muss erfolgen, wenn die Sichtbarkeit der Zelle größer als Null ist.

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

Nachdem er alle Sichtbarkeitsdaten zurückgesetzt hat, HexGrid.ResetVisibilitymuss er erneut Sichtbarkeit auf alle Trupps anwenden, für die er den Umfang jedes Trupps kennen muss. Angenommen, es kann mit der Eigenschaft abgerufen werden 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); } } 

Damit dies funktioniert, werden wir die Umbenennung HexUnit.visionRangein umgestalten HexUnit.VisionRangeund in eine Eigenschaft umwandeln. Es wird zwar einen konstanten Wert von 3 erhalten, aber in Zukunft wird es sich ändern.

  public int VisionRange { get { return 3; } } 

Aus diesem Grund werden die Sichtbarkeitsdaten zurückgesetzt und bleiben nach dem Ändern der Zellenbetrachtungshöhe korrekt. Es ist jedoch wahrscheinlich, dass wir die Regeln für die Bestimmung des Bereichs ändern und die Neukompilierung im Wiedergabemodus ausführen. Damit sich der Bereich unabhängig ändert, führen wir einen Reset durch, HexGrid.OnEnablewenn eine Neukompilierung erkannt wird.

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

Jetzt können Sie den Scope-Code ändern und die Ergebnisse anzeigen, während Sie im Wiedergabemodus bleiben.

Den Horizont erweitern


Die Berechnung des Umfangs wird festgelegt HexGrid.GetVisibleCells. Damit sich die Höhe auf den Bereich auswirkt, können wir einfach die Betrachtungshöhe verwenden, indem wir fromCellden übertragenen Bereich vorübergehend neu definieren. So können wir leicht überprüfen, ob dies funktioniert.

  List<HexCell> GetVisibleCells (HexCell fromCell, int range) { … range = fromCell.ViewElevation; fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); … } 


Verwenden Sie die Höhe als Zielfernrohr.

Hindernisse für die Sichtbarkeit


Das Anwenden einer Betrachtungshöhe als Bereich funktioniert nur dann ordnungsgemäß, wenn sich alle anderen Zellen auf der Höhe Null befinden. Wenn jedoch alle Zellen dieselbe Höhe wie der Blickwinkel haben, sollte das Sichtfeld Null sein. Außerdem sollten Zellen mit hohen Höhen die Sichtbarkeit der niedrigen Zellen dahinter blockieren. Bisher wurde nichts davon implementiert.


Der Umfang stört nicht.

Der korrekteste Weg, um den Umfang zu bestimmen, wäre die Überprüfung durch die Emission von Strahlen, aber es würde schnell teuer werden und immer noch seltsame Ergebnisse liefern. Wir brauchen eine schnelle Lösung, die gut genug Ergebnisse liefert, die nicht perfekt sein müssen. Darüber hinaus ist es wichtig, dass die Regeln zur Bestimmung des Umfangs für die Spieler einfach, intuitiv und vorhersehbar sind.

Unsere Lösung lautet wie folgt: Wenn Sie die Sichtbarkeit einer Zelle bestimmen, addieren wir die Betrachtungshöhe der benachbarten Zelle zur zurückgelegten Entfernung. Tatsächlich verringert dies den Umfang, wenn wir diese Zellen betrachten, und wenn sie übersprungen werden, können wir die Zellen hinter ihnen nicht erreichen.

  int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range) { continue; } 


Hohe Zellen blockieren die Ansicht.

Sollten wir in der Ferne keine hohen Zellen sehen?
, , , . , .

Schauen Sie nicht um die Ecken


Jetzt scheint es, dass hohe Zellen die Sicht auf niedrig blockieren, aber manchmal durchdringt der Bereich sie, obwohl es so scheint, dass dies nicht sein sollte. Dies geschieht, weil der Suchalgorithmus immer noch einen Pfad zu diesen Zellen findet und die blockierenden Zellen umgeht. Infolgedessen sieht es so aus, als ob unser Sichtbereich Hindernisse umgehen kann. Um dies zu vermeiden, müssen wir sicherstellen, dass bei der Bestimmung der Zellsichtbarkeit nur die kürzesten Pfade berücksichtigt werden. Dies kann erreicht werden, indem Pfade gelöscht werden, die länger als nötig werden.

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


Wir benutzen nur die kürzesten Wege.

Also haben wir die meisten offensichtlich fehlerhaften Fälle behoben. Für nahe gelegene Zellen funktioniert dies gut, da es nur kürzeste Wege zu ihnen gibt. Weiter entfernte Zellen haben mehr Optionen für Pfade, daher kann über große Entfernungen immer noch eine Sichtbarkeitshülle auftreten. Dies ist kein Problem, wenn die Sichtbereiche klein bleiben und die Unterschiede in benachbarten Höhen nicht zu groß sind.

Und schließlich fügen wir, anstatt das übertragene Sichtfeld zu ersetzen, die Höhe der Sicht hinzu. Das eigene Sichtfeld des Trupps zeigt seine Höhe, Flughöhe oder Aufklärungsfähigkeiten an.

  range += fromCell.ViewElevation; 


Ansicht mit vollem Sichtfeld bei niedriger Sicht.

Das heißt, die endgültigen Sichtbarkeitsregeln gelten für das Sehen, wenn Sie sich auf dem kürzesten Weg zum Sichtfeld bewegen, wobei der Unterschied in der Zellenhöhe relativ zum Blickwinkel berücksichtigt wird. Wenn eine Zelle außerhalb des Gültigkeitsbereichs liegt, blockiert sie alle Pfade durch sie. Infolgedessen werden hohe Beobachtungspunkte, von denen aus nichts die Sicht behindert, strategisch wertvoll.

Was ist mit der Behinderung der Sichtbarkeit von Objekten?
, , . , , . .

Einheitspaket

Zellen, die nicht erforscht werden können


Das letzte Problem mit der Sichtbarkeit betrifft die Kanten der Karte. Das Relief endet abrupt und ohne Übergänge, weil die Zellen am Rand keine Nachbarn haben.


Markierter Rand der Karte.

Im Idealfall sollte die visuelle Anzeige von unerforschten Bereichen und Kanten der Karte identisch sein. Wir können dies erreichen, indem wir Sonderfälle hinzufügen, wenn Kanten trianguliert werden, wenn sie keine Nachbarn haben. Dies erfordert jedoch zusätzliche Logik und wir müssen mit fehlenden Zellen arbeiten. Daher ist eine solche Lösung nicht trivial. Ein alternativer Ansatz besteht darin, zu erzwingen, dass die Grenzzellen der Karte nicht erforscht werden, selbst wenn sie sich im Bereich des Trupps befinden. Dieser Ansatz ist viel einfacher, also verwenden wir ihn. Außerdem können Sie diese als unerforschte und andere Zellen markieren, wodurch die Erstellung ungleichmäßiger Kanten der Karte erleichtert wird. Darüber hinaus können Sie mit versteckten Zellen an den Rändern Straßen und Flüsse erstellen, die in die Karte des Flusses und der Straße eintreten und diese verlassen, da ihre Endpunkte außerhalb des Bereichs liegen.Mithilfe dieser Lösung können Sie auch Einheiten hinzufügen, die die Karte betreten und verlassen.

Wir markieren Zellen als untersucht


Fügen Sie der HexCellEigenschaft hinzu, dass eine Zelle untersucht werden kann Explorable.

  public bool Explorable { get; set; } 

Jetzt kann eine Zelle sichtbar sein, wenn es sich um eine untersuchte handelt. IsVisibleDaher ändern wir die Eigenschaft , um dies zu berücksichtigen.

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

Gleiches gilt für IsExplored. Hierzu haben wir jedoch die Standardeigenschaft untersucht. Wir müssen es in eine explizite Eigenschaft konvertieren, um die Logik seines Getters ändern zu können.

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

Verstecke den Rand der Karte


Sie können den Rand einer rechteckigen Karte in der Methode ausblenden HexGrid.CreateCell. Zellen, die sich nicht am Rand befinden, werden untersucht, der Rest ist unerforscht.

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

Jetzt sind die Karten an den Rändern abgedunkelt und verstecken sich hinter ihnen riesige unerforschte Räume. Infolgedessen nimmt die Größe des untersuchten Kartenbereichs in jeder Dimension um zwei ab.


Unerforschter Rand der Karte.

Ist es möglich, den Forschungszustand bearbeitbar zu machen?
, , . .

Unerforschte Zellen beeinträchtigen die Sichtbarkeit


Wenn die Zelle nicht untersucht werden kann, sollte sie die Sichtbarkeit beeinträchtigen. Ändern Sie dies HexGrid.GetVisibleCells, um dies zu berücksichtigen.

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

Einheitspaket

Teil 23: Land erzeugen


  • Füllen Sie neue Karten mit generierten Landschaften.
  • Wir erheben Land über Wasser, wir überfluten einige.
  • Wir kontrollieren die Menge des geschaffenen Landes, seine Höhe und Unebenheiten.
  • Wir bieten Unterstützung für verschiedene Konfigurationsoptionen zum Erstellen von Variablenzuordnungen.
  • Wir machen es so, dass dieselbe Karte erneut generiert werden kann.

Dieser Teil des Tutorials ist der Beginn einer Reihe zur prozeduralen Kartengenerierung.

Dieser Teil wurde in Unity 2017.1.0 erstellt.


Eine der vielen generierten Karten.

Kartengenerierung


Obwohl wir jede Karte erstellen können, dauert es sehr lange. Es wäre praktisch, wenn die Anwendung dem Designer helfen könnte, indem er Karten für ihn generiert, die er dann nach seinem Geschmack ändern kann. Sie können einen weiteren Schritt unternehmen und das manuelle Erstellen des Entwurfs vollständig vermeiden, indem Sie die Verantwortung für das Generieren der fertigen Karte vollständig auf die Anwendung übertragen. Aus diesem Grund kann das Spiel jedes Mal mit einer neuen Karte gespielt werden und jede Spielsitzung ist anders. Damit dies alles möglich ist, müssen wir einen Algorithmus zur Kartengenerierung erstellen.

Welche Art von Generierungsalgorithmus Sie benötigen, hängt von der Art der Karte ab, die Sie benötigen. Es gibt keinen richtigen Ansatz, man muss immer nach einem Kompromiss zwischen Glaubwürdigkeit und Spielbarkeit suchen.

Damit eine Karte glaubwürdig ist, muss sie dem Spieler durchaus möglich und real erscheinen. Dies bedeutet nicht, dass die Karte wie ein Teil unseres Planeten aussehen sollte. Es kann ein anderer Planet oder eine völlig andere Realität sein. Aber wenn es das Relief der Erde anzeigen soll, dann muss es ihm zumindest teilweise ähneln.

Die Spielbarkeit hängt davon ab, wie die Karten dem Gameplay entsprechen. Manchmal widerspricht es der Glaubwürdigkeit. Zum Beispiel können Bergketten, obwohl sie schön aussehen können, gleichzeitig die Bewegung und Sicht von Einheiten stark einschränken. Wenn dies unerwünscht ist, müssen Sie auf Berge verzichten, was die Glaubwürdigkeit verringert und die Ausdruckskraft des Spiels einschränkt. Oder wir können die Berge retten, aber ihre Auswirkungen auf das Gameplay verringern, was auch die Glaubwürdigkeit verringern kann.

Darüber hinaus muss die Machbarkeit berücksichtigt werden. Sie können beispielsweise einen sehr realistischen erdähnlichen Planeten erstellen, indem Sie tektonische Platten, Erosion, Regen, Vulkanausbrüche, die Auswirkungen von Meteoriten und Mond usw. simulieren. Die Entwicklung eines solchen Systems wird jedoch viel Zeit in Anspruch nehmen. Darüber hinaus kann es lange dauern, einen solchen Planeten zu generieren, und die Spieler werden nicht einige Minuten warten wollen, bevor sie ein neues Spiel starten. Das heißt, Simulation ist ein mächtiges Werkzeug, aber es hat einen Preis.

Spiele verwenden häufig Kompromisse zwischen Glaubwürdigkeit, Spielbarkeit und Durchführbarkeit. Manchmal sind solche Kompromisse unsichtbar und scheinen völlig normal zu sein, und manchmal sehen sie zufällig, inkonsistent oder chaotisch aus, abhängig von den Entscheidungen, die während des Entwicklungsprozesses getroffen werden. Dies gilt nicht nur für die Kartengenerierung, sondern Sie müssen bei der Entwicklung eines prozeduralen Kartengenerators besonders darauf achten. Sie können viel Zeit damit verbringen, einen Algorithmus zu erstellen, der wunderschöne Karten generiert, die sich für das von Ihnen erstellte Spiel als nutzlos herausstellen.

In dieser Tutorialserie werden wir ein landähnliches Relief erstellen. Es sollte interessant aussehen, mit großer Variabilität und dem Fehlen großer homogener Bereiche. Der Reliefmaßstab wird groß sein, Karten werden einen oder mehrere Kontinente, Regionen der Ozeane oder sogar einen ganzen Planeten abdecken. Wir brauchen Kontrolle über die Geographie, einschließlich Landmassen, Klima, Anzahl der Regionen und Geländeunebenheiten. In diesem Teil legen wir den Grundstein für die Herstellung von Sushi.

Erste Schritte im Bearbeitungsmodus


Wir werden uns auf die Karte konzentrieren, nicht auf das Gameplay, daher ist es bequemer, die Anwendung im Bearbeitungsmodus zu starten. Dank dessen können wir die Karten sofort sehen. Daher ändern wir HexMapEditor.Awakeden Bearbeitungsmodus auf true und aktivieren das Shader-Schlüsselwort dieses Modus.

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

Kartengenerator


Da zum Generieren von prozeduralen Maps ziemlich viel Code benötigt wird, werden wir ihn nicht direkt hinzufügen HexGrid. Stattdessen erstellen wir eine neue Komponente HexMapGeneratorund HexGridwissen nichts darüber. Dies vereinfacht den Übergang zu einem anderen Algorithmus, wenn wir ihn benötigen.

Der Generator benötigt eine Verknüpfung zum Raster, daher fügen wir ihm ein allgemeines Feld hinzu. Zusätzlich fügen wir eine allgemeine Methode hinzu GenerateMap, die sich mit der Arbeit des Algorithmus befasst. Wir geben ihm die Abmessungen der Karte als Parameter und erzwingen dann, dass sie zum Erstellen einer neuen leeren Karte verwendet wird.

 using System.Collections.Generic; using UnityEngine; public class HexMapGenerator : MonoBehaviour { public HexGrid grid; public void GenerateMap (int x, int z) { grid.CreateMap(x, z); } } 

Fügen Sie der Szene ein Objekt mit einer Komponente hinzu HexMapGeneratorund verbinden Sie es mit dem Raster.


Kartengeneratorobjekt.

Ändern Sie das Menü einer neuen Karte


Wir werden es NewMapMenuso ändern , dass es Karten erzeugen kann, nicht nur leere. Wir werden seine Funktionalität über ein Boolesches Feld steuern generateMaps, das standardmäßig einen Wert hat true. Lassen Sie uns eine allgemeine Methode zum Festlegen dieses Felds erstellen, wie wir es getan haben, um Optionen zu wechseln HexMapEditor. Fügen Sie dem Menü den entsprechenden Schalter hinzu und verbinden Sie ihn mit der Methode.

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


Menü einer neuen Karte mit einem Schalter.

Geben Sie dem Menü einen Link zum Kartengenerator. Dann werden wir ihn zwingen, bei Bedarf die GenerateMapGeneratormethode aufzurufen und nicht nur das CreateMapRaster auszuführen .

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


Anschluss an den Generator.

Zellenzugriff


Damit der Generator funktioniert, muss er auf die Zellen zugreifen können. Wir HexGridhaben bereits gängige Methoden , GetCelldie oder Positionsvektor erfordern, oder Sechseck - Koordinaten. Der Generator muss weder mit dem einen noch mit dem anderen arbeiten, daher fügen wir zwei praktische Methoden hinzu HexGrid.GetCell, die mit den Koordinaten des Versatzes oder des Index der Zelle arbeiten.

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

Jetzt HexMapGeneratorkann es Zellen direkt empfangen. Nach dem Erstellen einer neuen Karte kann er beispielsweise Graskoordinaten verwenden, um Gras als Relief der mittleren Zellenspalte festzulegen.

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


Grassäule auf einer kleinen Karte.

Einheitspaket

Sushi machen


Bei der Erstellung einer Karte beginnen wir komplett ohne Land. Man kann sich vorstellen, dass die ganze Welt von einem riesigen Ozean überflutet ist. Ein Land entsteht, wenn ein Teil des Meeresbodens so weit nach oben gedrückt wird, dass er sich über das Wasser erhebt. Wir müssen entscheiden, wie viel Land auf diese Weise geschaffen werden soll, wo es erscheinen wird und welche Form es haben wird.

Erhöhen Sie die Erleichterung


Fangen wir klein an - heben Sie ein Stück Land über das Wasser. Dazu erstellen wir eine Methode RaiseTerrainmit einem Parameter zur Steuerung der Größe des Plots. Rufen Sie diese Methode auf GenerateMapund ersetzen Sie den vorherigen Testcode. Beginnen wir mit einem kleinen Stück Land, das aus sieben Zellen besteht.

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

Bisher verwenden wir das Relief „Gras“, um das erhöhte Land zu bezeichnen, und das ursprüngliche Relief „Sand“ bezieht sich auf den Ozean. Lassen Sie uns RaiseTerraineine zufällige Zelle nehmen und die Art ihres Reliefs ändern, bis wir die richtige Menge Land erhalten.

Um eine zufällige Zelle zu erhalten, fügen wir eine Methode hinzu GetRandomCell, die einen zufälligen Zellenindex bestimmt und die entsprechende Zelle aus dem Raster erhält.

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


Sieben zufällige Sushi-Zellen.

Da wir am Ende möglicherweise viele zufällige Zellen benötigen oder alle Zellen mehrmals durchlaufen müssen, verfolgen wir die Anzahl der Zellen in der Zelle selbst HexMapGenerator.

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

Erstellung einer Site


Bisher verwandeln wir sieben zufällige Zellen in Land, und sie können überall sein. Höchstwahrscheinlich bilden sie keine einzige Landfläche. Außerdem können wir dieselben Zellen mehrmals auswählen, sodass wir weniger Land erhalten. Um beide Probleme ohne Einschränkungen zu lösen, wählen wir nur die erste Zelle aus. Danach sollten wir nur die Zellen auswählen, die sich neben den zuvor ausgewählten befinden. Diese Einschränkungen ähneln den Einschränkungen der Pfadsuche, daher verwenden wir hier denselben Ansatz.

Wir fügen HexMapGeneratorunser eigenes Eigentum und den Zähler der Phase der Suchgrenze hinzu, wie es war HexGrid.

  HexCellPriorityQueue searchFrontier; int searchFrontierPhase; 

Überprüfen Sie, ob die Prioritätswarteschlange vorhanden ist, bevor wir sie benötigen.

  public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } RaiseTerrain(7); } 

Nach dem Erstellen einer neuen Karte ist die Suchgrenze für alle Zellen Null. Wenn wir jedoch bei der Kartengenerierung nach Zellen suchen, werden wir deren Suchgrenze in diesem Prozess erhöhen. Wenn wir viele Suchvorgänge ausführen, liegen diese möglicherweise vor der Phase der aufgezeichneten Suchgrenze HexGrid. Dies kann die Suche nach Einheitenpfaden beeinträchtigen. Um dies zu vermeiden, setzen wir am Ende des Kartengenerierungsprozesses die Suchphase aller Zellen auf Null zurück.

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

Jetzt muss ich RaiseTerrainnach den entsprechenden Zellen suchen und sie nicht zufällig auswählen. Dieser Vorgang ist der Suchmethode in sehr ähnlich HexGrid. Wir werden Zellen jedoch nicht mehr als einmal besuchen, sodass es ausreicht, die Phase des Suchrahmens um 1 statt um 2 zu erhöhen. Dann initialisieren wir den Rand mit der ersten Zelle, die zufällig ausgewählt wird. Zusätzlich zum Festlegen der Suchphase weisen wir wie üblich den Abstand und die Heuristik auf Null zu.

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

Danach ist uns die Suchschleife größtenteils vertraut. Um die Suche fortzusetzen, bis der Rand leer ist, müssen wir außerdem anhalten, wenn das Fragment die gewünschte Größe erreicht hat, damit wir es verfolgen können. Bei jeder Iteration extrahieren wir die nächste Zelle aus der Warteschlange, legen den Typ des Reliefs fest, erhöhen die Größe und umgehen dann die Nachbarn dieser Zelle. Alle Nachbarn werden einfach zur Grenze hinzugefügt, wenn sie dort noch nicht hinzugefügt wurden. Wir müssen keine Änderungen oder Vergleiche vornehmen. Nach Abschluss müssen Sie die Grenze löschen.

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


Eine Reihe von Zellen.

Wir haben ein einzelnes Grundstück der richtigen Größe. Es wird nur kleiner, wenn nicht genügend Zellen vorhanden sind. Aufgrund der Art und Weise, wie die Grenze gefüllt ist, besteht das Grundstück immer aus einer Linie, die nach Nordwesten verläuft. Es ändert die Richtung nur, wenn es den Rand der Karte erreicht.

Wir verbinden Zellen


Landflächen ähneln selten Linien, und wenn doch, sind sie nicht immer gleich ausgerichtet. Um die Form der Site zu ändern, müssen wir die Prioritäten der Zellen ändern. Die erste Zufallszelle kann als Mittelpunkt des Diagramms verwendet werden. Dann ist der Abstand zu allen anderen Zellen relativ zu diesem Punkt. Daher werden wir Zellen, die näher am Zentrum liegen, eine höhere Priorität einräumen, damit die Site nicht als Linie, sondern um das Zentrum herum wächst.

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


Die Ansammlung von Zellen.

Tatsächlich sind unsere sieben Zellen jetzt wunderschön in einem kompakten sechseckigen Bereich verpackt, wenn die zentrale Zelle nicht am Rand der Karte angezeigt wird. Versuchen wir jetzt, eine Plotgröße von 30 zu verwenden.

  RaiseTerrain(30); 


Sushi-Masse in 30 Zellen.

Wir haben wieder die gleiche Form, obwohl es nicht genügend Zellen gab, um das richtige Sechseck zu erhalten. Da der Radius des Diagramms größer ist, ist es wahrscheinlicher, dass es sich nahe am Rand der Karte befindet, wodurch eine andere Form angenommen wird.

Sushi Randomisierung


Wir möchten nicht, dass alle Bereiche gleich aussehen, daher ändern wir die Zellenprioritäten geringfügig. Jedes Mal, wenn wir eine benachbarte Zelle zum Rand hinzufügen Random.value, wird die Heuristik dieser Zelle nicht 0, sondern 1 , wenn die nächste Zahl kleiner als ein bestimmter Schwellenwert ist. Verwenden wir den Wert 0,5 als Schwellenwert, dh es wirkt sich höchstwahrscheinlich auf die Hälfte der Zellen aus.

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


Verzerrter Bereich.

Durch Erhöhen der Suchheuristik der Zelle haben wir sie später als erwartet besucht. Gleichzeitig werden andere Zellen, die sich einen Schritt weiter vom Zentrum entfernt befinden, früher besucht, sofern sie nicht auch die Heuristik erhöhen. Dies bedeutet, dass wenn wir die Heuristik aller Zellen um einen Wert erhöhen, dies keine Auswirkungen auf die Karte hat. Das heißt, Schwelle 1 hat keine Auswirkung wie Schwelle 0. Und Schwelle 0,8 entspricht 0,2. Das heißt, die Wahrscheinlichkeit von 0,5 macht den Suchvorgang zum "zitterndsten".

Das geeignete Maß an Schwingung hängt vom gewünschten Geländetyp ab. Machen wir es also anpassbar. Fügen Sie dem Generator ein generisches Float-Feld jitterProbabilitymit dem Attribut hinzuRangebegrenzt im Bereich von 0 bis 0,5. Geben wir einen Standardwert an, der dem Durchschnitt dieses Intervalls entspricht, d. H. 0,25. Auf diese Weise können wir den Generator im Unity-Inspektorfenster konfigurieren.

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


Wahrscheinlichkeit von Schwankungen.

Kannst du es in der Benutzeroberfläche des Spiels anpassbar machen?
, . UI, . , UI. , . , .

Um nun zu entscheiden, wann die Heuristik gleich 1 sein soll, verwenden wir die Wahrscheinlichkeit anstelle eines konstanten Werts.

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

Wir verwenden die heuristischen Werte 0 und 1. Obwohl größere Werte verwendet werden können, verschlechtert dies die Verformung der Abschnitte erheblich und verwandelt sie höchstwahrscheinlich in ein Bündel von Streifen.

Erhebe etwas Land


Wir werden uns nicht auf die Erzeugung eines Stücks Land beschränken. Zum Beispiel platzieren wir einen Aufruf RaiseTerraininnerhalb einer Schleife, um fünf Abschnitte zu erhalten.

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


Fünf Grundstücke.

Obwohl wir jetzt fünf Parzellen mit jeweils 30 Zellen erzeugen, erhalten wir nicht unbedingt genau 150 Zellen Land. Da jede Site separat erstellt wird, kennen sie sich nicht, sodass sie sich überschneiden können. Dies ist normal, da dadurch interessantere Landschaften als nur einzelne Abschnitte erstellt werden können.

Um die Variabilität des Landes zu erhöhen, können wir auch die Größe jedes Grundstücks ändern. Fügen Sie zwei ganzzahlige Felder hinzu, um die minimale und maximale Größe der Diagramme zu steuern. Weisen Sie ihnen ein ausreichend großes Intervall zu, z. B. 20-200. Ich werde das Standardminimum auf 30 und das Standardmaximum auf 100 setzen.

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


Größenintervall.

Wir verwenden diese Felder, um die Größe des Bereichs beim Aufruf zufällig zu bestimmen RaiseTerrain.

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


Fünf zufällig große Abschnitte auf der mittleren Karte.

Erstelle genug Sushi


Zwar können wir die Menge des erzeugten Landes nicht besonders kontrollieren. Obwohl wir die Konfigurationsoption für die Anzahl der Diagramme hinzufügen können, sind die Diagramme selbst zufällig groß und können sich geringfügig oder stark überlappen. Daher garantiert die Anzahl der Standorte nicht den Erhalt der erforderlichen Landmenge auf der Karte. Fügen wir eine Option hinzu, um den Prozentsatz des Landes, der als Ganzzahl ausgedrückt wird, direkt zu steuern. Da 100% Land oder Wasser nicht sehr interessant sind, beschränken wir es auf das Intervall 5–95 mit einem Standardwert von 50.

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


Prozentsatz von Sushi.

Um die Schaffung der richtigen Menge Land zu gewährleisten, müssen wir nur weiterhin Gebiete des Geländes anheben, bis wir eine ausreichende Menge erhalten. Dazu müssen wir den Prozess kontrollieren, der die Erzeugung von Land erschwert. Ersetzen wir daher den vorhandenen Zyklus zum Erhöhen von Websites durch Aufrufen einer neuen Methode CreateLand. Das erste, was diese Methode macht, ist die Anzahl der Zellen zu berechnen, die Land werden sollen. Diese Menge ist unsere Gesamtsumme der Sushi-Zellen.

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

CreateLandwird verursachen, RaiseTerrainbis wir die gesamte Menge an Zellen ausgegeben haben. Um den Betrag nicht zu überschreiten, ändern wir ihn RaiseTerrainso, dass er den Betrag als zusätzlichen Parameter erhält. Nach Beendigung der Arbeit muss er den Restbetrag zurückgeben.

 // void RaiseTerrain (int chunkSize) { int RaiseTerrain (int chunkSize, int budget) { … return budget; } 

Die Menge sollte jedes Mal abnehmen, wenn die Zelle von der Grenze entfernt und in Land umgewandelt wird. Wenn danach der gesamte Betrag ausgegeben wird, müssen wir die Suche beenden und die Site vervollständigen. Außerdem sollte dies nur erfolgen, wenn die aktuelle Zelle noch nicht gelandet ist.

  while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current.TerrainTypeIndex == 0) { current.TerrainTypeIndex = 1; if (--budget == 0) { break; } } size += 1; … } 

Jetzt CreateLandkann es Land anheben, bis es die gesamte Menge an Zellen verbraucht.

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


Genau die Hälfte der Karte wurde Land.

Einheitspaket

Berücksichtigen Sie die Höhe


Land ist nicht nur eine flache Platte, die durch die Küste begrenzt ist. Sie hat eine wechselnde Höhe mit Hügeln, Bergen, Tälern, Seen und so weiter. Aufgrund des Zusammenspiels sich langsam bewegender tektonischer Platten bestehen große Höhenunterschiede. Obwohl wir es nicht simulieren werden, sollten unsere Landflächen in gewisser Weise solchen Platten ähneln. Websites bewegen sich nicht, können sich jedoch überschneiden. Und das können wir nutzen.

Schieben Sie das Land nach oben


Jedes Grundstück repräsentiert einen Teil des Landes, der aus dem Meeresboden herausgedrückt wurde. Erhöhen wir daher ständig die Höhe der aktuellen Zelle RaiseTerrainund sehen, was passiert.

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


Lande mit Höhen.

Wir haben die Höhen, aber es ist schwer zu sehen. Sie können sie besser lesbar machen, wenn Sie für jede Höhenstufe einen eigenen Geländetyp verwenden, z. B. eine geografische Schichtung. Wir werden dies nur tun, damit die Höhen besser erkennbar sind, sodass Sie die Höhenebene einfach als Höhenindex verwenden können.

Was passiert, wenn die Höhe die Anzahl der Geländetypen überschreitet?
. , .

Anstatt den Geländetyp der Zelle bei jeder Höhenänderung zu aktualisieren, erstellen wir eine separate Methode SetTerrainType, um alle Geländetypen nur einmal festzulegen.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); cell.TerrainTypeIndex = cell.Elevation; } } 

Wir werden diese Methode nach dem Erstellen von Sushi aufrufen.

  public void GenerateMap (int x, int z) { … CreateLand(); SetTerrainType(); … } 

Jetzt RaiseTerrainkann er sich nicht mehr mit der Art der Erleichterung befassen und sich auf Höhen konzentrieren. Dazu müssen Sie die Logik ändern. Wenn die neue Höhe der aktuellen Zelle 1 ist, ist sie gerade zu Land geworden, sodass die Summe der Zellen abgenommen hat, was zum Abschluss des Wachstums des Standorts führen kann.

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


Schichtung der Schichten.

Wasser hinzufügen


Lassen Sie uns explizit angeben, welche Zellen Wasser oder Land sind, und den Wasserstand für alle Zellen auf 1 einstellen. Führen Sie dies aus, GenerateMapbevor Sie Land erstellen.

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

Für die Bezeichnung von Landschichten können wir nun alle Geländearten verwenden. Alle U-Boot-Zellen bleiben Sand, ebenso wie die niedrigsten Landzellen. Dies kann erreicht werden, indem der Wasserstand von der Höhe abgezogen und der Wert als Index für den Relieftyp verwendet wird.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (!cell.IsUnderwater) { cell.TerrainTypeIndex = cell.Elevation - cell.WaterLevel; } } } 


Land und Wasser.

Erhöhen Sie den Wasserstand


Wir sind nicht auf einen Wasserstand beschränkt. Machen wir es anpassbar, indem wir ein gemeinsames Feld mit einem Intervall von 1 bis 5 und einem Standardwert von 3 verwenden. Verwenden Sie diese Ebene beim Initialisieren der Zellen.

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



Wasserstand 3.

Wenn der Wasserstand 3 ist, bekommen wir weniger Land als erwartet. Dies liegt daran RaiseTerrain, dass es immer noch glaubt, dass der Wasserstand 1 ist. Lassen Sie es uns beheben.

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

Die Verwendung höherer Wasserstände führt dazu. dass die Zellen nicht sofort landen. Wenn der Wasserstand 2 ist, bleibt der erste Abschnitt immer noch unter Wasser. Der Meeresboden ist gestiegen, bleibt aber immer noch unter Wasser. Ein Land wird nur am Schnittpunkt von mindestens zwei Abschnitten gebildet. Je höher der Wasserstand, desto mehr Standorte müssen überquert werden, um Land zu schaffen. Mit steigendem Wasserstand wird das Land daher chaotischer. Wenn mehr Grundstücke benötigt werden, ist es außerdem wahrscheinlicher, dass sie sich auf bereits vorhandenem Land kreuzen, wodurch Berge häufiger vorkommen, und flaches Land weniger wahrscheinlich, wie im Fall der Verwendung kleinerer Grundstücke.





Der Wasserstand beträgt 2–5, Sushi immer 50%.

Einheitspaket

Vertikale Bewegung


Bisher haben wir die Grundstücke jeweils um eine Ebene angehoben, müssen uns aber nicht darauf beschränken.

Hohe Standorte


Obwohl jeder Abschnitt die Höhe seiner Zellen um eine Ebene erhöht, können Ausschnitte auftreten. Dies geschieht, wenn sich die Kanten zweier Abschnitte berühren. Dies kann zu isolierten Klippen führen, aber lange Klippenlinien sind selten. Wir können die Häufigkeit ihres Auftretens erhöhen, indem wir die Höhe des Diagramms um mehr als einen Schritt erhöhen. Dies muss jedoch nur für einen bestimmten Anteil der Websites erfolgen. Wenn alle Bereiche hoch ansteigen, ist es sehr schwierig, sich im Gelände zu bewegen. Lassen Sie uns diesen Parameter mithilfe eines Wahrscheinlichkeitsfelds mit einem Standardwert von 0,25 anpassbar machen.

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


Die Wahrscheinlichkeit eines starken Anstiegs der Zellen.

Obwohl wir jede Höhenerhöhung für hohe Bereiche verwenden können, gerät dies schnell außer Kontrolle. Der Höhenunterschied 2 erzeugt bereits Klippen, das ist also genug. Da Sie eine Höhe überspringen können, die dem Wasserstand entspricht, müssen wir die Art und Weise ändern, in der wir feststellen, ob eine Zelle zu Land geworden ist. Wenn es unter dem Wasserspiegel lag und jetzt auf dem gleichen oder einem höheren Niveau liegt, haben wir eine neue Landzelle erstellt.

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





Die Wahrscheinlichkeiten eines starken Höhenanstiegs betragen 0,25, 0,50, 0,75 und 1.

Senke das Land


Das Land steigt nicht immer, manchmal fällt es. Wenn das Land tief genug fällt, füllt es Wasser und es geht verloren. Bisher machen wir das nicht. Da wir nur die Gebiete nach oben schieben, sieht das Land normalerweise aus wie eine Reihe ziemlich runder Gebiete, die miteinander vermischt sind. Wenn wir den Bereich manchmal absenken, erhalten wir unterschiedlichere Formen.


Große Karte ohne versunkenes Sushi.

Wir können die Häufigkeit der Landabsenkung mithilfe eines anderen Wahrscheinlichkeitsfelds steuern. Da das Absenken Land zerstören kann, sollte die Wahrscheinlichkeit des Absenkens immer geringer sein als die Wahrscheinlichkeit des Anhebens. Andernfalls kann es sehr lange dauern, bis der richtige Prozentsatz an Land erreicht ist. Verwenden wir daher eine maximale Absenkungswahrscheinlichkeit von 0,4 mit einem Standardwert von 0,2.

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


Wahrscheinlichkeit des Absenkens.

Das Absenken der Site ähnelt dem Anheben mit einigen Unterschieden. Daher duplizieren wir die Methode RaiseTerrainund ändern ihren Namen in SinkTerrain. Anstatt die Größe des Anstiegs zu bestimmen, benötigen wir einen Absenkungswert, der dieselbe Logik verwenden kann. Gleichzeitig müssen Vergleiche durchgeführt werden, um zu überprüfen, ob wir die Wasseroberfläche passiert haben. Außerdem sind wir beim Absenken des Reliefs nicht auf die Summe der Zellen beschränkt. Stattdessen gibt jede verlorene Sushi-Zelle den dafür ausgegebenen Betrag zurück, sodass wir ihn erhöhen und weiterarbeiten.

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

Jetzt CreateLandmüssen wir bei jeder Iteration im Inneren das Land entweder senken oder erhöhen, abhängig von der Wahrscheinlichkeit des Absenkens.

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





Die Wahrscheinlichkeit eines Abfalls beträgt 0,1, 0,2, 0,3 und 0,4.

Höhe begrenzen


Gegenwärtig können wir möglicherweise viele Abschnitte überlappen, manchmal mit mehreren Höhenerhöhungen, von denen einige nach unten und dann wieder nach oben gehen können. Gleichzeitig können wir sehr hohe und manchmal sehr niedrige Höhen schaffen, insbesondere wenn ein hoher Prozentsatz an Land benötigt wird.


Riesige Höhen bei 90% Land.

Um die Höhe zu begrenzen, fügen wir ein benutzerdefiniertes Minimum und Maximum hinzu. Ein vernünftiges Minimum liegt irgendwo zwischen -4 und 0, und ein akzeptables Maximum kann im Bereich von 6 bis 10 liegen. Die Standardwerte sind -2 und 8. Wenn Sie die Karte manuell bearbeiten, liegen sie außerhalb des zulässigen Grenzwerts, sodass Sie den Schieberegler der Benutzeroberfläche des Editors ändern oder unverändert lassen können.

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


Minimale und maximale Höhe.

Jetzt RaiseTerrainmüssen wir sicherstellen, dass die Höhe das zulässige Maximum nicht überschreitet. Dies kann durch Überprüfen erfolgen, ob die aktuellen Zellen zu hoch sind. Wenn ja, überspringen wir sie, ohne die Höhe zu ändern und ihre Nachbarn hinzuzufügen. Dies führt dazu, dass Landflächen Gebiete meiden, die eine maximale Höhe erreicht haben, und um sie herum wachsen.

  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; 

Machen Sie dasselbe in SinkTerrain, aber für eine minimale Höhe.

  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; 


Begrenzte Höhe mit 90% Land.

Erhalt der negativen Höhe


Zu diesem Zeitpunkt kann der Speicher- und Ladecode keine negativen Höhen verarbeiten, da die Höhe als Byte gespeichert wird. Eine negative Zahl wird beim Speichern in eine große positive Zahl umgewandelt. Daher können beim Speichern und Laden der generierten Karte sehr hohe Karten anstelle der ursprünglichen Unterwasserzellen angezeigt werden.

Wir können negative Höhen unterstützen, indem wir sie als Ganzzahl und nicht als Byte speichern. Wir müssen jedoch immer noch nicht mehrere Höhenstufen unterstützen. Zusätzlich können wir den gespeicherten Wert durch Hinzufügen von 127 versetzen. Dadurch können wir Höhen im Bereich von –127–128 innerhalb eines Bytes korrekt speichern. Ändern Sie HexCell.Saveentsprechend.

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

Da wir die Art und Weise, wie wir Kartendaten speichern, geändert haben, erhöhen wir sie SaveLoadMenu.mapFileVersionauf 4.

  const int mapFileVersion = 4; 

Ändern Sie HexCell.Loades schließlich so, dass 127 von den aus Dateien der Version 4 geladenen Höhen abgezogen werden.

  public void Load (BinaryReader reader, int header) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); if (header >= 4) { elevation -= 127; } … } 

Einheitspaket

Die gleiche Karte neu erstellen


Jetzt können wir eine Vielzahl von Karten erstellen. Bei der Generierung wird jedes neue Ergebnis zufällig sein. Mit den Konfigurationsoptionen können wir nur die Eigenschaften der Karte steuern, nicht jedoch die genaueste Form. Aber manchmal müssen wir genau dieselbe Karte erneut erstellen. Zum Beispiel, um eine schöne Karte mit einem Freund zu teilen oder nach der manuellen Bearbeitung erneut zu beginnen. Es ist auch nützlich für den Spieleentwicklungsprozess. Fügen wir diese Funktion hinzu.

Verwenden von Seed


Um den Kartengenerierungsprozess unvorhersehbar zu machen, verwenden wir Random.Rangeund Random.value. Um wieder dieselbe pseudozufällige Folge von Zahlen zu erhalten, müssen Sie denselben Startwert verwenden. Wir haben bereits zuvor einen ähnlichen Ansatz gewählt HexMetrics.InitializeHashGrid. Zunächst wird der aktuelle Status des mit einem bestimmten Startwert initialisierten Zahlengenerators gespeichert und anschließend der ursprüngliche Status wiederhergestellt. Wir können den gleichen Ansatz für verwenden HexMapGenerator.GenerateMap. Wir können uns wieder an den alten Zustand erinnern und ihn nach Abschluss wiederherstellen, um nichts anderes zu stören, was verwendet wird Random.

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

Als nächstes müssen wir den Startwert zur Verfügung stellen, mit dem die letzte Karte generiert wurde. Dies erfolgt mithilfe eines gemeinsamen Ganzzahlfelds.

  public int seed; 


Startwert anzeigen.

Jetzt brauchen wir den Startwert zum Initialisieren Random. Um zufällige Karten zu erstellen, müssen Sie einen zufälligen Startwert verwenden. Der einfachste Ansatz besteht darin, einen beliebigen Startwert zum Generieren zu verwenden Random.Range. Damit der anfängliche Zufallszustand nicht beeinflusst wird, müssen wir dies nach dem Speichern tun.

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

Da wir nach Abschluss einen zufälligen Zustand wiederherstellen, erhalten wir, wenn wir sofort eine andere Karte generieren, den gleichen Startwert. Außerdem wissen wir nicht, wie der anfängliche Zufallszustand initialisiert wurde. Obwohl es als willkürlicher Ausgangspunkt dienen kann, benötigen wir daher etwas mehr, um es bei jedem Aufruf zufällig zu ordnen.

Es gibt verschiedene Möglichkeiten, Zufallszahlengeneratoren zu initialisieren. In diesem Fall können Sie einfach mehrere beliebige Werte kombinieren, die über einen weiten Bereich variieren. Das heißt, die Wahrscheinlichkeit, dieselbe Karte erneut zu generieren, ist gering. Zum Beispiel verwenden wir die unteren 32 Bits der Systemzeit, ausgedrückt in Zyklen, plus die aktuelle Laufzeit der Anwendung. Kombinieren Sie diese Werte mit der bitweisen exklusiven ODER-Verknüpfung, damit das Ergebnis nicht sehr groß ist.

  seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.unscaledTime; Random.InitState(seed); 

Die resultierende Zahl kann negativ sein, was für einen Startwert mit öffentlichem Wert nicht sehr gut aussieht. Wir können es streng positiv machen, indem wir eine bitweise Maskierung mit einem maximalen ganzzahligen Wert verwenden, der das Vorzeichenbit zurücksetzt.

  seed ^= (int)Time.unscaledTime; seed &= int.MaxValue; Random.InitState(seed); 

Wiederverwendbarer Samen


Wir generieren immer noch zufällige Karten, aber jetzt können wir sehen, welcher Startwert für jede von ihnen verwendet wurde. Um dieselbe Karte erneut zu erstellen, müssen wir den Generator anweisen, denselben Startwert erneut zu verwenden, anstatt einen neuen zu erstellen. Wir werden dies tun, indem wir einen Schalter unter Verwendung eines Booleschen Feldes hinzufügen.

  public bool useFixedSeed; 


Option zur Verwendung eines konstanten Samens.

Wenn ein konstanter Startwert ausgewählt ist, überspringen wir einfach die Generierung des neuen Startwerts in GenerateMap. Wenn wir das Startfeld nicht manuell ändern, ist das Ergebnis wieder dieselbe Karte.

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

Jetzt können wir den Startwert der gewünschten Karte kopieren und irgendwo speichern, um ihn in Zukunft erneut zu generieren. Vergessen Sie nicht, dass wir nur dann dieselbe Karte erhalten, wenn wir genau dieselben Generatorparameter verwenden, dh dieselbe Kartengröße sowie alle anderen Konfigurationsoptionen. Selbst eine kleine Änderung dieser Wahrscheinlichkeiten kann eine völlig andere Karte erzeugen. Daher müssen wir uns zusätzlich zum Startwert alle Einstellungen merken.



Große Karten mit Startwerten 0 und 929396788, Standardparameter.

Einheitspaket

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


All Articles