Unity Hexagon Maps: Wasser, Sehenswürdigkeiten und Festungsmauern

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 8: Wasser


  • Wasser in die Zellen geben.
  • Triangulieren Sie die Wasseroberfläche.
  • Erstellen Sie eine Brandung mit Schaum.
  • Kombinieren Sie Wasser und Flüsse.

Wir haben bereits Flussunterstützung hinzugefügt, und in diesem Teil werden wir die Zellen vollständig in das Wasser eintauchen.


Wasser kommt.

Wasserstand


Am einfachsten ist es, die Wasserunterstützung zu implementieren, indem Sie sie auf das gleiche Niveau einstellen. Alle Zellen, deren Höhe unter diesem Niveau liegt, werden in Wasser getaucht. Ein flexiblerer Weg wäre jedoch, das Wasser in unterschiedlichen Höhen zu halten. Lassen Sie uns also den Wasserstand veränderbar machen. Dazu muss HexCell den Wasserstand überwachen.

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

Falls gewünscht, können Sie sicherstellen, dass bestimmte Merkmale des Reliefs unter Wasser nicht vorhanden waren. Aber im Moment werde ich das nicht tun. Dinge wie Unterwasserstraßen passen zu mir. Sie können als Gebiete betrachtet werden, die kürzlich überflutet wurden.

Zellen überfluten


Jetzt, wo wir Wasserstände haben, ist die wichtigste Frage, ob sich die Zellen unter Wasser befinden. Eine Zelle befindet sich unter Wasser, wenn ihr Wasserstand über ihrer Höhe liegt. Um diese Informationen zu erhalten, fügen wir eine Eigenschaft hinzu.

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

Dies bedeutet, dass sich die Zelle bei gleichem Wasserstand und gleicher Höhe über das Wasser erhebt. Das heißt, die reale Wasseroberfläche befindet sich unterhalb dieser Höhe. HexMetrics.riverSurfaceElevationOffset wir wie bei Flussoberflächen den gleichen Versatz hinzu - HexMetrics.riverSurfaceElevationOffset . Ändern Sie den Namen in einen allgemeineren.

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

Ändern Sie HexCell.RiverSurfaceY so, dass der neue Name verwendet wird. Dann fügen wir der Wasseroberfläche der überfluteten Zelle eine ähnliche Eigenschaft hinzu.

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

Wasserbearbeitung


Das Bearbeiten des Wasserstandes ähnelt dem Ändern der Höhe. Daher muss HexMapEditor den aktiven Wasserstand überwachen und prüfen, ob er auf die Zellen angewendet werden soll.

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

Fügen Sie Methoden hinzu, um diese Parameter mit der Benutzeroberfläche zu verbinden.

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

Und fügen Sie den Wasserstand zu EditCell .

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

Um der Benutzeroberfläche einen Wasserstand hinzuzufügen, duplizieren Sie die Beschriftung und den Höhenregler und ändern Sie sie dann. Denken Sie daran, ihre Ereignisse an die entsprechenden Methoden anzuhängen.


Wasserstandsregler.

Einheitspaket

Wassertriangulation


Um Wasser zu triangulieren, brauchen wir ein neues Netz mit neuem Material. Erstellen Sie zunächst einen Wasser- Shader, der den River- Shader dupliziert. Ändern Sie es so, dass es die Farbeigenschaft verwendet.

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

Erstellen Sie mit diesem Shader ein neues Material, indem Sie das Wassermaterial duplizieren und durch einen Shader ersetzen. Lassen Sie die Rauschstruktur, da wir sie später verwenden werden.


Wassermaterial.

Fügen Sie dem Fertighaus ein neues Kind hinzu, indem Sie das Rivers- Kind duplizieren. Er braucht keine UV-Koordinaten und er muss Wasser verwenden . Wie üblich erstellen wir dazu eine Instanz des Fertighauses, ändern es und wenden die Änderungen dann auf das Fertighaus an. Entfernen Sie danach die Instanz.



Kinderobjekt Wasser.

Als nächstes fügen Sie HexGridChunk .

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

Und verbinden Sie es mit dem Fertighaus.


Das Wasserobjekt ist verbunden.

Sechsecke aus Wasser


Da Wasser eine zweite Schicht bildet, geben wir ihm für jede Richtung unsere eigene Triangulationsmethode. Wir müssen es nur nennen, wenn die Zelle in Wasser getaucht ist.

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

Wie bei Flüssen variiert die Höhe der Wasseroberfläche in Zellen mit gleichem Wasserstand nicht stark. Daher scheinen wir keine komplexen Rippen zu benötigen. Ein einfaches Dreieck wird ausreichen.

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


Sechsecke aus Wasser.

Wasserverbindungen


Wir können benachbarte Zellen mit Wasser mit einem Viereck verbinden.

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


Verbindungen der Ränder des Wassers.

Und füllen Sie die Ecken mit einem Dreieck.

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


Fugen von Wasserecken.

Jetzt haben wir Wasserzellen angeschlossen, wenn sie in der Nähe sind. Sie lassen eine Lücke zwischen sich und trockenen Zellen mit einer höheren Höhe, aber wir werden dies für später belassen.

Harmonisierte Wasserstände


Wir haben angenommen, dass benachbarte Unterwasserzellen den gleichen Wasserstand haben. Wenn dies so ist, sieht alles gut aus, aber wenn diese Annahme verletzt wird, treten Fehler auf.


Inkonsistenter Wasserstand.

Wir können dafür sorgen, dass das Wasser auf dem gleichen Niveau bleibt. Wenn sich beispielsweise der Wasserstand einer überfluteten Zelle ändert, können wir die Änderungen auf benachbarte Zellen übertragen, um die Wasserstände synchron zu halten. Dieser Prozess sollte jedoch fortgesetzt werden, bis er auf Zellen trifft, die nicht in Wasser eingetaucht sind. Diese Zellen definieren die Grenzen der Wassermasse.

Die Gefahr dieses Ansatzes besteht darin, dass er schnell außer Kontrolle geraten kann. Wenn die Bearbeitung nicht erfolgreich ist, kann Wasser die gesamte Karte bedecken. Dann müssen alle Fragmente gleichzeitig trianguliert werden, was zu einem enormen Anstieg der Verzögerungen führt.

Also lass es uns noch nicht tun. Diese Funktion kann in einem komplexeren Editor hinzugefügt werden. Während die Konsistenz des Wasserstandes, verlassen wir das Gewissen des Benutzers.

Einheitspaket

Wasseranimation


Anstelle einer einheitlichen Farbe werden wir etwas erzeugen, das Wellen ähnelt. Wie bei anderen Shadern werden wir vorerst nicht nach einer schönen Grafik streben, sondern nur die Wellen bezeichnen.


Perfekt flaches Wasser.

Machen wir das, was wir mit den Flüssen gemacht haben. Wir probieren das Rauschen mit der Position der Welt aus und fügen es einer einheitlichen Farbe hinzu. Fügen Sie der V-Koordinate Zeit hinzu, um die Oberfläche zu animieren.

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


Scrollendes Wasser, Zeit × 10.

Zwei Richtungen


Bisher ist das überhaupt nicht wie Wellen. Lassen Sie uns das Bild komplizieren, indem wir ein zweites Rauschmuster hinzufügen
und diesmal Hinzufügen der U-Koordinate. Wir verwenden einen anderen Rauschkanal, um als Ergebnis zwei verschiedene Muster zu erhalten. Die fertigen Wellen sind diese beiden zusammen gestapelten Samples.

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

Wenn wir beide Stichproben summieren, erhalten wir Ergebnisse im Intervall 0–2, daher müssen wir sie auf 0–1 zurückskalieren. Anstatt die Wellen einfach in zwei smoothstep zu teilen, können wir die smoothstep Funktion verwenden, um ein interessanteres Ergebnis zu smoothstep . Wir setzen ¾ - 2 auf 0–1, damit keine sichtbaren Wellen auf der Wasseroberfläche sind.

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


Zwei Richtungen, Zeit × 10.

Wellen des Mischens


Es fällt immer noch auf, dass wir zwei sich bewegende Geräuschmuster haben, die sich nicht wirklich ändern. Es wäre plausibler, wenn sich die Muster ändern würden. Wir können dies realisieren, indem wir zwischen verschiedenen Kanälen von Rauschproben interpolieren. Dies kann jedoch nicht auf die gleiche Weise erfolgen, da sich sonst gleichzeitig die gesamte Wasseroberfläche ändert, was sehr auffällig ist. Stattdessen werden wir eine Welle der Verwirrung erzeugen.

Wir werden mit Hilfe einer Sinuskurve eine Mischwelle erzeugen, die sich diagonal entlang der Wasseroberfläche bewegt. Wir werden dies tun, indem wir die Weltkoordinaten X und Z addieren und die Summe als Eingabe für die sin Funktion verwenden. Verkleinern Sie die Ansicht, um ausreichend große Bänder zu erhalten. Und natürlich fügen wir den gleichen Wert hinzu, um sie zu animieren.

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

Sinuswellen liegen zwischen -1 und 1, und wir benötigen ein Intervall von 0–1. Sie können es erhalten, indem Sie die Welle quadrieren. Um ein isoliertes Ergebnis anzuzeigen, verwenden Sie es anstelle der geänderten Farbe als Ausgabewert.

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


Wellen des Mischens.

Um die Mischwellen weniger wahrnehmbar zu machen, fügen Sie ihnen etwas Rauschen von beiden Samples hinzu.

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


Verzerrte Mischwellen.

Schließlich verwenden wir eine Mischwelle, um zwischen den beiden Kanälen beider Rauschproben zu interpolieren. Nehmen Sie für maximale Variation vier verschiedene Kanäle.

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


Wellen mischen, Zeit × 2.

Einheitspaket

Die Küste


Wir sind mit offenem Wasser fertig, aber jetzt müssen wir die Lücke im Wasser entlang der Küste füllen. Da wir uns an die Landkonturen anpassen müssen, erfordert das Küstenwasser einen anderen Ansatz. Teilen wir TriangulateWater in zwei Methoden ein - eine für offenes Wasser und eine für die Küste. Um zu verstehen, wann wir mit der Küste arbeiten, müssen wir uns die benachbarte Zelle ansehen. Das heißt, in TriangulateWater wir einen Nachbarn bekommen. Wenn es einen Nachbarn gibt und er nicht unter Wasser ist, dann haben wir es mit der Küste zu tun.

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


Es gibt keine Triangulation entlang der Küste.

Da die Küste verzerrt ist, müssen wir die Wasserdreiecke entlang der Küste verzerren. Deshalb brauchen wir die Kanten und einen Fächer mit Dreiecken.

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


Fans von Dreiecken entlang der Küste.

Als nächstes kommt ein Rippenstreifen, wie bei einem normalen Relief. Wir sind jedoch nicht verpflichtet, uns nur auf bestimmte Gebiete zu beschränken, da wir TriangulateWaterShore nur anrufen, wenn wir die Küste treffen, für die der Streifen immer benötigt wird.

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


Rippenstreifen entlang der Küste.

Ebenso müssen wir jedes Mal ein eckiges Dreieck hinzufügen.

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


Die Ecken der Rippen entlang der Küste.

Jetzt haben wir Wasser für die Küste bereit. Ein Teil davon befindet sich immer unterhalb des Reliefnetzes, sodass keine Löcher vorhanden sind.

UV-Küste


Wir können alles so lassen, wie es ist, aber es wäre interessant, wenn das Küstenwasser einen eigenen Zeitplan hätte. Zum Beispiel die Wirkung von Schaum, der bei Annäherung an die Küste größer wird. Um es zu implementieren, muss der Shader wissen, wie nahe das Fragment an der Küste liegt. Wir können diese Informationen über UV-Koordinaten übertragen.

Offenes Wasser hat keine UV-Koordinaten und benötigt keinen Schaum. Es wird nur für Wasser in Küstennähe benötigt. Daher sind die Anforderungen für beide Wassertypen sehr unterschiedlich. Es wäre logisch, für jeden Typ ein eigenes Netz zu erstellen. Aus diesem Grund fügen wir HexGridChunk Unterstützung für ein anderes Netzobjekt hinzu.

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

Dieses neue Netz verwendet TriangulateWaterShore .

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

Duplizieren Sie das Wasserobjekt, verbinden Sie es mit dem Fertighaus und stellen Sie es so ein, dass es UV-Koordinaten verwendet. Wir erstellen auch einen Shader und ein Material für Küstenwasser, wobei der vorhandene Shader und das vorhandene Wassermaterial dupliziert werden.


Wasseruferanlage und UV-Material.

Ändern Sie den Water Shore- Shader so, dass anstelle von Wasser UV-Koordinaten angezeigt werden.

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

Da noch keine Koordinaten festgelegt wurden, wird eine Volltonfarbe angezeigt. Dank dessen ist leicht zu erkennen, dass die Küste tatsächlich ein separates Netz mit Material verwendet.


Separates Netz für die Küste.

Lassen Sie uns die Küsteninformationen in Koordinate V einfügen. Weisen Sie ihr auf der Wasserseite den Wert 0 zu, auf der Landseite den Wert 1. Da wir nichts anderes übertragen müssen, sind alle U-Koordinaten einfach 0.

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


Übergänge zu den Küsten sind falsch.

Der obige Code funktioniert für Kanten, ist jedoch in einigen Winkeln falsch. Wenn der nächste Nachbar unter Wasser ist, ist dieser Ansatz korrekt. Wenn der nächste Nachbar nicht unter Wasser ist, befindet sich der dritte Gipfel des Dreiecks unter Land.

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


Übergänge zu den Küsten sind korrekt.

Schaum an der Küste


Nachdem die Übergänge zur Küste korrekt implementiert wurden, können Sie sie verwenden, um einen Schaumeffekt zu erzielen. Am einfachsten ist es, den Wert der Küste einer einheitlichen Farbe hinzuzufügen.

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


Linearer Schaum.

Um den Schaum interessanter zu machen, multiplizieren Sie ihn mit dem Quadrat der Sinuskurve.

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


Verblassender sinusförmiger quadratischer Schaum.

Lassen Sie uns die Schaumstofffront größer machen, wenn Sie sich dem Ufer nähern. Dies kann erreicht werden, indem die Quadratwurzel gezogen wird, bevor der Küstenwert verwendet wird.

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


Der Schaum wird in Ufernähe dicker.

Fügen Sie Verzerrungen hinzu, damit es natürlicher aussieht. Lassen Sie uns die Verzerrung schwächer machen, wenn wir uns der Küste nähern. Es wird also besser sein, die Küste zu säumen.

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


Schaum mit Verzerrung.

Und natürlich animieren wir all dies: sowohl eine Sinuskurve als auch Verzerrungen.

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


Animierter Schaum.

Neben dem ankommenden Schaum gibt es auch einen zurückgehenden. Fügen wir eine zweite Sinuskurve hinzu, die sich in die entgegengesetzte Richtung bewegt, um sie zu simulieren. Machen Sie es schwächer und fügen Sie eine Zeitverschiebung hinzu. Fertiger Schaum ist das Maximum dieser beiden Sinuskurven.

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


Eingehender und zurückgehender Schaum.

Mischung aus Wellen und Schaum


Es gibt einen scharfen Übergang zwischen offenem und Küstenwasser, da offene Wasserwellen nicht im Küstenwasser enthalten sind. Um dies zu beheben, müssen wir diese Wellen in den Water Shore- Shader aufnehmen.

Anstatt den Wave-Code zu kopieren, fügen wir ihn in die Water.cginc- Include-Datei ein. Tatsächlich fügen wir Code sowohl für Schaum als auch für Wellen als separate Funktion ein.

Wie funktionieren Shader-Include-Dateien?
Das Erstellen eigener Include-Shader-Dateien wird im Rendering 5- Lernprogramm Multiple Lights behandelt .

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

Ändern Sie den Water Shader so, dass er die neue Include-Datei verwendet.

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

Im Water Shore Shader werden Werte sowohl für Schaum als auch für Wellen berechnet. Dann dämpfen wir die Wellen, wenn wir uns dem Ufer nähern. Das Endergebnis ist ein Maximum an Schaum und Wellen.

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


Eine Mischung aus Schaum und Wellen.

Einheitspaket

Wieder über Küstenwasser


Ein Teil des Küstennetzes ist unter dem Reliefnetz versteckt. Dies ist normal, aber nur ein kleiner Teil ist verborgen. Leider verbergen steile Klippen den größten Teil des Küstenwassers und schäumen daher.


Fast verstecktes Küstenwasser.

Wir können damit umgehen, indem wir den Küstenstreifen vergrößern. Dies kann durch Verringern des Radius der Sechsecke des Wassers erfolgen. Dazu benötigen wir zusätzlich zum Integritätskoeffizienten einen HexMetrics Wasserkoeffizienten sowie Methoden zur HexMetrics Wasserwinkeln.

Der Integritätskoeffizient beträgt 0,8. Um die Größe der Wasserverbindungen zu verdoppeln, müssen wir den Wasserkoeffizienten auf 0,6 einstellen.

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

Wir werden diese neuen Methoden verwenden HexGridChunk, um die Winkel des Wassers zu finden.

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


Wasserecken benutzen.

Der Abstand zwischen den Sechsecken des Wassers hat sich tatsächlich verdoppelt. Jetzt HexMetricssollte es auch eine Methode zum Erstellen von Brücken im Wasser geben.

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

Ändern Sie, HexGridChunkdamit er die neue Methode verwendet.

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


Lange Brücken im Wasser.

Zwischen den Rippen von Wasser und Land


Obwohl dies uns mehr Platz für Schaum gibt, ist jetzt noch mehr unter dem Relief verborgen. Idealerweise können wir eine Wasserrippe auf der Wasserseite und eine Landrippe auf der Landseite verwenden.

Wir können keine einfache Brücke benutzen, um den gegenüberliegenden Rand des Landes zu finden, wenn wir von den Ecken des Wassers aus starten. Stattdessen können wir vom Zentrum des Nachbarn aus in die entgegengesetzte Richtung gehen. Ändern Sie TriangulateWaterShorediesen neuen Ansatz.

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


Falsche Kantenecken.

Dies hat funktioniert, erst jetzt müssen wir wieder zwei Fälle für eckige Dreiecke betrachten.

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


Die richtigen Ecken der Kanten.

Dies hat gut funktioniert, aber jetzt, da der größte Teil des Schaums sichtbar ist, wird er ziemlich ausgeprägt. Um dies zu kompensieren, werden wir den Effekt etwas schwächer machen, indem wir die Skala des Küstenwerts im Shader reduzieren.

  shore = sqrt(shore) * 0.9; 


Fertigschaum.

Einheitspaket

U-Boot-Flüsse


Wir haben Wasser bekommen, zumindest an den Stellen, an denen keine Flüsse hineinfließen. Da sich Wasser und Flüsse noch nicht bemerken, fließen Flüsse durch und unter Wasser.


Flüsse fließen im Wasser.

Die Reihenfolge, in der durchscheinende Objekte gerendert werden, hängt von ihrer Entfernung zur Kamera ab. Die nächsten Objekte werden zuletzt gerendert, sodass sie sich oben befinden. Wenn Sie die Kamera bewegen, bedeutet dies, dass manchmal Flüsse und manchmal Wasser übereinander erscheinen. Beginnen wir damit, die Renderreihenfolge konstant zu halten. Flüsse müssen über das Wasser gezogen werden, damit die Wasserfälle korrekt angezeigt werden. Wir können dies implementieren, indem wir die Warteschlange des River- Shaders ändern .

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


Wir zeichnen die Flüsse zuletzt.

Unterwasserfluss verstecken


Obwohl sich das Flussbett möglicherweise unter Wasser befindet und tatsächlich Wasser durch es fließen kann, sollten wir dieses Wasser nicht sehen. Und noch mehr, es sollte nicht auf einer echten Wasseroberfläche gerendert werden. Wir können das Wasser von U-Boot-Flüssen loswerden, indem wir nur dann Flusssegmente hinzufügen, wenn sich die aktuelle Zelle nicht unter Wasser befindet.

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

Zu TriangulateConnectionbeginnen , dass ein Segment des Flusses hinzufügen, wenn kein Strom, keine Nachbarzelle nicht unter Wasser ist.

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


Keine Unterwasserflüsse mehr.

Wasserfälle


Es gibt keine Unterwasserflüsse mehr, aber jetzt haben wir Löcher an den Stellen der Flüsse, an denen sie auf die Wasseroberfläche treffen. Flüsse auf gleicher Wasserhöhe bilden kleine Löcher oder Überlagerungen. Am auffälligsten sind jedoch die fehlenden Wasserfälle für Flüsse, die aus größerer Höhe fließen. Kümmern wir uns zuerst um sie.

Ein Flusssegment mit einem Wasserfall, der durch die Wasseroberfläche führte. Infolgedessen befand er sich teilweise über und teilweise unter Wasser. Wir müssen einen Teil über dem Wasserspiegel halten und alles andere wegwerfen. Sie müssen hart dafür arbeiten, also erstellen Sie eine separate Methode.

Die neue Methode erfordert vier Gipfel, zwei Flussniveaus und einen Wasserstand. Wir werden es so einstellen, dass wir in Richtung der Strömung den Wasserfall hinunter schauen. Daher befinden sich die ersten beiden Peaks sowie die linke und rechte Seite oben und die unteren folgen.

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

Wir werden diese Methode aufrufen, TriangulateConnectionwenn ein Nachbar unter Wasser ist und wir einen Wasserfall erstellen.

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

Wir müssen die Wasserfälle auch in die entgegengesetzte Richtung verarbeiten, wenn sich die aktuelle Zelle unter Wasser befindet und die nächste nicht.

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

Also bekommen wir wieder das Quad des ursprünglichen Flusses. Als nächstes müssen wir uns ändern TriangulateWaterfallInWater, damit die unteren Spitzen auf den Wasserspiegel angehoben werden. Leider reicht es nicht aus, nur die Y-Koordinaten zu ändern. Dies kann den Wasserfall von der Klippe schieben, wodurch Löcher entstehen können. Stattdessen müssen Sie die unteren Scheitelpunkte durch Interpolation zu den oberen verschieben.


Interpolieren.

Um die unteren Gipfel nach oben zu bewegen, teilen Sie ihren Abstand unter der Wasseroberfläche durch die Höhe des Wasserfalls. Dies gibt uns einen Interpolatorwert.

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

Als Ergebnis erhalten wir einen verkürzten Wasserfall mit der gleichen Ausrichtung. Da sich jedoch die Positionen der unteren Scheitelpunkte geändert haben, werden sie nicht wie die ursprünglichen Scheitelpunkte verzerrt. Dies bedeutet, dass das Endergebnis immer noch nicht mit dem ursprünglichen Wasserfall übereinstimmt. Um dieses Problem zu lösen, müssen wir die Scheitelpunkte vor der Interpolation manuell verzerren und dann das unverzerrte Quad hinzufügen.

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

Da wir bereits eine Methode zum Hinzufügen unverzerrter Dreiecke haben, müssen wir für Quads wirklich keine erstellen. Deshalb fügen wir die notwendige Methode hinzu HexMesh.AddQuadUnperturbed.

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


Wasserfälle enden an der Wasseroberfläche.

Einheitspaket

Flussmündungen


Wenn Flüsse auf der gleichen Höhe wie die Wasseroberfläche fließen, berührt das Flussnetz das Küstennetz. Wenn es ein Fluss wäre, der ins Meer oder in den Ozean fließt, dann würde es einen Strom des Flusses mit einer Brandung geben. Deshalb werden wir solche Gebiete Flussmündungen nennen.


Der Fluss trifft auf die Küste, ohne die Gipfel zu verzerren.

Jetzt haben wir zwei Probleme mit dem Mund. Erstens verbinden Quad-Flüsse die zweite und vierte Spitze der Rippen und überspringen die dritte. Da die Küste des Wassers nicht den dritten Gipfel nutzt, kann es zu einem Loch oder einer Überlappung kommen. Wir können dieses Problem lösen, indem wir die Geometrie der Münder ändern.

Das zweite Problem ist, dass es einen scharfen Übergang zwischen Schaum und Flussmaterialien gibt. Um es zu lösen, brauchen wir ein anderes Material, das die Auswirkungen von Fluss und Wasser vermischt.

Dies bedeutet, dass die Münder einen speziellen Ansatz erfordern. Erstellen wir daher eine separate Methode für sie. Es sollte aufgerufen werden, TriangulateWaterShorewenn sich ein Fluss in die aktuelle Richtung bewegt.

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

Ein Bereich, der beide Effekte mischt, ist nicht erforderlich, um den gesamten Streifen zu füllen. Die Trapezform wird für uns ausreichen. Daher können wir zwei Küstendreiecke an den Seiten verwenden.

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


Trapezloch für den Mischbereich.

UV2-Koordinaten


Um einen Flusseffekt zu erzielen, benötigen wir UV-Koordinaten. Um einen Schaumeffekt zu erzielen, benötigen Sie jedoch auch UV-Koordinaten. Das heißt, wenn wir sie mischen, benötigen wir zwei Sätze von UV-Koordinaten. Glücklicherweise können Unity-Engine-Meshes bis zu vier UV-Sets unterstützen. Wir müssen nur zur HexMeshUnterstützung des zweiten Satzes hinzufügen .

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

Um einen zweiten Satz UVs hinzuzufügen, duplizieren wir die Methoden zum Arbeiten mit UV und ändern die Art und Weise, wie wir sie benötigen.

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

River Shader-Funktion


Da wir den River-Effekt in zwei Shadern verwenden, verschieben wir den Code vom River- Shader in die neue Water Include-Dateifunktion .

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

Ändern Sie den River- Shader , um diese neue Funktion zu verwenden.

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

Mundgegenstände


Fügen Sie einen HexGridChunkMund hinzu , um das Netzobjekt zu stützen.

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

Erstellen Sie einen Shader, ein Material und ein Objekt des Mundes, duplizieren Sie die Küste und ändern Sie sie. Verbinden Sie es mit dem Fragment und lassen Sie es die UV- und UV2-Koordinaten verwenden.


Objekt Estuarties.

Mundtriangulation


Wir können das Loch- oder Überlappungsproblem lösen, indem wir ein Dreieck zwischen dem Ende des Flusses und der Mitte des Wasserrands platzieren. Da unser Mund-Shader ein Duplikat des Coast-Shaders ist, stellen wir die UV-Koordinaten so ein, dass sie dem Schaumeffekt entsprechen.

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


Mittleres Dreieck.

Wir können das gesamte Trapez füllen, indem wir auf beiden Seiten des mittleren Dreiecks ein Quad hinzufügen.

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


Fertiges Trapez.

Drehen wir die Quad-Ausrichtung nach links, so dass sie eine verkürzte diagonale Verbindung hat, und als Ergebnis erhalten wir eine symmetrische Geometrie.

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


Gedrehtes Quad, symmetrische Geometrie

Flussfluss


Um den Flusseffekt zu unterstützen, müssen wir UV2-Koordinaten hinzufügen. Der Grund des mittleren Dreiecks befindet sich in der Mitte des Flusses, daher sollte seine Koordinate U gleich 0,5 sein. Da der Fluss in Richtung des Wassers fließt, erhält der linke Punkt die U-Koordinate gleich 1 und der rechte die U-Koordinate mit einem Wert von 0. Wir setzen die Y-Koordinaten auf 0 und 1, entsprechend der Richtung des Stroms.

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

Die Vierecke auf beiden Seiten des Dreiecks sollten mit dieser Ausrichtung übereinstimmen. Wir behalten die gleichen U-Koordinaten für Punkte bei, die die Breite des Flusses überschreiten.

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


UV2-Trapez.

Um sicherzustellen, dass die UV2-Koordinaten korrekt eingestellt sind, lassen Sie sie vom Estuary- Shader rendern. Wir können auf diese Koordinaten zugreifen, indem wir sie zur Eingabestruktur hinzufügen float2 uv2_MainTex.

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


UV2-Koordinaten.

Alles sieht gut aus, Sie können einen Shader verwenden, um einen Flusseffekt zu erzeugen.

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


Verwenden Sie UV2, um einen Flusseffekt zu erzielen.

Wir haben die Flüsse so erstellt, dass sich beim Triangulieren der Verbindungen zwischen den Zellen die Koordinaten des V-Flusses von 0,8 auf 1 ändern. Daher sollten wir hier auch dieses Intervall verwenden und nicht von 0 auf 1. Die Küstenverbindung ist jedoch 50% höher als bei normalen Zellverbindungen . Für eine optimale Anpassung an den Flusslauf müssen wir daher die Werte von 0,8 auf 1,1 ändern.

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



Synchronisierter Fluss von Fluss und Mündung.

Durchflusseinstellung


Während sich der Fluss in einer geraden Linie bewegt. Wenn Wasser jedoch in ein größeres Gebiet fließt, dehnt es sich aus. Der Strom wird sich krümmen. Wir können dies simulieren, indem wir die UV2-Koordinaten falten.

Anstatt die oberen U-Koordinaten außerhalb der Flussbreite konstant zu halten, verschieben Sie sie um 0,5. Der Punkt ganz links ist 1,5, der Punkt ganz rechts ist –0,5.

Gleichzeitig erweitern wir den Fluss, indem wir die U-Koordinaten des linken und rechten unteren Punkts verschieben. Ändern Sie die linke von 1 auf 0,7 und die rechte von 0 auf 0,3.

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



Erweiterung des Flusses.

Um den Krümmungseffekt zu vervollständigen, ändern Sie die V-Koordinaten derselben vier Punkte. Da das Wasser vom Ende des Flusses wegfließt, erhöhen wir die Koordinaten von V der oberen Punkte auf 1. Um eine bessere Kurve zu erstellen, erhöhen wir die Koordinaten von V der beiden unteren Punkte auf 1,15.

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



Der geschwungene Flusslauf.

Fluss- und Küstenmischung


Wir müssen nur noch die Auswirkungen von Küste und Fluss mischen. Dazu verwenden wir die lineare Interpolation, wobei der Küstenwert als Interpolator verwendet wird.

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

Obwohl dies funktionieren sollte, wird möglicherweise ein Kompilierungsfehler angezeigt. Der Compiler beschwert sich über eine Neudefinition _MainTex_ST. Der Grund ist ein Fehler im Unity Surface Shader-Compiler, der durch die gleichzeitige Verwendung von uv_MainTexund verursacht wird uv2_MainTex. Wir müssen eine Problemumgehung finden.

Anstatt uv2_MainTexes zu verwenden, müssen wir die sekundären UV-Koordinaten manuell übertragen. Um dies zu tun, umbenennen uv2_MainTexin riverUV. Fügen Sie dann dem Shader eine Scheitelpunktfunktion hinzu, die ihm Koordinaten zuweist.

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


Interpolation basierend auf dem Küstenwert.

Die Interpolation funktioniert mit Ausnahme des linken und rechten Scheitelpunkts oben. An diesen Stellen sollte der Fluss verschwinden. Daher können wir den Wert der Küste nicht verwenden. Wir müssen einen anderen Wert verwenden, der an diesen beiden Eckpunkten 0 ist. Glücklicherweise haben wir immer noch die U-Koordinate des ersten UV-Satzes, sodass wir diesen Wert dort speichern können.

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


Die richtige Mischung.

Jetzt haben die Mündungen eine gute Mischung zwischen dem expandierenden Fluss, dem Küstenwasser und dem Schaum. Obwohl dies nicht genau zu Wasserfällen passt, sieht dieser Effekt auch bei Wasserfällen gut aus.


Mündungen in Aktion

Einheitspaket

Flüsse fließen aus Gewässern


Wir haben bereits Flüsse, die in Gewässer fließen, aber es gibt keine Unterstützung für Flüsse, die in eine andere Richtung fließen. Es gibt Seen, aus denen Flüsse fließen, also müssen wir sie auch hinzufügen.

Wenn ein Fluss aus einem Gewässer fließt, fließt er tatsächlich in eine höhere Höhe. Dies ist derzeit nicht möglich. Wir müssen eine Ausnahme machen und diese Situation zulassen, wenn der Wasserstand der Höhe des Zielpunkts entspricht. Fügen wir einer HexCellprivaten Methode hinzu, die nach unserem neuen Kriterium prüft, ob der Nachbar der richtige Zielpunkt für den abgehenden Fluss ist.

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

Wir werden unsere neue Methode verwenden, um festzustellen, ob es möglich ist, einen abgehenden Fluss zu erzeugen.

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

Außerdem müssen Sie dort den Fluss überprüfen, wenn Sie die Höhe der Zelle oder den Wasserstand ändern. Erstellen wir eine private Methode, die diese Aufgabe übernimmt.

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

Wir werden diese neue Methode in den Eigenschaften Elevationund verwenden WaterLevel.

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


Ausgehende und eintretende Flussseen.

Wende das Blatt


Wir haben geschaffen HexGridChunk.TriangulateEstuary, was darauf hindeutet, dass Flüsse nur in Gewässer fließen können. Daher bewegt sich der Flusslauf immer in eine Richtung. Wir müssen den Fluss umkehren, wenn es sich um einen Fluss handelt, der aus einem Gewässer fließt. Dazu müssen Sie TriangulateEstuarydie Strömungsrichtung kennen. Daher geben wir ihm einen Booleschen Parameter, der bestimmt, ob es sich um einen eingehenden Fluss handelt.

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

Wir werden diese Informationen übergeben, wenn wir diese Methode von aufrufen TriangulateWaterShore.

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

Jetzt müssen wir den Flussfluss erweitern, indem wir die Koordinaten von UV2 ändern. U-Koordinaten für ausgehende Flüsse müssen gespiegelt werden: –0,5 wird 1,5, 0 wird 1, 1 wird 0 und 1,5 wird –0,5.

Mit V-Koordinaten sind die Dinge etwas komplizierter. Wenn Sie sich ansehen, wie wir mit invertierten Flussverbindungen gearbeitet haben, sollte 0,8 0 und 1 –0,2 sein. Dies bedeutet, dass 1,1 zu –0,3 und 1,15 zu –0,35 wird.

Da die UV2-Koordinaten in jedem Fall sehr unterschiedlich sind, schreiben wir einen separaten Code für sie.

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


Der richtige Lauf der Flüsse.

Einheitspaket

Teil 9: Reliefmerkmale


  • Fügen Sie dem Relief Objekte hinzu.
  • Wir schaffen Unterstützung für die Dichte von Objekten.
  • Wir verwenden verschiedene Objekte im Level.
  • Mischen Sie drei verschiedene Arten von Objekten.

In diesem Teil werden wir über das Hinzufügen von Objekten zum Gelände sprechen. Wir werden Objekte wie Gebäude und Bäume erstellen.


Konflikt zwischen Wäldern, landwirtschaftlichen Flächen und Urbanisierung.

Unterstützung für Objekte hinzufügen


Obwohl die Form des Reliefs unterschiedlich ist, passiert bisher nichts darauf. Dies ist ein lebloses Land. Um ihm Leben einzuhauchen, müssen Sie solche Objekte hinzufügen. wie Bäume und Häuser. Diese Objekte sind nicht Teil des Reliefnetzes, sondern separate Objekte. Dies hindert uns jedoch nicht daran, sie beim Triangulieren des Geländes hinzuzufügen.

HexGridChunkEs ist mir egal, wie das Netz funktioniert. Er befiehlt einfach einem seiner Kinder, HexMeshein Dreieck oder Quad hinzuzufügen. Ebenso kann es ein untergeordnetes Element geben, das sich mit der Platzierung von Objekten auf ihnen befasst.

Objektmanager


Erstellen wir eine Komponente HexFeatureManager, die sich um Objekte innerhalb eines einzelnen Fragments kümmert. Wir verwenden das gleiche Schema wie in HexMesh- geben Sie ihm Methoden Clear, Applyund AddFeature. Da das Objekt irgendwo platziert werden muss, AddFeatureempfängt die Methode den Positionsparameter.

Wir werden mit einer leeren Implementierung beginnen, die vorerst nichts bewirken wird.

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

Jetzt können wir einen Link zu einer solchen Komponente in hinzufügen HexGridChunk. Dann können Sie es wie alle untergeordneten Elemente in den Triangulationsprozess einbeziehen HexMesh.

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

Beginnen wir mit der Platzierung eines Objekts in der Mitte jeder Zelle

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

Jetzt brauchen wir einen echten Objektmanager. Fügen Sie dem Fertighaus Hex Grid Chunk ein weiteres Kind hinzu und geben Sie ihm eine Komponente HexFeatureManager. Dann können Sie ein Fragment damit verbinden.




Ein Objektmanager, der dem Fragment-Fertighaus hinzugefügt wurde.

Fertighäuser


Welches Geländeobjekt werden wir erstellen? Für den ersten Test ist ein Würfel gut geeignet. Erstellen wir beispielsweise einen ausreichend großen Würfel mit einer Skala von (3, 3, 3) und verwandeln ihn in ein Fertighaus. Erstellen Sie auch Material für ihn. Ich habe das Standardmaterial mit Rot verwendet. Entfernen wir den Collider, weil wir ihn nicht brauchen.


Fertigwürfel.

Objektmanager benötigen einen Link zu diesem Fertighaus. Fügen Sie ihn hinzu HexFeatureManagerund verbinden Sie sie. Da zum Platzieren des Objekts Zugriff auf die Transformationskomponente erforderlich ist, verwenden wir es als Verknüpfungstyp.

  public Transform featurePrefab; 


Objektmanager mit Fertighaus.

Objektinstanzen erstellen


Die Struktur ist fertig und wir können mit dem Hinzufügen von Geländemerkmalen beginnen! Erstellen Sie einfach eine Instanz des Fertighauses HexFeatureManager.AddFeatureund legen Sie dessen Position fest.

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


Instanzen von Geländemerkmalen.

Von nun an wird das Gelände mit Würfeln gefüllt. Zumindest die oberen Hälften der Würfel, da der lokale Ursprung für das Würfelnetz in Unity in der Mitte des Würfels liegt und der Boden unter der Oberfläche des Reliefs liegt. Um Würfel auf der Topographie zu platzieren, müssen wir sie um die Hälfte ihrer Höhe nach oben bewegen.

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


Würfel auf der Oberfläche des Reliefs.

Was ist, wenn wir ein anderes Netz verwenden?
. , , . .

Natürlich sind unsere Zellen verzerrt, daher müssen wir die Position von Objekten verzerren. So werden wir die perfekte Wiederholbarkeit des Netzes los.

  instance.localPosition = HexMetrics.Perturb(position); 


Verzerrte Positionen von Objekten.

Zerstörung von Hilfsgegenständen


Jedes Mal, wenn ein Fragment aktualisiert wird, erstellen wir neue Reliefobjekte. Dies bedeutet, dass wir immer mehr Objekte an denselben Positionen erstellen. Um Duplikate zu vermeiden, müssen wir beim Reinigen eines Fragments alte Objekte entfernen.

Der schnellste Weg, dies zu tun, besteht darin, ein Spielcontainerobjekt zu erstellen und alle Reliefobjekte in seine untergeordneten Objekte umzuwandeln. Wenn Clearwir dann aufgerufen werden, zerstören wir diesen Container und erstellen einen neuen. Der Container selbst ist ein Kind seines Managers.

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

Wahrscheinlich ist es ineffizient, jedes Mal Reliefobjekte zu erstellen und zu zerstören.
, , . . . , , , . HexFeatureManager.Apply . . , , .

Einheitspaket

Platzierung von Reliefobjekten


Während wir Objekte in der Mitte jeder Zelle platzieren. Für leere Zellen sieht dies normal aus, aber für Zellen, die Flüsse und Straßen enthalten und mit Wasser überflutet sind, scheint es seltsam.


Objekte sind überall.

Überprüfen Sie daher vor dem Platzieren des Objekts, HexGridChunk.Triangulateob die Zelle leer ist.

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


Begrenzte Unterkunft.

Ein Objekt pro Richtung


Nur ein Objekt pro Zelle ist nicht zu viel. Es gibt immer noch viel Platz für ein paar Gegenstände. Daher fügen wir der Mitte jedes der sechs Dreiecke der Zelle ein zusätzliches Objekt hinzu, dh eines pro Richtung.

Wir werden dies auf eine andere Weise tun Triangulate, wenn wir wissen, dass sich kein Fluss in der Zelle befindet. Wir müssen noch überprüfen, ob wir unter Wasser sind und ob sich eine Straße in der Zelle befindet. In diesem Fall interessieren wir uns jedoch nur für Straßen, die in die aktuelle Richtung verlaufen.

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


Viele Einrichtungen, aber nicht in der Nähe von Flüssen.

Dadurch entstehen viel mehr Objekte! Sie erscheinen in der Nähe der Straßen, meiden aber dennoch die Flüsse. Um Objekte entlang von Flüssen zu platzieren, können wir sie auch hinzufügen TriangulateAdjacentToRiver. Aber wieder nur, wenn das Dreieck nicht unter Wasser ist und keine Straße darauf ist.

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


Objekte tauchten neben den Flüssen auf.

Ist es möglich, so viele Objekte zu rendern?
, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.

Einheitspaket

Vielzahl von Objekten


Alle unsere Reliefobjekte haben die gleiche Ausrichtung, was völlig unnatürlich aussieht. Lassen Sie uns jedem eine zufällige Wendung geben.

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


Zufällige Drehungen.

Das Ergebnis wird also viel vielfältiger. Leider erhalten Objekte jedes Mal, wenn ein Fragment aktualisiert wird, eine neue zufällige Drehung. Das Bearbeiten von Zellen sollte keine Objekte in der Nachbarschaft ändern, daher benötigen wir einen anderen Ansatz.

Wir haben eine Rauschstruktur, die immer gleich ist. Diese Textur enthält jedoch Perlin-Gradientenrauschen und ist lokal konsistent. Dies ist genau das, was wir brauchen, wenn wir die Positionen von Eckpunkten in Zellen verzerren. Aber die Kurven müssen nicht konsistent sein. Alle Windungen sollten gleich wahrscheinlich und gemischt sein. Daher benötigen wir eine Textur mit zufälligen Werten ohne Gradienten, die ohne bilineare Filterung abgetastet werden können. Im Wesentlichen handelt es sich hierbei um ein Hash-Gitter, das die Grundlage für Gradientenrauschen bildet.

Erstellen einer Hash-Tabelle


Wir können eine Hash-Tabelle aus einem Array von Float-Werten erstellen und sie einmal mit zufälligen Werten füllen. Dank dessen brauchen wir überhaupt keine Textur. Fügen wir es hinzu HexMetrics. Eine Größe von 256 x 256 reicht für eine ausreichende Variation aus.

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

Zufallswerte werden durch eine mathematische Formel generiert, die immer die gleichen Ergebnisse liefert. Die resultierende Sequenz hängt von der Anzahl der Seeds ab, die standardmäßig dem aktuellen Zeitwert entspricht. Deshalb erhalten wir in jeder Spielsitzung unterschiedliche Ergebnisse.

Um sicherzustellen, dass immer identische Objekte neu erstellt werden, müssen wir den Startparameter zur Initialisierungsmethode hinzufügen.

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

Nachdem wir den Zufallszahlenstrom initialisiert haben, erhalten wir immer die gleiche Reihenfolge. Daher sind scheinbar zufällige Ereignisse, die nach der Erstellung der Karte auftreten, auch immer gleich. Wir können dies vermeiden, indem wir den Status des Zufallszahlengenerators speichern, bevor wir ihn initialisieren. Nach Abschluss der Arbeiten können wir ihn nach dem alten Zustand fragen.

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

Die Hash-Tabelle wird HexGridgleichzeitig mit der Zuweisung der Rauschtextur initialisiert . Das heißt, in den Methoden HexGrid.Startund HexGrid.Awake. Wir machen es so, dass die Werte nicht öfter als nötig generiert werden.

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

Mit der generischen Startvariablen können wir den Startwert für die Karte auswählen. Jeder Wert reicht aus. Ich habe 1234 gewählt.


Die Wahl des Samens.

Verwenden einer Hash-Tabelle


Fügen Sie der HexMetricsStichprobenmethode hinzu, um die Hash-Tabelle zu verwenden. Ebenso SampleNoisewerden die Koordinaten der XZ-Position verwendet, um den Wert zu erhalten. Der Hash-Index wird ermittelt, indem die Koordinaten auf ganzzahlige Werte beschränkt werden und dann der Rest der ganzzahligen Division durch die Größe der Tabelle ermittelt wird.

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

Was macht %?
, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.

Dies funktioniert für positive Koordinaten, aber nicht für negative, da für solche Zahlen der Rest negativ ist. Wir können dies beheben, indem wir die Größe der Tabelle zu den negativen Ergebnissen hinzufügen.

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

Jetzt schaffen wir für jede quadratische Einheit unseren eigenen Wert. Tatsächlich benötigen wir jedoch keine solche Tabellendichte. Objekte sind voneinander beabstandet. Wir können die Tabelle dehnen, indem wir die Positionsskala reduzieren, bevor wir den Index berechnen. Ein eindeutiger Wert für ein 4 x 4-Quadrat reicht für uns aus.

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

Gehen wir zurück zu HexFeatureManager.AddFeatureund verwenden Sie unsere neue Hash-Tabelle, um den Wert zu erhalten. Nachdem wir es angewendet haben, um die Drehung festzulegen, bleiben die Objekte beim Bearbeiten des Geländes stationär.

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

Platzierungsschwelle


Obwohl Objekte unterschiedliche Rotationen haben, ist ein Muster in ihrer Platzierung immer noch erkennbar. Jede Zelle hat sieben Objekte. Wir können diesem Schema Chaos hinzufügen und einige der Objekte willkürlich überspringen. Wie entscheiden wir, ob wir ein Objekt hinzufügen oder nicht? Natürlich einen anderen zufälligen Wert auschecken!

Das heißt, anstelle eines Hashwerts benötigen wir zwei. Ihre Unterstützung kann durch Verwendung von Hashes anstelle einer floatVariablen als Tabellenarraytyp hinzugefügt werden Vector2. Da Vektoroperationen für Hash-Werte jedoch keinen Sinn ergeben, erstellen wir zu diesem Zweck eine spezielle Struktur. Sie benötigt nur zwei Float-Werte. Fügen wir eine statische Methode hinzu, um ein Paar zufälliger Werte zu erstellen.

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

Muss es nicht serialisiert werden?
, , Unity. , .

Ändern Sie es HexMetricsso, dass es die neue Struktur verwendet.

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

Hat jetzt HexFeatureManager.AddFeatureZugriff auf zwei Hashwerte. Lassen Sie uns zunächst entscheiden, ob ein Objekt hinzugefügt oder übersprungen werden soll. Wenn der Wert gleich oder größer als 0,5 ist, überspringen Sie. Auf diese Weise werden wir etwa die Hälfte der Objekte entfernen. Der zweite Wert wird wie gewohnt zur Bestimmung der Drehung verwendet.

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


Die Dichte von Objekten wird um 50% reduziert.

Einheitspaket

Objekte zeichnen


Anstatt Objekte überall zu platzieren, können Sie sie bearbeitbar machen. Wir werden jedoch keine separaten Objekte zeichnen, sondern jeder Zelle die Objektebene hinzufügen. Diese Stufe steuert die Wahrscheinlichkeit, dass Objekte in der Zelle erscheinen. Standardmäßig ist der Wert Null, dh Objekte fehlen.

Da rote Würfel in unserem Gelände nicht wie natürliche Objekte aussehen, nennen wir sie Gebäude. Sie werden die Urbanisierung darstellen. Fügen wir den HexCellGrad der Urbanisierung hinzu.

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

Wir können den Urbanisierungsgrad für eine Unterwasserzelle auf Null setzen, aber das ist nicht notwendig, wir überspringen trotzdem die Erstellung von Unterwasserobjekten. Und vielleicht werden wir irgendwann Gewässer der Urbanisierung hinzufügen, wie Docks und Unterwasserstrukturen.

Dichteschieberegler


Um den Grad der Urbanisierung zu ändern, fügen wir HexMapEditoreinen weiteren Schieberegler zur Unterstützung hinzu.

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

Fügen Sie der Benutzeroberfläche einen weiteren Schieberegler hinzu und kombinieren Sie ihn mit den entsprechenden Methoden. Ich werde ein neues Feld auf der rechten Seite des Bildschirms platzieren, um ein Überlaufen des linken Feldes zu vermeiden.

Wie viele Level brauchen wir? Lassen Sie uns bei vier verweilen und Null, niedrige, mittlere und hohe Dichte bezeichnen.



Urbanisierungsregler.

Schwellenwertänderung


Jetzt, da wir den Grad der Urbanisierung haben, müssen wir ihn verwenden, um zu bestimmen, ob Objekte platziert werden sollen. Dazu müssen wir den Urbanisierungsgrad als zusätzlichen Parameter hinzufügen HexFeatureManager.AddFeature. Machen wir noch einen Schritt und übertragen einfach die Zelle selbst. In Zukunft wird es für uns bequemer sein.

Der schnellste Weg, den Urbanisierungsgrad zu verwenden, besteht darin, ihn mit 0,25 zu multiplizieren und den Wert als neuen Schwellenwert für das Überspringen von Objekten zu verwenden. Aus diesem Grund erhöht sich die Wahrscheinlichkeit des Auftretens des Objekts mit jedem Level um 25%.

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

Damit dies funktioniert, übergeben wir die Zellen an HexGridChunk.

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


Zeichnen der Urbanisierungsdichte.

Einheitspaket

Mehrere Fertighäuser von Reliefobjekten


Unterschiede in der Wahrscheinlichkeit des Auftretens von Objekten reichen nicht aus, um eine klare Trennung zwischen niedrigem und hohem Urbanisierungsgrad zu erreichen. In einigen Zellen wird es einfach mehr oder weniger als die erwartete Anzahl von Gebäuden geben. Wir können den Unterschied klarer machen, indem wir für jedes Level unser eigenes Fertighaus verwenden.

Wir werden das Feld featurePrefabin loswerden HexFeatureManagerund es durch ein Array für Fertighäuser der Urbanisierung ersetzen. Um das passende Fertighaus zu erhalten, subtrahieren wir eines vom Urbanisierungsgrad und verwenden den Wert als Index.

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

Erstellen Sie zwei Duplikate des Fertighauses des Objekts, benennen Sie es um und ändern Sie es so, dass es drei verschiedene Urbanisierungsebenen anzeigt. Stufe 1 ist eine niedrige Dichte, daher verwenden wir einen Würfel mit einer Einheitslänge einer Kante, der eine Hütte bezeichnet. Ich werde das Fertighaus der Ebene 2 auf (1,5, 2, 1,5) skalieren, damit es wie ein zweistöckiges Gebäude aussieht. Für hohe Gebäude der Stufe 3 habe ich die Skala (2, 5, 2) verwendet.



Verwendung unterschiedlicher Fertighäuser für jede Urbanisierungsstufe.

Fertigmischung


Wir sind nicht verpflichtet, uns auf eine strikte Trennung der Gebäudetypen zu beschränken. Sie können sie ein wenig mischen, wie es in der realen Welt passiert. Verwenden Sie anstelle eines Schwellenwerts pro Ebene drei, einen für jeden Gebäudetyp.

Auf Stufe 1 verwenden wir in 40% der Fälle die Platzierung von Hütten. Es wird hier überhaupt keine anderen Gebäude geben. Für die Ebene verwenden wir die drei Werte (0,4, 0, 0).

Ersetzen Sie auf Stufe 2 die Hütten durch größere Gebäude und erhöhen Sie die Wahrscheinlichkeit für zusätzliche Hütten um 20%. Wir werden keine hohen Gebäude bauen. Das heißt, wir verwenden den Schwellenwert drei Werte (0,2, 0,4, 0).

Auf Stufe 3 ersetzen wir mittlere Gebäude durch hohe, ersetzen erneut Hütten und erhöhen die Wahrscheinlichkeit von Hütten um weitere 20%. Die Schwellenwerte sind gleich (0,2, 0,2, 0,4).

Das heißt, die Idee ist, dass wir mit zunehmendem Urbanisierungsgrad bestehende Gebäude aufwerten und leere Orte um neue erweitern werden. Um ein vorhandenes Gebäude zu entfernen, müssen dieselben Hashwertintervalle verwendet werden. Wenn die Hashes zwischen 0 und 0,4 auf Ebene 1 Hütten waren, werden auf Ebene 3 im gleichen Intervall hohe Gebäude erstellt. Auf Stufe 3 sollten hohe Gebäude mit Hashes im Bereich von 0 bis 0,4, zweistöckigen Gebäuden im Bereich von 0,4 bis 0,6 und Hütten im Bereich von 0,6 bis 0,8 erstellt werden. Wenn Sie sie vom größten zum kleinsten prüfen, kann dies mit dem Dreifach der Schwellenwerte (0,4, 0,6, 0,8) erfolgen. Die Schwellenwerte der Stufe 2 werden dann (0, 0,4, 0,6) und die Schwellenwerte der Stufe 1 werden (0, 0, 0,4).

Speichern wir diese Schwellenwerte inHexMetricsals Sammlung von Arrays mit einer Methode, mit der Sie Schwellenwerte für eine bestimmte Ebene abrufen können. Da wir nur an Ebenen mit Objekten interessiert sind, ignorieren wir Ebene 0.

  static float[][] featureThresholds = { new float[] {0.0f, 0.0f, 0.4f}, new float[] {0.0f, 0.4f, 0.6f}, new float[] {0.4f, 0.6f, 0.8f} }; public static float[] GetFeatureThresholds (int level) { return featureThresholds[level]; } 

HexFeatureManagerFügen Sie als Nächstes der Methode hinzu, die die Hash-Ebene und den Wert verwendet, um das Fertighaus auszuwählen. Wenn der Pegel größer als Null ist, erhalten wir Schwellenwerte mit einem um eins reduzierten Pegel. Dann durchlaufen wir die Schwellenwerte, bis einer von ihnen den Hashwert überschreitet. Dies bedeutet, dass wir ein Fertighaus gefunden haben. Wenn wir nicht finden, geben Sie null zurück.

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

Dieser Ansatz erfordert die Neuordnung von Links zu Fertighäusern, sodass diese von hoher zu niedriger Dichte wechseln.


Invertierte Fertighausreihenfolge.

Wir werden unsere neue Methode verwenden AddFeature, um ein Fertighaus auszuwählen. Wenn wir es nicht erhalten, überspringen wir das Objekt. Andernfalls erstellen Sie eine Instanz davon und fahren Sie wie zuvor fort.

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); // if (hash.a >= cell.UrbanLevel * 0.25f) { // return; // } // Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f); instance.SetParent(container, false); } 


Mischen Sie die Fertighäuser.

Pegelschwankungen


Jetzt haben wir gut gemischte Gebäude, aber bisher gibt es nur drei. Wir können die Variabilität weiter erhöhen, indem wir eine Sammlung von Fertighäusern mit jeder Ebene der Urbanisierungsdichte verknüpfen. Danach ist es möglich, eine davon zufällig auszuwählen. Dadurch wird einen neuen Zufallswert erfordern, so ein dritten hinzuzufügen HexHash.

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

Lassen Sie es uns HexFeatureManager.urbanPrefabsin ein Array von Arrays verwandeln und der Methode einen PickPrefabParameter hinzufügen choice. Wir verwenden es, um den Index des eingebauten Arrays auszuwählen, ihn mit der Länge dieses Arrays zu multiplizieren und in eine Ganzzahl umzuwandeln.

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

Begründen wir unsere Wahl mit dem Wert des zweiten Hashs (B). Dann müssen Sie von B nach C abbiegen.

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a, hash.b); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.c, 0f); instance.SetParent(container, false); } 

Bevor wir fortfahren, müssen wir überlegen, was Random.valueeinen Wert von 1 zurückgeben könnte. Aus diesem Grund kann der Array-Index darüber hinausgehen. Um dies zu verhindern, skalieren wir die Hash-Werte leicht. Wir skalieren sie einfach alle, um uns keine Gedanken über die von uns verwendeten Besonderheiten zu machen.

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

Leider zeigt der Inspektor keine Arrays von Arrays an. Daher können wir sie nicht konfigurieren. Um diese Einschränkung zu umgehen, erstellen Sie eine serialisierbare Struktur, in die das integrierte Array eingekapselt werden kann. Geben wir ihr eine Methode, die von der Auswahl in den Array-Index konvertiert und ein Fertighaus zurückgibt.

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

Wir verwenden HexFeatureManageranstelle von eingebauten Arrays ein Array solcher Sammlungen.

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

Jetzt können wir jeder Dichtestufe mehrere Gebäude zuweisen. Da sie unabhängig sind, müssen wir nicht den gleichen Betrag pro Level verwenden. Ich habe nur zwei Optionen pro Level verwendet und jeder eine längere niedrigere Option hinzugefügt. Ich habe die Skalen für sie (3,5, 3, 2), (2,75, 1,5, 1,5) und (1,75, 1, 1) gewählt.



Zwei Gebäudetypen pro Dichtestufe.

Einheitspaket

Verschiedene Arten von Objekten


In dem bestehenden Schema können wir durchaus würdige städtische Strukturen schaffen. Das Relief kann aber nicht nur Gebäude enthalten. Wie wäre es mit Bauernhöfen oder Pflanzen? Lassen Sie uns zu den HexCellEbenen und für sie hinzufügen . Sie schließen sich nicht gegenseitig aus und können sich mischen.

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

Dies erfordert natürlich die Unterstützung von HexMapEditorzwei zusätzlichen Schiebereglern.

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

Fügen Sie sie der Benutzeroberfläche hinzu.


Drei Schieberegler.

Außerdem werden zusätzliche Sammlungen benötigt HexFeatureManager.

  public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections; 


Drei Sammlungen von Reliefobjekten.

Ich habe sowohl für Farmen als auch für Pflanzen zwei Fertighäuser pro Dichtestufe sowie für den Bau von Sammlungen erstellt. Für alle habe ich Würfel verwendet. Bauernhöfe haben hellgrünes Material, Pflanzen haben dunkelgrünes Material.

Ich habe Farmwürfel mit einer Höhe von 0,1 Einheiten hergestellt, um quadratische Parzellen landwirtschaftlicher Flächen anzuzeigen. Als hochdichte Skalen habe ich (2,5, 0,1, 2,5) und (3,5, 0,1, 2) gewählt. Im Durchschnitt haben Standorte eine Fläche von 1,75 und eine Größe von 2,5 x 1,25. Im Bereich 1 wurde eine geringe Dichte und eine Größe von 1,5 mal 0,75 erhalten.

Fertige Pflanzen bezeichnen hohe Bäume und große Sträucher. Fertighäuser mit hoher Dichte sind die größten (1,25, 4,5, 1,25) und (1,5, 3, 1,5). Die durchschnittlichen Skalen sind (0,75, 3, 0,75) und (1, 1,5, 1). Die kleinsten Pflanzen haben Größen (0,5, 1,5, 0,5) und (0,75, 1, 0,75).

Auswahl der Reliefmerkmale


Jeder Objekttyp muss einen eigenen Hashwert erhalten, damit sie unterschiedliche Erstellungsmuster haben und Sie sie mischen können. Fügen Sie HexHashzwei zusätzliche Werte hinzu.

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

Jetzt müssen Sie HexFeatureManager.PickPrefabmit verschiedenen Sammlungen arbeiten. Fügen Sie einen Parameter hinzu, um den Vorgang zu vereinfachen. Ändern Sie außerdem den von der Variante des ausgewählten Fertighauses verwendeten Hash in D und den Hash für die Rotation in E.

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

Derzeit AddFeaturewählt vorgefertigte Urbanisierung. Das ist normal, wir brauchen mehr Optionen. Deshalb fügen wir ein weiteres Fertighaus von den Farmen hinzu. Verwenden Sie als Hash-Wert B. Die Option ist wieder D.

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

Welche Art von Fertiginstanz werden wir als Ergebnis erstellen? Wenn sich herausstellt, dass einer von ihnen null ist, liegt die Wahl auf der Hand. Wenn jedoch beide existieren, müssen wir eine Entscheidung treffen. Fügen wir einfach das Fertighaus mit dem niedrigsten Hashwert hinzu.

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


Eine Mischung aus städtischen und ländlichen Objekten.
Machen Sie dasselbe mit den Pflanzen, indem Sie den Wert des C-Hash verwenden.

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

Wir können den Code jedoch nicht einfach kopieren. Wenn wir ländliches statt städtisches Objekt wählen, müssen wir den Hasch der Pflanzen mit dem Hasch der Farmen vergleichen und nicht mit dem städtischen. Daher müssen wir den Hash verfolgen, den wir ausgewählt und mit ihm verglichen haben.

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


Eine Mischung aus städtischen, ländlichen und pflanzlichen Objekten.

Einheitspaket

Teil 10: Wände


  • Wir schließen die Zellen ein.
  • Wir bauen Mauern entlang der Ränder der Zellen.
  • Lass uns durch Flüsse und Straßen gehen.
  • Vermeiden Sie Wasser und verbinden Sie sich mit Klippen.

In diesem Teil werden wir zwischen den Zellen der Wand hinzufügen.


Nichts ist einladender als eine hohe Mauer.

Wandbearbeitung


Um die Wände zu stützen, müssen wir wissen, wo sie platziert werden müssen. Wir werden sie zwischen die Zellen entlang der Kanten legen, die sie verbinden. Da sich bereits vorhandene Objekte im zentralen Teil der Zellen befinden, müssen wir uns keine Sorgen machen, dass die Wände durch sie hindurchgehen.


Wände entlang der Ränder.

Wände sind Geländeobjekte, obwohl sie groß sind. Wie andere Objekte werden wir sie nicht direkt bearbeiten. Stattdessen werden wir die Zellen ändern. Wir werden keine getrennten Segmente der Wände haben, sondern die Zellen als Ganzes einschließen.

Ummauertes Eigentum


Fügen Sie der HexCellEigenschaft hinzu, um eingezäunte Zellen zu unterstützen Walled. Dies ist ein einfacher Schalter. Da sich die Wände zwischen den Zellen befinden, müssen wir sowohl die bearbeiteten Zellen als auch ihre Nachbarn aktualisieren.

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

Editor wechseln


Um den "eingezäunten" Zustand der Zellen zu ändern, müssen wir HexMapEditorUnterstützung für den Schalter hinzufügen . Daher fügen wir ein weiteres Feld OptionalToggleund eine Methode zum Festlegen hinzu.

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

Im Gegensatz zu Flüssen und Straßen verlaufen Mauern nicht von Zelle zu Zelle, sondern befinden sich zwischen ihnen. Daher müssen wir nicht über Drag & Drop nachdenken. Wenn der Wandschalter aktiv ist, setzen wir einfach den eingezäunten Zustand der aktuellen Zelle basierend auf dem Zustand dieses Schalters.

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

Wir duplizieren eines der vorherigen Elemente der UI-Schalter und ändern sie so, dass sie den Status des "Fechtens" steuern. Ich werde sie zusammen mit anderen Objekten in das UI-Bedienfeld einfügen.


Der Schalter "Fechten".

Einheitspaket

Wände schaffen


Da die Wände den Konturen der Zellen folgen, sollten sie keine konstante Form haben. Daher können wir nicht einfach ein Fertighaus für sie verwenden, wie wir es bei anderen Geländemerkmalen getan haben. Stattdessen müssen wir ein Netz bauen, wie wir es mit dem Relief getan haben. Dies bedeutet, dass unser Fertighausfragment ein weiteres untergeordnetes Element benötigt HexMesh. Duplizieren Sie eines der anderen untergeordneten Netze und lassen Sie die neuen Walls- Objekte Schatten werfen. Sie benötigen nur Scheitelpunkte und Dreiecke, daher müssen alle Optionen HexMeshdeaktiviert werden.



Tochtergefertigte Fertigmauern.

Es wird logisch sein, dass die Wände ein städtisches Objekt sind, deshalb habe ich für sie das rote Material der Gebäude verwendet.

Wandverwaltung


Da die Wände Reliefobjekte sind, müssen sie sich mit ihnen befassen HexFeatureManager. Daher wird der Manager Relief Objekt Referenz auf das Objekt geben Walls , und stellen Sie sicher , dass die Methoden aufrufen Clearund Apply.

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


Mit dem Topografiemanager verbundene Wände.

Sollten Wände nicht ein Kind von Features sein?
, . , Walls Hex Grid Chunk .

Jetzt müssen wir dem Manager eine Methode hinzufügen, mit der wir ihm Wände hinzufügen können. Da sich die Wände entlang der Kanten zwischen den Zellen befinden, muss er die entsprechenden Eckpunkte der Kanten und Zellen kennen. HexGridChunkwird es TriangulateConnectionzum Zeitpunkt der Triangulation der Zelle und eines ihrer Nachbarn durch verursachen. Unter diesem Gesichtspunkt befindet sich die aktuelle Zelle auf der nahen Seite der Wand und die andere auf der anderen Seite.

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

Wir werden diese neue Methode HexGridChunk.TriangulateConnectionnach Abschluss aller anderen Verbindungsarbeiten und unmittelbar vor dem Übergang zum Winkeldreieck aufrufen. Wir lassen den Manager der Reliefobjekte selbst entscheiden, wo sich die Wand tatsächlich befinden soll.

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

Bauen Sie ein Wandsegment


Die gesamte Wand schlängelt sich durch mehrere Kanten der Zellen. Jede Kante enthält nur ein Wandelement. Aus Sicht der nahen Zelle beginnt das Segment auf der linken Seite der Rippe und endet auf der rechten Seite. Fügen wir eine HexFeatureManagerseparate Methode hinzu, die vier Eckpunkte an den Ecken einer Kante verwendet.

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


Nahe und ferne Seite.

AddWallkann diese Methode mit der ersten und letzten Kante der Kanten aufrufen. Wände sollten jedoch nur hinzugefügt werden, wenn eine Verbindung zwischen einer eingezäunten Zelle und einer nicht eingezäunten Zelle besteht. Es spielt keine Rolle, welche der Zellen sich innerhalb und welche außerhalb befindet, nur der Unterschied in ihren Zuständen wird berücksichtigt.

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

Das einfachste Segment der Wand ist ein Quad, das in der Mitte der Rippe steht. Wir werden seine unteren Spitzen finden, die von den nächstgelegenen zu den am weitesten entfernten Spitzen in die Mitte interpolieren.

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

Wie hoch sollte die Wand sein? Stellen wir die Höhe auf ein HexMetrics. Ich habe sie auf die Größe einer Zellenhöhe gebracht.

  public const float wallHeight = 3f; 

HexFeatureManager.AddWallSegmentMit dieser Höhe können Sie den dritten und vierten Scheitelpunkt des Quad positionieren und dem Netz hinzufügen walls.

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

Jetzt können wir die Wände bearbeiten und sie werden als Quad-Streifen angezeigt. Wir werden jedoch keine durchgehende Wand sehen. Jedes Quad ist nur auf einer Seite sichtbar. Sein Gesicht ist auf die Zelle gerichtet, aus der es hinzugefügt wurde.


Einseitige Quadwände.

Wir können dieses Problem schnell lösen, indem wir ein zweites Quad hinzufügen, das in die andere Richtung zeigt.

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


Bilaterale Wände.

Jetzt sind alle Wände in ihrer Gesamtheit sichtbar, aber es gibt immer noch Löcher in den Ecken der Zellen, in denen sich die drei Zellen treffen. Wir werden sie später ausfüllen.

Dicke Wände


Obwohl die Wände bereits auf beiden Seiten sichtbar sind, haben sie keine Dicke. Tatsächlich sind die Wände dünn wie Papier und in einem bestimmten Winkel fast unsichtbar. Machen wir sie also ganz, indem wir die Dicke hinzufügen. Stellen Sie ihre Dicke ein HexMetrics. Ich habe einen Wert von 0,75 Einheiten gewählt, es schien mir geeignet.

  public const float wallThickness = 0.75f; 

Um zwei Wände dick zu machen, müssen Sie zwei Quads an den Seiten trennen. Sie sollten sich in entgegengesetzte Richtungen bewegen. Eine Seite sollte sich zur nahen Kante bewegen, die andere zur fernen Kante. Der Versatzvektor hierfür ist gleich far - near, aber um die Oberseite der Wand flach zu lassen, müssen wir ihre Komponente Y auf 0 setzen.

Da dies sowohl für die linke als auch für die rechte Seite des Wandsegments erfolgen muss, fügen wir der HexMetricsMethode einen Versatzvektor hinzu, um dies zu berechnen.

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

Damit die Wand in der Mitte der Rippe bleibt, sollte der tatsächliche Bewegungsabstand entlang dieses Vektors der halben Dicke für jede Seite entsprechen. Und um sicherzustellen, dass wir wirklich die richtige Strecke zurückgelegt haben, normalisieren wir den Verschiebungsvektor, bevor wir ihn skalieren.

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

Wir verwenden diese Methode HexFeatureManager.AddWallSegment, um die Position der Quads zu ändern. Da der Verschiebungsvektor von der nächsten zur fernen Zelle geht, subtrahieren Sie ihn vom nahen Quad und addieren Sie zum fernen.

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


Wände mit Versatz.

Die Quads sind jetzt voreingenommen, obwohl dies nicht ganz auffällt.

Sind die Wandstärken gleich?
, «-» . , . . , . , . , - , . .

Spitzen der Wände


Um die Wandstärke von oben sichtbar zu machen, müssen wir oben an der Wand ein Quad hinzufügen. Der einfachste Weg, dies zu tun, besteht darin, sich die beiden oberen Eckpunkte des ersten Quadrats zu merken und sie mit den beiden oberen Eckpunkten des zweiten Quadrats zu verbinden.

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


Wände mit Spitzen.

Kurvenfahrt


Wir haben immer noch Löcher an den Ecken der Zellen. Um sie zu füllen, müssen wir dem dreieckigen Bereich zwischen den Zellen ein Segment hinzufügen. Jede Ecke verbindet drei Zellen. Jede Zelle kann eine Wand haben oder nicht. Das heißt, acht Konfigurationen sind möglich.


Winkelkonfigurationen.

Wir platzieren Wände nur zwischen Zellen mit unterschiedlichen eingezäunten Zuständen. Dies reduziert die Anzahl der Konfigurationen auf sechs. In jedem von ihnen befindet sich eine der Zellen innerhalb der Krümmung der Wände. Betrachten wir diese Zelle als Referenzpunkt, um den die Wand gekrümmt ist. Aus der Sicht dieser Zelle beginnt die Wand mit einer Kante, die der linken Zelle gemeinsam ist, und endet mit einer Kante, die der rechten Zelle gemeinsam ist.


Zellrollen.

Das heißt, wir müssen eine Methode erstellen, AddWallSegmentderen Parameter drei Eckpunkte der Ecke sind. Obwohl wir Code schreiben können, um dieses Segment zu triangulieren, ist dies tatsächlich ein Sonderfall der Methode AddWallSegment. Ein Ankerpunkt spielt die Rolle beider naher Eckpunkte.

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

Erstellen Sie als Nächstes eine Variante der Methode AddWallfür die drei Eckpunkte des Winkels und ihre Zellen. Das Ziel dieser Methode ist es, den Winkel zu bestimmen, der der Referenzpunkt ist, falls vorhanden. Daher muss er alle acht möglichen Konfigurationen berücksichtigen und AddWallSegmentsechs davon anfordern.

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

Rufen Sie diese Methode am Ende auf, um Eckensegmente hinzuzufügen HexGridChunk.TriangulateCorner.

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


Wände mit Ecken, aber es gibt noch Löcher.

Schließen Sie die Löcher


Es gibt immer noch Löcher in den Wänden, da die Höhe der Wandsegmente variabel ist. Während die Segmente entlang der Kanten eine konstante Höhe haben, befinden sich die Ecksegmente zwischen zwei verschiedenen Kanten. Da jede Kante ihre eigene Höhe haben kann, erscheinen an den Ecken Löcher.

Um dies zu beheben, ändern Sie AddWallSegmentes so, dass die Y-Koordinaten des linken und rechten oberen Scheitelpunkts getrennt gespeichert werden.

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


Geschlossene Wände.

Die Wände sind jetzt geschlossen, aber Sie sehen wahrscheinlich immer noch Löcher in den Schatten der Wand. Dies wird durch den Parameter Normal Bias der Richtungsschatteneinstellung verursacht. Wenn es größer als Null ist, bewegen sich die Dreiecke der Objekte, die einen Schatten werfen, entlang der Normalen zur Oberfläche. Dies vermeidet Selbstbeschattung, erzeugt aber gleichzeitig Löcher in Fällen, in denen die Dreiecke in verschiedene Richtungen schauen. In diesem Fall können Löcher in den Schatten feiner Geometrie erzeugt werden, beispielsweise in unseren Wänden.

Sie können diese Schattenartefakte entfernen, indem Sie die normale Vorspannung auf Null senken. Oder ändern Sie den Cast Shadows Mesh Renderer- Wandmodus in Two Sided . Dadurch rendert das Schattenwurfobjekt beide Seiten jedes Wanddreiecks zum Rendern, wodurch alle Löcher geschlossen werden.


Es gibt keine Löcher mehr in den Schatten.

Einheitspaket

Leistenwand


Bisher sind unsere Wände gerade genug. Für ein flaches Gelände ist dies überhaupt nicht schlecht, aber es sieht seltsam aus, wenn die Wände mit den Leisten zusammenfallen. Dies geschieht, wenn zwischen Zellen auf gegenüberliegenden Seiten der Wand ein Höhenunterschied von einer Höhe besteht.


Gerade Wände auf den Leisten.

Folgen Sie der Kante


Anstatt ein Segment für die gesamte Kante zu erstellen, erstellen wir eines für jeden Teil des Kantenstreifens. Wir können dies tun, indem wir AddWallSegmentin der AddWallEdge- Version viermal aufrufen .

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


Geschwungene Wände.

Wände wiederholen nun die Form verzerrter Kanten. In Kombination mit den Leisten sieht es viel besser aus. Darüber hinaus entstehen interessantere Wände auf einem flachen Relief.

Wände auf den Boden stellen


Wenn Sie sich die Wände auf den Leisten ansehen, können Sie ein Problem feststellen. Die Wände hängen über dem Boden! Dies gilt für geneigte flache Kanten, ist aber normalerweise nicht so auffällig.


Wände hängen in der Luft.

Um das Problem zu lösen, müssen wir die Wände absenken. Am einfachsten ist es, die gesamte Wand so abzusenken, dass ihre Oberseite flach bleibt. Gleichzeitig senkt sich ein Teil der Wand auf der Oberseite leicht in das Relief, aber das passt zu uns.

Um die Wand abzusenken, müssen wir bestimmen, welche Seite niedriger ist - nah oder fern. Wir können nur die Höhe der untersten Seite verwenden, müssen aber nicht so tief gehen. Sie können die Y-Koordinate mit einem Versatz von knapp 0,5 von niedrig nach hoch interpolieren. Da die Wände nur gelegentlich höher als die untere Stufe der Kante werden, können wir die vertikale Stufe der Kante als Versatz verwenden. Eine unterschiedliche Wandstärke der Leistenkonfiguration kann einen anderen Versatz erfordern.


Die abgesenkte Wand.

Fügen wir der HexMetricsMethode WallLerp, die sich mit dieser Interpolation befasst, zusätzlich zur Mittelung der X- und Z-Koordinaten der nahen und fernen Eckpunkte hinzu. Es basiert auf einer Methode TerraceLerp.

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

Erzwingen Sie HexFeatureManagerdiese Methode, um den linken und rechten Scheitelpunkt zu bestimmen.

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


Wände stehen auf dem Boden.

Änderung der Wandverzerrung


Jetzt stimmen unsere Wände gut mit Höhenunterschieden überein. Sie entsprechen jedoch immer noch nicht vollständig den verzerrten Kanten, obwohl sie sich in ihrer Nähe befinden. Dies geschah, weil wir zuerst die Oberseiten der Wände bestimmen und sie dann verzerren. Da diese Scheitelpunkte irgendwo zwischen den Scheitelpunkten der nahen und fernen Kante liegen, ist ihre Verzerrung geringfügig unterschiedlich.

Die Tatsache, dass die Wände den Rippen ungenau folgen, ist kein Problem. Die Verformung der Wandoberseiten ändert sich jedoch ansonsten relativ gleichmäßige Dicke. Wenn wir die Wände basierend auf verzerrten Eckpunkten anordnen und dann unverzerrte Quads hinzufügen, sollte ihre Dicke nicht stark variieren.

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


Die unverzerrten Oberseiten der Wände.

Dank dieses Ansatzes folgen die Wände nicht mehr so ​​genau wie zuvor den Kanten. Im Gegenzug werden sie jedoch weniger gebrochen und haben eine konstantere Dicke.


Gleichmäßigere Wandstärke.

Einheitspaket

Löcher in den Wänden


Bisher haben wir die Möglichkeit ignoriert, dass ein Fluss oder eine Straße die Mauer überquert. In diesem Fall müssen wir ein Loch in die Wand bohren, durch das ein Fluss oder eine Straße führen kann.

Fügen Sie dazu AddWallzwei Boolesche Parameter hinzu, um anzugeben, ob ein Fluss oder eine Straße durch eine Kante verläuft. Obwohl wir sie unterschiedlich behandeln können, entfernen wir in beiden Fällen einfach die beiden mittleren Segmente.

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

Jetzt HexGridChunk.TriangulateConnectionsollte es die notwendigen Daten liefern. Da er bereits dieselben Informationen benötigte, speichern wir sie in booleschen Variablen und zeichnen die Aufrufe der entsprechenden Methoden nur einmal auf.

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


Die Löcher in den Wänden für den Durchgang von Flüssen und Straßen.

Wir bedecken die Wände


Diese neuen Öffnungen schaffen Orte, um die Wände zu vervollständigen. Wir müssen diese Endpunkte mit Quads schließen, damit wir nicht durch die Seiten der Wände schauen können. Erstellen wir zu diesem Zweck eine HexFeatureManagerMethode AddWallCap. Es funktioniert wie AddWallSegment, aber es braucht nur ein Paar Nah-Fern-Spitzen. Lassen Sie ihn ein Quad hinzufügen, das von der nahen zur anderen Seite der Wand geht.

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

Wenn sich AddWallherausstellt, dass wir ein Loch benötigen, fügen wir eine Abdeckung zwischen dem zweiten und vierten Kantenpaar der Kanten hinzu. Für das vierte Scheitelpunktpaar müssen Sie die Ausrichtung ändern, da sonst die Quad-Fläche nach innen schaut.

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


Geschlossene Löcher in den Wänden.

Was ist mit Löchern an den Rändern der Karte?
, . . , .

Einheitspaket

Vermeiden Sie Klippen und Wasser


Schauen wir uns zum Schluss Kanten an, die Klippen oder Wasser enthalten. Da es sich bei den Klippen im Wesentlichen um große Mauern handelt, wäre es unlogisch, eine zusätzliche Mauer darauf zu platzieren. Außerdem wird es schlecht aussehen. Unterwasserwände sind ebenso völlig unlogisch wie die Einschränkung durch die Küstenwände.


Wände auf Klippen und im Wasser.

Wir können die Wände von diesen unnötigen Kanten durch zusätzliches Einchecken entfernen AddWall. Eine Wand kann nicht unter Wasser sein, und eine gemeinsame Rippe kann keine Klippe sein.

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


Die störenden Wände entlang der Rippen wurden entfernt, aber die Ecken blieben an Ort und Stelle.

Wandecken entfernen


Das Entfernen unnötiger Ecksegmente erfordert etwas mehr Aufwand. Der einfachste Fall ist, wenn sich die Stützzelle unter Wasser befindet. Dadurch wird sichergestellt, dass keine Wandsegmente in der Nähe angeschlossen werden können.

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


Es gibt keine Unterwasser-Stützzellen mehr.

Jetzt müssen wir uns zwei andere Zellen ansehen. Befindet sich einer von ihnen unter Wasser oder ist er durch eine Unterbrechung mit der Stützzelle verbunden, gibt es entlang dieser Rippe keine Wand. Wenn dies für mindestens eine Seite zutrifft, sollte sich in dieser Ecke kein Wandsegment befinden.

Wir bestimmen individuell, ob es eine linke oder eine rechte Wand gibt. Wir setzen die Ergebnisse in boolesche Variablen, um die Arbeit zu erleichtern.

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


Alle Störwinkel entfernt.

Schließen Sie die Ecken


Wenn sich weder am linken noch am rechten Rand eine Wand befindet, sind die Arbeiten abgeschlossen. Wenn sich die Wand jedoch nur in eine Richtung befindet, bedeutet dies, dass sich ein weiteres Loch in der Wand befindet. Daher müssen Sie es schließen.

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


Wir schließen die Wände.

Verbindung von Mauern mit Klippen


In einer Situation sehen die Wände unvollkommen aus. Wenn die Wand den Boden der Klippe erreicht, endet sie. Da die Klippen jedoch nicht vollständig vertikal sind, entsteht zwischen der Wand und dem Rand der Klippe ein schmales Loch. Oben auf der Klippe tritt ein solches Problem nicht auf.


Löcher zwischen Wänden und Felswänden.

Es wäre viel besser, wenn die Mauer bis zum Rand der Klippe reichen würde. Wir können dies tun, indem wir ein weiteres Wandsegment zwischen dem aktuellen Ende der Wand und der Ecke oben auf der Klippe hinzufügen. Da der größte Teil dieses Segments in der Klippe verborgen ist, können wir darauf verzichten, die Wandstärke innerhalb der Klippe auf Null zu reduzieren. Es reicht uns also, einen Keil zu erstellen: zwei Quads, die auf den Punkt gehen, und ein Dreieck darüber. Erstellen wir zu diesem Zweck eine Methode AddWallWedge. Dies kann durch Kopieren AddWallCapund Hinzufügen eines Keilpunkts erfolgen.

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

In AddWallSegmentEcken nennen wir diese Methode, wenn die Wand nur in eine Richtung verläuft und sich diese Wand auf einer niedrigeren Höhe als die andere Seite befindet. Unter diesen Bedingungen stoßen wir auf den Rand einer Klippe.

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


, .

unitypackage

11:


  • .
  • .
  • .


.


Im vorherigen Teil haben wir eine Wandstütze hinzugefügt. Dies sind einfache gerade Wandsegmente ohne erkennbare Unterschiede. Jetzt werden wir die Wände interessanter machen, indem wir ihnen Türme hinzufügen.

Wandsegmente müssen prozedural erstellt werden, um dem Relief zu entsprechen. Dies ist für die Türme nicht erforderlich, wir können das übliche Fertighaus verwenden.

Wir können einen einfachen Turm aus zwei Würfeln mit rotem Material erstellen. Die Basis des Turms hat eine Größe von 2 mal 2 Einheiten und eine Höhe von 4 Einheiten, dh sie ist dicker und höher als die Wand. Über diesem Würfel platzieren wir einen Einheitswürfel, der die Spitze des Turms kennzeichnet. Wie alle anderen Fertighäuser benötigen diese Würfel keine Kollider.

Da das Turmmodell aus mehreren Objekten besteht, machen wir sie zu Kindern des Stammobjekts. Platzieren Sie sie so, dass der lokale Ursprung der Wurzel am Fuß des Turms liegt. Dank dessen können wir die Türme platzieren, ohne uns um ihre Höhe sorgen zu müssen.


Fertighaus.

Fügen Sie einen Link zu diesem Fertighaus hinzu HexFeatureManagerund verbinden Sie es.

  public Transform wallTower; 


Link zum Fertighaus.

Türme bauen


Beginnen wir mit der Platzierung von Türmen in der Mitte jedes Wandsegments. Dazu erstellen wir am Ende der Methode einen Turm AddWallSegment. Ihre Position ist der Durchschnitt der linken und rechten Punkte des Segments.

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


Ein Turm pro Wandsegment.

Wir haben viele Türme entlang der Mauer, aber ihre Ausrichtung ändert sich nicht. Wir müssen ihre Drehung so ändern, dass sie mit der Wand ausgerichtet sind. Da wir den rechten und den linken Punkt der Wand haben, wissen wir, welche Richtung richtig ist. Mit diesem Wissen können wir die Ausrichtung des Wandsegments und damit des Turms bestimmen.

Anstatt die Rotation selbst zu berechnen, weisen wir Transform.rightder Eigenschaft einfach einen Vektor zu. Der Einheitscode ändert die Drehung des Objekts so, dass seine lokale Richtung rechts dem übertragenen Vektor entspricht.

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


Die Türme sind mit der Wand ausgerichtet.

Wie funktioniert die Zuweisung von Transform.right?
Quaternion.FromToRotation . .

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

Reduzieren Sie die Anzahl der Türme


Ein Turm pro Wandsegment ist zu viel. Lassen Sie uns das Hinzufügen des Turms optional machen, indem wir dem AddWallSegmentBooleschen Wert einen Parameter hinzufügen . Stellen Sie den Standardwert ein false. In diesem Fall verschwinden alle Türme.

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

Platzieren wir die Türme nur in den Ecken der Zellen. Infolgedessen erhalten wir weniger Türme mit ziemlich konstanten Abständen zwischen ihnen.

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


Die Türme sind nur in den Ecken.

Es sieht gut aus, aber wir müssen die Türme möglicherweise weniger regelmäßig platzieren. Wie bei anderen Geländemerkmalen können wir anhand der Hash-Tabelle entscheiden, ob der Turm in eine Ecke gestellt werden soll. Dazu verwenden wir die Mitte der Ecke, um die Tabelle abzutasten, und vergleichen dann einen der Hash-Werte mit dem Schwellenwert der Türme.

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

Der Schwellenwert bezieht sich auf HexMetrics. Mit einem Wert von 0,5 werden Türme in der Hälfte der Fälle erstellt, aber wir können Wände mit vielen Türmen oder ohne sie erstellen.

  public const float wallTowerThreshold = 0.5f; 


Zufällige Türme.

Wir entfernen die Türme von den Hängen


Jetzt platzieren wir Türme unabhängig von der Form des Geländes. An den Hängen des Turms wirken sie jedoch unlogisch. Hier verlaufen die Wände schräg und können die Turmspitze durchschneiden.


Türme an den Pisten.

Um Steigungen zu vermeiden, prüfen wir, ob sich die Zellen der rechten und linken Ecke auf derselben Höhe befinden. Nur in diesem Fall ist es möglich, einen Turm zu platzieren.

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


Es gibt keine Türme mehr an den Wänden der Pisten.

Wir stellen die Mauern und Türme auf den Boden


Obwohl wir Wände an Hängen vermeiden, kann das Relief auf beiden Seiten der Wand immer noch unterschiedliche Höhen haben. Wände können entlang von Leisten verlaufen, und Zellen gleicher Höhe können unterschiedliche vertikale Positionen haben. Aus diesem Grund kann sich die Basis des Turms in der Luft befinden.


Türme in der Luft.

Tatsächlich können die Wände an den Hängen auch in der Luft hängen, aber dies ist nicht so auffällig wie bei den Türmen.


Wände sind in der Luft.

Dies kann behoben werden, indem die Basis der Wände und Türme auf den Boden gespannt wird. Fügen Sie dazu den Y-Versatz für die Wände in hinzu HexMetrics. Eine Einheit weniger wird ausreichen. Erhöhen Sie die Höhe der Türme um den gleichen Betrag.

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

Wir ändern es HexMetrics.WallLerpso, dass bei der Bestimmung der Y-Koordinate der neue Versatz berücksichtigt wird.

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

Wir müssen auch das Fertighaus des Turms ändern, da die Basis jetzt eine Einheit unter der Erde sein wird. Daher erhöhen wir die Höhe des Basiswürfels um eine Einheit und ändern dementsprechend die lokale Position der Würfel.



Mauern und Türme auf dem Boden.

Einheitspaket

Brücken


Zu diesem Zeitpunkt haben wir Flüsse und Straßen, aber Straßen können Flüsse in keiner Weise überqueren. Es ist der richtige Zeitpunkt, um Brücken hinzuzufügen.

Beginnen wir mit einem einfachen skalierten Würfel, der die Rolle einer vorgefertigten Brücke spielt. Die Breite der Flüsse variiert, aber es gibt ungefähr sieben Abstandseinheiten zwischen den Straßenzentren auf beiden Seiten. Daher geben wir eine ungefähre Skala (3, 1, 7). Fügen Sie vorgefertigtes rotes städtisches Material hinzu und entfernen Sie den Collider. Platzieren Sie den Würfel wie bei Türmen mit derselben Skalierung im Stammobjekt. Aus diesem Grund ist die Geometrie der Brücke selbst nicht wichtig.

Fügen Sie einen Link zum Fertighaus der Brücke hinzu HexFeatureManagerund weisen Sie ihm ein Fertighaus zu.

  public Transform wallTower, bridge; 


Zugewiesenes Brückenfertigteil.

Platzierung von Brücken


Um die Brücke zu platzieren, brauchen wir eine Methode HexFeatureManager.AddBridge. Die Brücke sollte sich zwischen der Mitte des Flusses und einer der Seiten des Flusses befinden.

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

Wir werden unverzerrte Straßenzentren übertragen, daher müssen wir sie verzerren, bevor wir die Brücke platzieren.

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

Um die Brücke richtig auszurichten, können wir den gleichen Ansatz wie beim Drehen der Türme verwenden. In diesem Fall definieren die Straßenmitten den Vorwärtsvektor der Brücke. Da wir in derselben Zelle bleiben, ist dieser Vektor definitiv horizontal, sodass wir seine Komponente Y nicht auf Null setzen müssen.

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

Wir bauen Brücken über gerade Flüsse


Die einzigen Flusskonfigurationen, die Brücken erfordern, sind gerade und gekrümmt. Straßen können durch Endpunkte verlaufen, und im Zickzack können Straßen nur in der Nähe sein.

Lassen Sie uns zunächst gerade Flüsse herausfinden. Im Inneren ordnet der HexGridChunk.TriangulateRoadAdjacentToRivererste Betreiber else ifStraßen in der Nähe solcher Flüsse an. Deshalb werden wir hier Brücken hinzufügen.

Wir sind auf einer Seite des Flusses. Das Zentrum der Straße bewegt sich vom Fluss weg, und dann verschiebt sich auch das Zentrum der Zelle. Um die Mitte der Straße auf der gegenüberliegenden Seite zu finden, müssen wir die entgegengesetzte Richtung um den gleichen Betrag bewegen. Dies muss erfolgen, bevor das Zentrum selbst geändert wird.

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


Brücken über gerade Flüsse.

Brücken sind erschienen! Aber jetzt haben wir eine Instanz von Brücken für jede Richtung, durch die der Fluss nicht fließt. Wir müssen sicherstellen, dass nur eine Instanz der Brücke in der Zelle generiert wird. Dies kann durch Auswahl einer Richtung relativ zum Fluss und auf dessen Grundlage zur Erzeugung einer Brücke erfolgen. Sie können eine beliebige Richtung wählen.

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

Außerdem müssen wir nur dann eine Brücke hinzufügen, wenn es auf beiden Seiten des Flusses eine Straße gibt. Im Moment sind wir uns bereits sicher, dass es auf der aktuellen Seite eine Straße gibt. Daher müssen Sie überprüfen, ob sich auf der anderen Seite des Flusses eine Straße befindet.

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


Brücken zwischen den Straßen auf beiden Seiten.

Brücken über geschwungene Flüsse


Brücken über gekrümmte Flüsse funktionieren ähnlich, aber ihre Topologie unterscheidet sich geringfügig. Wir werden eine Brücke hinzufügen, wenn wir uns außerhalb der Kurve befinden. Dies geschieht im letzten Block else. Es verwendet die mittlere Richtung, um die Mitte der Straße zu versetzen. Wir müssen diesen Offset zweimal mit verschiedenen Maßstäben verwenden, also speichern Sie ihn in einer Variablen.

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

Die Verschiebungsskala an der Außenseite der Kurve beträgt 0,25 und an der Innenseite HexMetrics.innerToOuter * 0.7f. Wir benutzen es, um die Brücke zu platzieren.

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


Brücken über geschwungene Flüsse.

Auch hier müssen doppelte Brücken vermieden werden. Wir können dies tun, indem wir Brücken nur aus der mittleren Richtung hinzufügen.

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

Und wieder müssen Sie sicherstellen, dass sich die Straße auf der gegenüberliegenden Seite befindet.

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


Brücken zwischen den Straßen auf beiden Seiten.

Brückenskalierung


Da wir das Gelände verzerren, variiert der Abstand zwischen den Straßenmitten und den gegenüberliegenden Seiten des Flusses. Manchmal sind die Brücken zu kurz, manchmal zu lang.


Unterschiedliche Abstände bei konstanten Brückenlängen.

Obwohl wir eine Brücke mit einer Länge von sieben Einheiten erstellt haben, können Sie sie so skalieren, dass sie dem tatsächlichen Abstand zwischen den Straßenmitten entspricht. Dies bedeutet, dass das Brückenmodell deformiert ist. Da die Abstände nicht sehr unterschiedlich sind, kann eine Verformung akzeptabler sein als Brücken, die für die Länge nicht geeignet sind.

Um eine ordnungsgemäße Skalierung durchzuführen, müssen wir die Anfangslänge des Brückenfertigteils kennen. Wir werden diese Länge in speichern HexMetrics.

  public const float bridgeDesignLength = 7f; 

Jetzt können wir den Maßstab entlang der Z-Instanz der Brücke dem Abstand zwischen den Straßenmitten, geteilt durch die ursprüngliche Länge, zuordnen. Da die Wurzel des Fertighauses der Brücke den gleichen Maßstab hat, wird die Brücke korrekt gedehnt.

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


Die wechselnde Länge der Brücken.

Brückenbau


Anstelle eines einfachen Würfels können wir ein interessanteres Brückenmodell verwenden. Sie können beispielsweise eine grobe Bogenbrücke aus drei skalierten und gedrehten Würfeln erstellen. Natürlich können Sie viel komplexere 3D-Modelle erstellen, einschließlich Teilen der Straße. Beachten Sie jedoch, dass das gesamte Objekt leicht komprimiert und gedehnt wird.



Bogenbrücken unterschiedlicher Länge.

Einheitspaket

Spezielle Objekte


Bisher können unsere Zellen städtische, ländliche und pflanzliche Objekte enthalten. Obwohl jede von ihnen drei Ebenen hat, sind alle Objekte im Vergleich zur Größe der Zelle recht klein. Was ist, wenn wir ein großes Gebäude wie eine Burg brauchen?

Fügen wir dem Gelände einen speziellen Objekttyp hinzu. Solche Objekte sind so groß, dass sie die gesamte Zelle einnehmen. Jedes dieser Objekte ist einzigartig und benötigt ein eigenes Fertighaus. Zum Beispiel kann eine einfache Burg aus einem zentralen Würfel plus vier Ecktürmen erstellt werden. Die Skala (6, 4, 6) für den zentralen Würfel erzeugt eine ausreichend große Verriegelung, die dennoch auch in eine stark deformierte Zelle passt.


Fertighaus der Burg.

Ein weiteres spezielles Objekt kann beispielsweise eine Zikkurat sein, die aus drei übereinander angeordneten Würfeln besteht. Für den unteren Würfel ist die Skala (8, 2,5, 8) geeignet.


Fertige Zikkurat.

Spezielle Objekte können beliebig sein, nicht unbedingt architektonisch. Beispielsweise kann eine Gruppe von massiven Bäumen mit einer Höhe von bis zu zehn Einheiten auf eine mit Megaflora gefüllte Zelle hinweisen.


Fertige Megaflora.

Fügen Sie dem HexFeatureManagerArray hinzu, um diese Fertighäuser zu verfolgen.

  public Transform[] special; 

Fügen Sie zuerst eine Burg zum Array hinzu, dann Zikkurat und dann Megaflora.


Anpassung von Spezialobjekten.

Zellen zu etwas Besonderem machen


Jetzt HexCellist ein Index spezieller Objekte erforderlich, der den Typ eines speziellen Objekts bestimmt, wenn es sich dort befindet.

  int specialIndex; 

Geben wir ihm wie anderen Reliefobjekten die Möglichkeit, diesen Wert zu empfangen und einzustellen.

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

Standardmäßig enthält die Zelle kein spezielles Objekt. Wir bezeichnen dies mit dem Index 0. Fügen Sie eine Eigenschaft hinzu, die diesen Ansatz verwendet, um festzustellen, ob eine Zelle etwas Besonderes ist.

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

Fügen Sie zum Bearbeiten von Zellen Unterstützung für den Index spezieller Objekte in hinzu HexMapEditor. Es funktioniert ähnlich wie städtische, ländliche und pflanzliche Einrichtungen.

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

Fügen Sie der Benutzeroberfläche einen Schieberegler hinzu, um das spezielle Objekt zu steuern. Da wir drei Objekte haben, verwenden wir das Intervall 0–3 im Schieberegler. Null bedeutet das Fehlen eines Objekts, eines - einer Burg, zwei - Zikkurat, drei - Megaflora.


Schieberegler für spezielle Objekte.

Spezielle Objekte hinzufügen


Jetzt können wir den Zellen spezielle Objekte zuweisen. Damit sie angezeigt werden, müssen wir eine HexFeatureManagerandere Methode hinzufügen . Es wird einfach eine Instanz des gewünschten Spezialobjekts erstellt und an der gewünschten Position platziert. Da Null das Fehlen eines Objekts anzeigt, müssen wir die Einheit vom Index der speziellen Objekte der Zelle abziehen, bevor wir Zugriff auf das Array von Fertighäusern erhalten.

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

Geben Sie dem Objekt mithilfe der Hash-Tabelle eine beliebige Drehung.

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

Beim Triangulieren einer Zelle HexGridChunk.Triangulateprüfen wir , ob die Zelle ein spezielles Objekt enthält. Wenn ja, dann nennen wir unsere neue Methode genau so AddFeature.

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


Spezielle Objekte. Sie sind viel größer als gewöhnlich.

Vermeiden Sie die Flüsse


Da sich spezielle Objekte in der Mitte der Zellen befinden, verbinden sie sich nicht mit den Flüssen, da sie über ihnen hängen.


Objekte auf den Flüssen.

Um zu verhindern, dass spezielle Objekte auf den Flüssen erstellt werden, ändern wir die Eigenschaft HexCell.SpecialIndex. Wir werden den Index nur ändern, wenn sich keine Flüsse in der Zelle befinden.

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

Wenn wir einen Fluss hinzufügen, müssen wir außerdem alle speziellen Objekte entfernen. Der Fluss sollte sie abwaschen. Dies kann erreicht werden, HexCell.SetOutgoingRiverindem der Index spezieller Objekte in der Methode auf 0 gesetzt wird .

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

Wir meiden Straßen


Wie Flüsse verlaufen auch Straßen schlecht mit speziellen Gegenständen, aber nicht alles ist so schrecklich. Sie können die Straßen sogar so lassen, wie sie sind. Einige Einrichtungen sind möglicherweise mit Straßen kompatibel, andere möglicherweise nicht. Daher können Sie sie vom Objekt abhängig machen. Aber wir werden es einfacher machen.


Objekte auf der Straße.

Lassen Sie in diesem Fall die Spezialobjekte die Straße besiegen. Wenn Sie den Index spezieller Objekte ändern, werden daher auch alle Straßen aus der Zelle entfernt.

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

Was ist, wenn wir ein bestimmtes Objekt löschen?
0, , . .

Dies bedeutet außerdem, dass wir beim Hinzufügen von Straßen zusätzliche Überprüfungen durchführen müssen. Wir werden nur Straßen hinzufügen, wenn keine der Zellen eine Zelle mit einem speziellen Objekt ist.

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

Vermeiden Sie andere Gegenstände


Spezielle Objekte können nicht mit anderen Objekttypen gemischt werden. Wenn sie sich überlappen, sieht es unordentlich aus. Es kann auch von einem bestimmten Objekt abhängen, aber wir werden den gleichen Ansatz verwenden.


Ein Objekt, das sich mit anderen Objekten schneidet.

In diesem Fall unterdrücken wir kleinere Objekte, als wären sie unter Wasser. Diesmal werden wir einchecken HexFeatureManager.AddFeature.

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

Vermeiden Sie Wasser


Wir haben auch ein Problem mit Wasser. Bleiben die Besonderheiten während des Hochwassers bestehen? Da wir kleine Objekte in überfluteten Zellen zerstören, machen wir dasselbe mit speziellen Objekten.


Gegenstände im Wasser.

In werden HexGridChunk.Triangulatewir den gleichen Hochwassercheck für spezielle und gewöhnliche Objekte durchführen.

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

Da beide Bediener ifjetzt prüfen, ob sich die Zelle unter Wasser befindet, können wir den Test übertragen und nur einmal durchführen.

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

Für Experimente wird uns eine solche Anzahl von Objekten ausreichen.

Einheitspaket

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


All Articles