Von einem Übersetzer: Dieser Artikel ist der erste einer detaillierten (27 Teile) Reihe von Tutorials zum Erstellen von Karten aus Sechsecken. Dies sollte ganz am Ende der Tutorials geschehen.Teile 1-3: Netz, Farben und ZellenhöhenTeile 4-7: Unebenheiten, Flüsse und StraßenTeile 8-11: Wasser, Landformen und WälleTeile 12-15: Speichern und Laden, Texturen, EntfernungenTeile 16-19: Weg finden, Spielerkader, AnimationenTeile 20-23: Nebel des Krieges, Kartenforschung, VerfahrensgenerierungTeile 24-27: Wasserkreislauf, Erosion, Biomes, zylindrische KarteTeil 1: Eingriff aus Sechsecken
Inhaltsverzeichnis
- Wandle Quadrate in Sechsecke um.
- Triangulieren Sie ein Sechseckgitter.
- Wir arbeiten mit kubischen Koordinaten.
- Wir interagieren mit Gitterzellen.
- Erstelle einen In-Game-Editor.
Dieses Tutorial ist der Beginn einer Reihe über Sechseckkarten. Hexagon-Netze werden in vielen Spielen verwendet, insbesondere in Strategien, einschließlich Age of Wonders 3, Civilization 5 und Endless Legend. Wir werden mit den Grundlagen beginnen, nach und nach neue Funktionen hinzufügen und als Ergebnis ein komplexes Relief auf der Basis von Sechsecken erstellen.
In diesem Tutorial wird davon ausgegangen, dass Sie bereits die
Mesh Basics- Reihe studiert haben, die mit dem
prozeduralen Raster beginnt. Es wurde unter Unity 5.3.1 erstellt. Die Serie verwendet mehrere Versionen von Unity. Der letzte Teil wurde in Unity 2017.3.0p3 erstellt.
Eine einfache Karte von Sechsecken.Über Sechsecke
Warum werden Sechsecke benötigt? Wenn wir ein Raster benötigen, ist es logisch, Quadrate zu verwenden. Quadrate sind sehr einfach zu zeichnen und zu positionieren, haben aber auch einen Nachteil. Schauen Sie sich ein einzelnes Quadrat des Gitters und dann seine Nachbarn an.
Der Platz und seine Nachbarn.Insgesamt hat der Platz acht Nachbarn. Vier davon können durch Überqueren des Randes des Quadrats erreicht werden. Dies sind horizontale und vertikale Nachbarn. Die anderen vier können durch Überqueren der Ecke des Platzes erreicht werden. Dies sind diagonale Nachbarn.
Wie groß ist der Abstand zwischen den Zentren benachbarter quadratischer Gitterzellen? Wenn die Kantenlänge 1 ist, ist die Antwort für horizontale und vertikale Nachbarn 1. Für diagonale Nachbarn ist die Antwort jedoch √2.
Der Unterschied zwischen den beiden Arten von Nachbarn führt zu Schwierigkeiten. Wenn wir diskrete Bewegungen verwenden, wie kann man dann die Bewegung entlang der Diagonale wahrnehmen? Soll ich es überhaupt zulassen? Wie kann das Erscheinungsbild organischer gestaltet werden? Unterschiedliche Spiele verwenden unterschiedliche Ansätze mit ihren Vor- und Nachteilen. Ein Ansatz besteht darin, überhaupt kein quadratisches Gitter zu verwenden, sondern stattdessen Sechsecke zu verwenden.
Sechseck und seine Nachbarn.Im Gegensatz zu einem Quadrat hat ein Sechseck nicht acht, sondern sechs Nachbarn. Alle diese Nachbarn grenzen an die Ränder, es gibt keine Ecknachbarn. Das heißt, es gibt nur eine Art von Nachbarn, was viel vereinfacht. Natürlich ist ein Sechseckgitter schwerer zu bauen als ein Quadrat, aber wir können damit umgehen.
Bevor wir beginnen, müssen wir die Größe der Sechsecke bestimmen. Die Kantenlänge sei gleich 10 Einheiten. Da das Sechseck aus einem Kreis von sechs gleichseitigen Dreiecken besteht, beträgt der Abstand vom Zentrum zu einem beliebigen Winkel ebenfalls 10. Dieser Wert bestimmt den Außenradius der sechseckigen Zelle.
Der äußere und innere Radius des Sechsecks.Es gibt auch einen Innenradius, der der Abstand von der Mitte zu jeder der Kanten ist. Dieser Parameter ist wichtig, da der Abstand zwischen den Mittelpunkten der Nachbarn gleich diesem Wert mal zwei ist. Der Innenradius ist
f r a c s q r t 3 2 vom äußeren Radius, das heißt in unserem Fall
5 s q r t 3 . Lassen Sie uns diese Parameter der Einfachheit halber in eine statische Klasse einordnen.
using UnityEngine; public static class HexMetrics { public const float outerRadius = 10f; public const float innerRadius = outerRadius * 0.866025404f; }
Wie leitet man den Wert des Innenradius ab?Nimm eines der sechs Dreiecke eines Sechsecks. Der Innenradius entspricht der Höhe dieses Dreiecks. Diese Höhe kann erhalten werden, indem das Dreieck in zwei reguläre Dreiecke geteilt und dann der Satz von Pythagoras verwendet wird.
Daher für die Länge der Rippe e Innenradius ist sqrte2−(e/2)2= sqrt3e2/4=e sqrt3/2 ca.0,886e .
Wenn wir dies bereits tun, bestimmen wir die Positionen der sechs Ecken relativ zur Mitte der Zelle. Es ist zu beachten, dass es zwei Möglichkeiten gibt, das Sechseck auszurichten: mit einer scharfen oder flachen Seite nach oben. Wir werden die Ecke aufstellen. Beginnen wir in diesem Winkel und fügen den Rest im Uhrzeigersinn hinzu. Legen Sie sie so auf die XZ-Ebene, dass die Sechsecke auf dem Boden liegen.
Mögliche Orientierungen. public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius) };
EinheitspaketVernetzung
Um ein Sechseckgitter zu erstellen, benötigen wir Gitterzellen. Erstellen Sie zu diesem Zweck die
HexCell
Komponente. Lassen Sie es vorerst leer, da wir noch keine bestimmten Zellen verwenden.
using UnityEngine; public class HexCell : MonoBehaviour { }
Erstellen Sie zunächst ein Standardebenenobjekt, fügen Sie eine Zellenkomponente hinzu und verwandeln Sie alles in ein Fertighaus.
Verwendung einer Ebene als Fertighaus einer sechseckigen Zelle.Nun gehen wir ins Netz. Erstellen wir eine einfache Komponente mit allgemeinen Variablen für Zellenbreite, -höhe und -fertigung. Fügen Sie dann der Szene ein Spielobjekt mit dieser Komponente hinzu.
using UnityEngine; public class HexGrid : MonoBehaviour { public int width = 6; public int height = 6; public HexCell cellPrefab; }
Sechseck-Netzobjekt.Beginnen wir mit der Erstellung eines regelmäßigen Quadrats von Quadraten, da wir bereits wissen, wie das geht. Speichern wir die Zellen in einem Array, um darauf zugreifen zu können.
Da die Ebenen standardmäßig eine Größe von 10 x 10 Einheiten haben, verschieben wir jede Zelle um diesen Betrag.
HexCell[] cells; void Awake () { cells = new HexCell[height * width]; for (int z = 0, i = 0; z < height; z++) { for (int x = 0; x < width; x++) { CreateCell(x, z, i++); } } } void CreateCell (int x, int z, int i) { Vector3 position; position.x = x * 10f; position.y = 0f; position.z = z * 10f; HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; }
Quadratisches Gitter von Flugzeugen.So haben wir ein schönes festes Gitter aus quadratischen Zellen. Aber welche Zelle ist wo? Dies ist natürlich leicht zu überprüfen, aber es gibt Schwierigkeiten mit Sechsecken. Es wäre praktisch, wenn wir gleichzeitig die Koordinaten aller Zellen sehen könnten.
Koordinatenanzeige
Fügen Sie der Szene Leinwand hinzu, indem Sie
GameObject / UI / Canvas auswählen und sie zu einem
untergeordneten Element unseres
Netzobjekts machen. Da diese Zeichenfläche nur zur Information dient, entfernen wir ihre Raycaster-Komponente. Sie können auch das Ereignissystemobjekt löschen, das automatisch zur Szene hinzugefügt wurde, da wir es derzeit nicht benötigen.
Stellen Sie den
Render-Modus auf "
Weltraum" und drehen Sie ihn um 90 Grad entlang der X-Achse, sodass die Leinwand über dem Raster liegt. Drehpunkt und Position auf Null setzen. Geben Sie ihm einen leichten vertikalen Versatz, damit der Inhalt oben ist. Breite und Höhe sind uns nicht wichtig, da wir den Inhalt selbst arrangieren. Wir können den Wert auf 0 setzen, um das große Rechteck im Szenenfenster zu entfernen.
Als letzten Schliff erhöhen Sie die
dynamischen Pixel pro Einheit auf 10. Wir garantieren daher, dass Textobjekte eine ausreichende Texturauflösung verwenden.
Leinwand für Gitterkoordinaten von Sechsecken.Um die Koordinaten anzuzeigen, erstellen Sie ein
Textobjekt (
GameObject / UI / Text ) und verwandeln Sie es in ein Fertighaus. Zentrieren Sie die Anker und drehen Sie sie, und stellen Sie die Größe auf 5 x 15 ein. Der Text sollte auch horizontal und vertikal in der Mitte ausgerichtet sein. Stellen Sie die Schriftgröße auf 4 ein. Schließlich möchten wir den Standardtext nicht verwenden und verwenden keinen
Rich-Text . Außerdem spielt es für uns keine Rolle, ob das
Raycast-Ziel aktiviert ist, da es für unsere Leinwand immer noch nicht benötigt wird.
Fertighausetikett.Jetzt müssen wir dem Raster etwas über Leinwand und Fertighaus erzählen. Fügen Sie am Anfang ihres Skripts
using UnityEngine.UI;
um bequem auf den Typ
UnityEngine.UI.Text
. Ein vorgefertigtes Etikett benötigt eine gemeinsam genutzte Variable, und Canvas kann durch Aufrufen von
GetComponentInChildren
.
public Text cellLabelPrefab; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); … }
Verbindungsfertige Tags.Nachdem Sie das Fertighaus des Etiketts verbunden haben, können Sie seine Instanzen erstellen und die Koordinaten der Zelle anzeigen. Fügen Sie zwischen X und Z ein Zeilenumbruchzeichen ein, sodass sie in separaten Zeilen angezeigt werden.
void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = x.ToString() + "\n" + z.ToString(); }
Koordinatenanzeige.Sechseckpositionen
Jetzt, da wir jede Zelle visuell erkennen können, können wir sie verschieben. Wir wissen, dass der Abstand zwischen benachbarten hexagonalen Zellen in X-Richtung dem doppelten Innenradius entspricht. Wir werden es benutzen. Außerdem sollte der Abstand zur nächsten Zellenreihe 1,5-mal größer sein als der äußere Radius.
Geometrie benachbarter Sechsecke. position.x = x * (HexMetrics.innerRadius * 2f); position.y = 0f; position.z = z * (HexMetrics.outerRadius * 1.5f);
Wir wenden Abstände zwischen Sechsecken ohne Offsets an.Natürlich liegen die Ordnungsreihen der Sechsecke nicht genau übereinander. Jede Zeile ist entlang der X-Achse um den Wert des Innenradius versetzt. Dieser Wert kann erhalten werden, indem die Hälfte von Z zu X addiert und dann mit dem doppelten Innenradius multipliziert wird.
position.x = (x + z * 0.5f) * (HexMetrics.innerRadius * 2f);
Durch die richtige Platzierung der Sechsecke entsteht ein rautenförmiges Gitter.Obwohl wir auf diese Weise die Zellen an den richtigen Positionen der Sechsecke platziert haben, füllt unser Gitter jetzt eher die Raute als das Rechteck. Wir arbeiten viel einfacher mit rechteckigen Gittern. Lassen Sie uns also die Zellen wieder in Betrieb nehmen. Dies kann durch Zurückschieben eines Teils des Versatzes erfolgen. In jeder zweiten Zeile müssen alle Zellen um einen weiteren Schritt zurückgeschoben werden. Dazu müssen wir das Ergebnis der ganzzahligen Division von Z durch 2 subtrahieren, bevor wir multiplizieren.
position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f);
Die Position der Sechsecke in einem rechteckigen Bereich.EinheitspaketHexagon-Rendering
Nachdem die Zellen korrekt platziert wurden, können wir die realen Sechsecke anzeigen. Zuerst müssen wir die Ebenen entfernen, damit wir alle Komponenten außer
HexCell
aus dem Zellen-
HexCell
.
Es gibt keine Flugzeuge mehr.Wie in den Tutorials zu den
Netzgrundlagen verwenden wir ein Netz, um das gesamte Netz zu rendern. Dieses Mal werden wir jedoch die Anzahl der erforderlichen Eckpunkte und Dreiecke nicht voreingestellt. Stattdessen werden wir Listen verwenden.
Erstellen Sie eine neue
HexMesh
Komponente,
HexMesh
um unser Netz kümmert. Es erfordert einen Netzfilter und einen Renderer, ein Netz und Listen für Scheitelpunkte und Dreiecke.
using UnityEngine; using System.Collections.Generic; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class HexMesh : MonoBehaviour { Mesh hexMesh; List<Vector3> vertices; List<int> triangles; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); hexMesh.name = "Hex Mesh"; vertices = new List<Vector3>(); triangles = new List<int>(); } }
Erstellen Sie mit dieser Komponente ein neues untergeordnetes Objekt für dieses Netz. Er erhält automatisch einen Mesh-Renderer, ihm wird jedoch kein Material zugewiesen. Fügen Sie daher das Standardmaterial hinzu.
Hex-Mesh-Objekt.HexGrid
kann nun sein Sechsecknetz auf dieselbe Weise abrufen, wie er die Leinwand gefunden hat.
HexMesh hexMesh; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); … }
Nach dem Wachnetz sollte es dem Netz befehlen, seine Zellen zu triangulieren. Wir müssen sicher sein, dass dies nach der Awake-Komponente des Hex-Netzes geschieht. Da
Start
später aufgerufen wird, geben Sie dort den entsprechenden Code ein.
void Start () { hexMesh.Triangulate(cells); }
Diese
HexMesh.Triangulate
Methode kann jederzeit aufgerufen werden, auch wenn die Zellen bereits zuvor trianguliert wurden. Daher sollten wir zunächst alte Daten bereinigen. Wenn wir alle Zellen durchlaufen, triangulieren wir sie einzeln. Nach Abschluss dieses Vorgangs weisen wir dem Netz die generierten Scheitelpunkte und Dreiecke zu und berechnen abschließend die Netznormalen neu.
public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); triangles.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } hexMesh.vertices = vertices.ToArray(); hexMesh.triangles = triangles.ToArray(); hexMesh.RecalculateNormals(); } void Triangulate (HexCell cell) { }
Da die Sechsecke aus Dreiecken bestehen, erstellen wir eine bequeme Methode zum Hinzufügen eines Dreiecks basierend auf den Positionen von drei Eckpunkten. Es werden nur die Eckpunkte der Reihe nach hinzugefügt. Er addiert auch die Indizes dieser Eckpunkte, um ein Dreieck zu bilden. Der Index des ersten Scheitelpunkts entspricht der Länge der Liste der Scheitelpunkte, bevor neue Scheitelpunkte hinzugefügt werden. Vergessen Sie dies nicht, wenn Sie Scheitelpunkte hinzufügen.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); }
Jetzt können wir unsere Zellen triangulieren. Beginnen wir mit dem ersten Dreieck. Sein erster Gipfel befindet sich in der Mitte des Sechsecks. Die beiden anderen Eckpunkte sind der erste und der zweite Winkel relativ zur Mitte.
void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[0], center + HexMetrics.corners[1] ); }
Das erste Dreieck jeder Zelle.Das hat funktioniert, also lasst uns alle sechs Dreiecke umrunden.
Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); }
Können Spitzen geteilt werden?Ja, das kannst du. Tatsächlich können wir es sogar noch besser machen und nur vier statt sechs Dreiecke zum Rendern verwenden. Wenn wir dies jedoch aufgeben, werden wir unsere Arbeit vereinfachen, und es wird richtig sein, da in den folgenden Tutorials alles komplizierter wird. Die Optimierung von Eckpunkten und Dreiecken in dieser Phase wird uns in Zukunft behindern.
Leider führt dieser Prozess zu einer
IndexOutOfRangeException
. Dies liegt daran, dass das letzte Dreieck versucht, die siebte Ecke zu erhalten, die nicht existiert. Natürlich sollte er zurückgehen und als letzten Scheitelpunkt der ersten Ecke verwenden. Oder wir können die erste Ecke in
HexMetrics.corners
, um nicht über die Grenzen hinauszugehen.
public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) };
Sechsecke vollständig.EinheitspaketSechseckige Koordinaten
Schauen wir uns noch einmal die Koordinaten der Zellen an, jetzt im Kontext eines Sechseckgitters. Die Z-Koordinate sieht gut aus und die X-Koordinate im Zickzack. Dies ist ein Nebeneffekt des Linienversatzes, um einen rechteckigen Bereich abzudecken.
Versetzte Koordinaten mit hervorgehobenen Nulllinien.Bei der Arbeit mit Sechsecken sind solche Versatzkoordinaten nicht einfach zu handhaben.
HexCoordinates
wir eine Struktur
HexCoordinates
, die zum Konvertieren in ein anderes Koordinatensystem verwendet werden kann. Machen wir es serialisierbar, damit Unity es speichern kann und es im Wiedergabemodus neu kompiliert wird. Wir machen diese Koordinaten auch unveränderlich, indem wir die öffentlichen schreibgeschützten Eigenschaften verwenden.
using UnityEngine; [System.Serializable] public struct HexCoordinates { public int X { get; private set; } public int Z { get; private set; } public HexCoordinates (int x, int z) { X = x; Z = z; } }
Fügen Sie eine statische Methode hinzu, um einen Satz von Koordinaten aus normalen Versatzkoordinaten zu erstellen. Im Moment werden wir diese Koordinaten einfach kopieren.
public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x, z); } }
Wir fügen auch praktische Methoden zur Konvertierung von Zeichenfolgen hinzu. Die
ToString
Methode gibt standardmäßig einen Strukturtypnamen zurück, was für uns nicht sehr nützlich ist. Wir definieren es neu, damit es die Koordinaten in einer Zeile zurückgibt. Wir werden auch eine Methode zum Anzeigen von Koordinaten in separaten Zeilen hinzufügen, da wir bereits ein solches Schema verwenden.
public override string ToString () { return "(" + X.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Z.ToString(); }
Jetzt können wir viele Koordinaten an unsere
HexCell
Komponente übergeben.
public class HexCell : MonoBehaviour { public HexCoordinates coordinates; }
Ändern Sie
HexGrid.CreateCell
so, dass die neuen Koordinaten verwendet werden können.
HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = cell.coordinates.ToStringOnSeparateLines();
Wiederholen wir nun diese X-Koordinaten so, dass sie entlang einer geraden Achse ausgerichtet sind. Dies kann durch Aufheben der horizontalen Verschiebung erfolgen. Das resultierende Ergebnis wird üblicherweise als Axialkoordinaten bezeichnet.
public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x - z / 2, z); }
Axialkoordinaten.Dieses zweidimensionale Koordinatensystem ermöglicht es uns, die Bewegung der Verschiebung in vier Richtungen nacheinander zu beschreiben. Zwei verbleibende Richtungen erfordern jedoch noch besondere Aufmerksamkeit. Dies macht uns klar, dass es eine dritte Dimension gibt. Und tatsächlich würden wir, wenn wir die Dimension von X horizontal spiegeln würden, die fehlende Dimension von Y erhalten.
Die Y-Messung wird angezeigt.Da diese Messungen von X und Y Spiegelkopien voneinander sind, ergibt die Addition ihrer Koordinaten immer das gleiche Ergebnis, wenn Z konstant bleibt. Wenn Sie alle drei Koordinaten addieren, erhalten wir immer Null. Wenn Sie eine Koordinate erhöhen, müssen Sie die andere reduzieren. Tatsächlich gibt uns dies sechs mögliche Bewegungsrichtungen. Solche Koordinaten werden normalerweise als kubisch bezeichnet, da sie dreidimensional sind und die Topologie einem Würfel ähnelt.
Da die Summe aller Koordinaten Null ist, können wir immer eine der Koordinaten von den anderen beiden erhalten. Da wir bereits die X- und Z-Koordinaten speichern, müssen wir die Y-Koordinate nicht speichern.
Wir können eine Eigenschaft hinzufügen, die sie bei Bedarf auswertet, und sie in Zeichenfolgenmethoden verwenden.
public int Y { get { return -X - Z; } } public override string ToString () { return "(" + X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString(); }
Kubische Koordinaten.Inspektorkoordinaten
Wählen Sie im Wiedergabemodus eine der Gitterzellen aus. Es stellt sich heraus, dass der Inspektor seine Koordinaten nicht anzeigt,
HexCell.coordinates
nur die
HexCell.coordinates
.
Der Inspektor zeigt die Koordinaten nicht an.Obwohl dies kein großes Problem ist, wäre es großartig, die Koordinaten anzuzeigen. Unity zeigt keine Koordinaten an, da diese nicht als serialisierbare Felder markiert sind. Um sie anzuzeigen, müssen Sie explizit serialisierbare Felder für X und Z angeben.
[SerializeField] private int x, z; public int X { get { return x; } } public int Z { get { return z; } } public HexCoordinates (int x, int z) { this.x = x; this.z = z; }
X- und Z-Koordinaten werden jetzt angezeigt, können jedoch geändert werden. Wir brauchen das nicht, weil die Koordinaten festgelegt werden müssen. Es ist auch nicht sehr gut, dass sie untereinander angezeigt werden.
Wir können es besser machen: Definieren Sie unsere eigene Eigenschaftsschublade für den
HexCoordinates
Typ. Erstellen Sie ein
HexCoordinatesDrawer
Skript und fügen Sie es in den
Editor- Ordner ein, da dieses Skript nur für den Editor bestimmt ist.
Die Klasse muss
UnityEditor.PropertyDrawer
und benötigt das Attribut
UnityEditor.CustomPropertyDrawer
, um es einem geeigneten Typ zuzuordnen.
using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(HexCoordinates))] public class HexCoordinatesDrawer : PropertyDrawer { }
Eigenschaftsschubladen zeigen ihren Inhalt mit der
OnGUI
Methode an. Mit dieser Methode konnten serialisierbare Eigenschaftsdaten und die Bezeichnung des Felds, zu dem sie gehören, innerhalb des Bildschirmrechtecks gezeichnet werden.
public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { }
Wir extrahieren die Werte von x und z aus der Eigenschaft und verwenden sie dann, um einen neuen Satz von Koordinaten zu erstellen. Zeichnen Sie dann das GUI-Label an der ausgewählten Position mit unserer
HexCoordinates.ToString
Methode.
public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { HexCoordinates coordinates = new HexCoordinates( property.FindPropertyRelative("x").intValue, property.FindPropertyRelative("z").intValue ); GUI.Label(position, coordinates.ToString()); }
Koordinaten ohne Präfixbezeichnung.Dadurch werden die Koordinaten angezeigt, aber jetzt fehlt der Feldname. Diese Namen werden normalerweise mit der
EditorGUI.PrefixLabel
Methode gerendert. Als Bonus wird ein ausgerichtetes Rechteck zurückgegeben, das dem Leerzeichen rechts neben dieser Beschriftung entspricht.
position = EditorGUI.PrefixLabel(position, label); GUI.Label(position, coordinates.ToString());
Koordinaten mit einem Etikett.EinheitspaketZellen berühren
Ein Sechseckgitter ist nicht sehr interessant, wenn wir nicht damit interagieren können. Die einfachste Interaktion besteht darin, die Zelle zu berühren. Fügen wir also Unterstützung hinzu. Im
HexGrid
fügen wir diesen Code einfach direkt in
HexGrid
. Wenn es anfängt zu funktionieren, werden wir es an einen anderen Ort verschieben.
Um eine Zelle zu berühren, können Sie von der Position des Mauszeigers aus Strahlen in die Szene senden. Wir können den gleichen Ansatz wie im Tutorial zur
Netzdeformation verwenden .
void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { TouchCell(hit.point); } } void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); Debug.Log("touched at " + position); }
Bisher macht der Code nichts. Wir müssen dem Gitter einen Kollider hinzufügen, damit der Strahl mit etwas kollidieren kann. Daher geben wir das
HexMesh
Collider-Netz an.
MeshCollider meshCollider; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); … }
Weisen Sie dem Collider nach Abschluss der Triangulation ein Netz zu.
public void Triangulate (HexCell[] cells) { … meshCollider.sharedMesh = hexMesh; }
Können wir nicht einfach Box Collider verwenden?Wir können, aber es wird nicht genau mit dem Umriss unseres Gitters übereinstimmen. Ja, und unser Raster wird nicht lange flach bleiben, aber dies ist ein Thema für zukünftige Tutorials.
Jetzt können wir das Gitter berühren! Aber welche Zelle berühren wir? Um dies herauszufinden, müssen wir die Berührungsposition in die Koordinaten der Sechsecke umwandeln. Dies ist eine Arbeit für
HexCoordinates
, daher erklären wir, dass es eine statische
FromPosition
Methode gibt.
public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); Debug.Log("touched at " + coordinates.ToString()); }
Wie bestimmt diese Methode, welche Koordinate zur Position gehört? Wir können beginnen, indem wir x durch die horizontale Breite des Sechsecks teilen. Und da die Y-Koordinate ein Spiegelbild der X-Koordinate ist, ergibt ein negatives x y.
public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / (HexMetrics.innerRadius * 2f); float y = -x; }
Dies würde uns natürlich die richtigen Koordinaten geben, wenn Z Null wäre. Wir müssen uns erneut verschieben, wenn wir uns entlang Z bewegen. Alle zwei Zeilen müssen wir um eine Einheit nach links verschieben.
float offset = position.z / (HexMetrics.outerRadius * 3f); x -= offset; y -= offset;
Infolgedessen erweisen sich unsere x- und y-Werte als Ganzzahlen in der Mitte jeder Zelle. Wenn wir sie daher auf die nächste ganze Zahl runden, müssen wir die Koordinaten erhalten. Wir berechnen auch Z und erhalten so die endgültigen Koordinaten. int iX = Mathf.RoundToInt(x); int iY = Mathf.RoundToInt(y); int iZ = Mathf.RoundToInt(-x -y); return new HexCoordinates(iX, iZ);
Die Ergebnisse sehen vielversprechend aus, aber sind diese Koordinaten korrekt? Bei sorgfältiger Untersuchung können Sie feststellen, dass wir manchmal die Koordinaten erhalten, deren Summe ungleich Null ist! Aktivieren Sie die Benachrichtigung, um sicherzustellen, dass dies wirklich geschieht. if (iX + iY + iZ != 0) { Debug.LogWarning("rounding error!"); } return new HexCoordinates(iX, iZ);
Wir erhalten tatsächlich Benachrichtigungen. Wie beheben wir diesen Fehler? Es entsteht nur neben den Kanten zwischen den Sechsecken. Das heißt, das Runden von Koordinaten verursacht Probleme. Welche Koordinate ist in die falsche Richtung gerundet? Je weiter wir uns von der Mitte der Zelle entfernen, desto runder werden wir. Daher ist es logisch anzunehmen, dass die am meisten gerundete Koordinate falsch ist.Dann besteht die Lösung darin, die Koordinate mit dem größten Rundungsdelta zu löschen und sie aus den Werten der beiden anderen neu zu erstellen. Da wir jedoch nur X und Z benötigen, können wir Y nicht neu erstellen. if (iX + iY + iZ != 0) { float dX = Mathf.Abs(x - iX); float dY = Mathf.Abs(y - iY); float dZ = Mathf.Abs(-x -y - iZ); if (dX > dY && dX > dZ) { iX = -iY - iZ; } else if (dZ > dY) { iZ = -iX - iY; } }
Sechsecke Malvorlagen
Jetzt, da wir die richtige Zelle berühren können, ist die Zeit für echte Interaktion gekommen. Lassen Sie uns die Farbe jeder Zelle ändern, in die wir gelangen. Fügen Sie für die HexGrid
benutzerdefinierten Farben der Standardzelle und der betroffenen Zelle hinzu. public Color defaultColor = Color.white; public Color touchedColor = Color.magenta;
Auswahl der Zellenfarbe.Zum HexCell
allgemeinen Farbfeld hinzufügen. public class HexCell : MonoBehaviour { public HexCoordinates coordinates; public Color color; }
Weisen Sie HexGrid.CreateCell
es der Standardfarbe zu. void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … }
Wir müssen auch HexMesh
die Farbinformationen ergänzen . List<Color> colors; void Awake () { … vertices = new List<Vector3>(); colors = new List<Color>(); … } public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); colors.Clear(); … hexMesh.vertices = vertices.ToArray(); hexMesh.colors = colors.ToArray(); … }
Beim Triangulieren müssen wir nun jedem Dreieck Farbdaten hinzufügen. Zu diesem Zweck erstellen wir eine separate Methode. void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); AddTriangleColor(cell.color); } } void AddTriangleColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); }
Zurück zu HexGrid.TouchCell
. Konvertieren Sie zunächst die Koordinaten der Zelle in den entsprechenden Index des Arrays. Für ein quadratisches Gitter wäre dies nur X plus Z multipliziert mit der Breite, aber in unserem Fall müssen wir auch einen Versatz von der Hälfte Z hinzufügen. Dann nehmen wir die Zelle, ändern ihre Farbe und triangulieren das Netz erneut.Müssen wir wirklich das gesamte Netz neu triangulieren?, . . , , . .
public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = touchedColor; hexMesh.Triangulate(cells); }
Obwohl wir die Zellen jetzt einfärben können, sind visuelle Änderungen noch nicht sichtbar. Dies liegt daran, dass der Shader standardmäßig keine Scheitelpunktfarben verwendet. Wir müssen unsere eigenen schreiben. Erstellen Sie einen neuen Standard-Shader ( Assets / Create / Shader / Default Surface Shader ). Es müssen nur zwei Änderungen vorgenommen werden. Fügen Sie zunächst Farbdaten zu der Eingabestruktur hinzu. Zweitens multiplizieren Sie die Albedo mit dieser Farbe. Wir interessieren uns nur für RGB-Kanäle, weil das Material undurchsichtig ist. Shader "Custom/VertexColors" { 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"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; float4 color : COLOR; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Erstellen Sie mit diesem Shader ein neues Material und lassen Sie das Maschennetz dieses Material verwenden. Dank dessen erscheinen die Farben der Zellen.Farbige Zellen.Ich bekomme seltsame Schattenartefakte!Unity . , , Z-. .
EinheitspaketKarteneditor
Nachdem wir nun wissen, wie man Farben ändert, erstellen wir einen einfachen In-Game-Editor. Diese Funktionalität gilt nicht für Funktionen HexGrid
, daher werden wir sie TouchCell
in eine allgemeine Methode mit einem zusätzlichen Farbparameter umwandeln. Löschen Sie auch das Feld touchedColor
. public void ColorCell (Vector3 position, Color color) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = color; hexMesh.Triangulate(cells); }
Erstellen Sie eine Komponente HexMapEditor
und verschieben Sie die Methoden Update
und dorthin HandleInput
. Fügen Sie ein gemeinsames Feld hinzu, um auf das Sechseckraster, ein Array von Farben und ein privates Feld zu verweisen, um die aktive Farbe zu verfolgen. Fügen Sie abschließend eine allgemeine Methode zum Auswählen einer Farbe hinzu und lassen Sie sie zunächst die erste Farbe auswählen. using UnityEngine; public class HexMapEditor : MonoBehaviour { public Color[] colors; public HexGrid hexGrid; private Color activeColor; void Awake () { SelectColor(0); } void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { hexGrid.ColorCell(hit.point, activeColor); } } public void SelectColor (int index) { activeColor = colors[index]; } }
Fügen Sie eine weitere Zeichenfläche hinzu, wobei Sie die Standardeinstellungen beibehalten. Fügen Sie eine Komponente hinzu HexMapEditor
, definieren Sie mehrere Farben und verbinden Sie sie mit einem Sechseckraster. Dieses Mal benötigen wir ein Ereignissystemobjekt, das automatisch neu erstellt wurde.Vierfarbiger Sechskant-Karteneditor.Fügen Sie der Leinwand ein Bedienfeld zum Speichern von Farbwählern hinzu ( GameObject / UI / Bedienfeld ). Fügen Sie ihre Umschaltgruppe hinzu ( Komponenten / Benutzeroberfläche / Umschaltgruppe ). Machen Sie das Panel klein und platzieren Sie es in der Ecke des Bildschirms.Farbbedienfeld mit Umschaltgruppe.Füllen Sie nun das Bedienfeld mit Schaltern für jede Farbe ( GameObject / UI / Toggle ). Solange wir uns nicht mit der Erstellung einer komplexen Benutzeroberfläche beschäftigen, reicht eine einfache manuelle Konfiguration aus.Ein Schalter für jede Farbe.Schalten Sie den ersten Schalter ein. Machen Sie außerdem alle Schalter zu Teilen der Umschaltgruppe, sodass jeweils nur einer ausgewählt werden kann. Verbinden Sie sie schließlich mit SelectColor
der Methode unseres Editors. Dies kann über die Schaltfläche "+" der Benutzeroberfläche des Ereignisses " On Value Changed" erfolgen . Wählen Sie das Karteneditorobjekt aus und wählen Sie dann die gewünschte Methode aus der Dropdown-Liste aus.Der erste Schalter.Dieses Ereignis übergibt ein boolesches Argument, das bestimmt, ob der Schalter bei jeder Änderung eingeschaltet wird. Aber es ist uns egal. Stattdessen müssen wir manuell ein ganzzahliges Argument übergeben, das dem Farbindex entspricht, den wir verwenden möchten. Lassen Sie daher den Wert 0 für den ersten Schalter, setzen Sie den Wert 1 auf den zweiten und so weiter.Wann wird die Switch-Event-Methode aufgerufen?. , , .
, , . , SelectColor
. , .
Färbung in mehreren Farben.Obwohl die Benutzeroberfläche funktioniert, gibt es ein nerviges Detail. Bewegen Sie das Bedienfeld so, dass es das Sechseckgitter bedeckt, um es zu sehen. Bei der Auswahl einer neuen Farbe werden auch die Zellen unter der Benutzeroberfläche eingefärbt. Das heißt, wir interagieren gleichzeitig mit der Benutzeroberfläche und dem Raster. Dies ist ein unerwünschtes Verhalten.Dies kann behoben werden, indem das Ereignissystem gefragt wird, ob es die Position des Cursors über einem Objekt bestimmt hat. Da sie nur über UI-Objekte Bescheid weiß, wird uns dies mitteilen, dass wir mit der UI interagieren. Daher müssen wir die Eingabe nur dann selbst verarbeiten, wenn dies nicht der Fall ist. using UnityEngine; using UnityEngine.EventSystems; … void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } }
EinheitspaketTeil 2: Mischen von Zellfarben
Inhaltsverzeichnis
- Verbinden Sie die Nachbarn.
- Interpolieren Sie die Farben zwischen den Dreiecken.
- Erstellen Sie Mischbereiche.
- Vereinfachen Sie die Geometrie.
Im vorherigen Teil haben wir den Grundstein für das Raster gelegt und die Möglichkeit zum Bearbeiten von Zellen hinzugefügt. Jede Zelle hat ihre eigene Volltonfarbe und die Farben an den Zellenrändern ändern sich dramatisch. In diesem Tutorial erstellen wir Übergangszonen, in denen die Farben benachbarter Zellen gemischt werden.Glatte Übergänge zwischen Zellen.Nachbarzellen
Bevor wir eine Glättung zwischen den Farben der Zellen durchführen, müssen wir herausfinden, welche der Zellen nebeneinander liegen. Jede Zelle hat sechs Nachbarn, die in Richtung der Kardinalpunkte identifiziert werden können. Wir erhalten die folgenden Richtungen: Nordosten, Osten, Südosten, Südwesten, Westen und Nordwesten. Erstellen wir eine Aufzählung für sie und fügen sie in eine separate Skriptdatei ein. public enum HexDirection { NE, E, SE, SW, W, NW }
Was ist Aufzählung?enum
, . . , . , .
enum . , integer . , - , integer.
Sechs Nachbarn, sechs Richtungen.Fügen Sie dem HexCell
Array hinzu, um diese Nachbarn zu speichern . Obwohl wir es allgemein machen können, werden wir es stattdessen privat machen und den Zugriff auf Methoden unter Verwendung von Anweisungen ermöglichen. Wir machen es auch serialisierbar, damit die Bindungen bei der Neukompilierung nicht verloren gehen. [SerializeField] HexCell[] neighbors;
Müssen wir alle Verbindungen zu Nachbarn speichern?, . — , .
Jetzt wird das Array der Nachbarn im Inspektor angezeigt. Da jede Zelle sechs Nachbarn hat, legen wir für unser Hex Cell- Fertighaus die Größe von Array 6 fest.In unserem Fertighaus ist Platz für sechs Nachbarn.Fügen wir nun eine allgemeine Methode hinzu, um einen Zellnachbarn in eine Richtung zu erhalten. Da der Richtungswert immer im Bereich von 0 bis 5 liegt, müssen wir nicht prüfen, ob sich der Index innerhalb des Arrays befindet. public HexCell GetNeighbor (HexDirection direction) { return neighbors[(int)direction]; }
Fügen Sie eine Methode hinzu, um einen Nachbarn anzugeben. public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; }
Die Beziehungen der Nachbarn sind bidirektional. Wenn Sie einen Nachbarn in eine Richtung setzen, ist es daher logisch, einen Nachbarn sofort in die entgegengesetzte Richtung zu setzen. public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; cell.neighbors[(int)direction.Opposite()] = this; }
Nachbarn in entgegengesetzte Richtungen.Dies deutet natürlich darauf hin, dass wir Anweisungen für den gegenüberliegenden Nachbarn anfordern können. Wir können dies implementieren, indem wir eine Erweiterungsmethode für erstellen HexDirection
. Um die entgegengesetzte Richtung zu erhalten, müssen Sie zum Original 3 hinzufügen. Dies funktioniert jedoch nur für die ersten drei Richtungen, für den Rest müssen Sie 3 subtrahieren. public enum HexDirection { NE, E, SE, SW, W, NW } public static class HexDirectionExtensions { public static HexDirection Opposite (this HexDirection direction) { return (int)direction < 3 ? (direction + 3) : (direction - 3); } }
Was ist eine Erweiterungsmethode?— , - . — , , , . this
. , .
? , , , . ? — . , , .
Nachbarverbindung
Wir können den Nachbarlink in initialisieren HexGrid.CreateCell
. Wenn wir Zellen Zeile für Zeile von links nach rechts durchlaufen, wissen wir, welche Zellen bereits erstellt wurden. Dies sind die Zellen, mit denen wir uns verbinden können.Am einfachsten ist die E - W - Verbindung. Die erste Zelle jeder Reihe hat keinen östlichen Nachbarn. Aber alle anderen Zellen haben es. Und diese Nachbarn werden vor der Zelle erstellt, mit der wir gerade arbeiten. Deshalb können wir sie verbinden.Die Verbindung von E nach W während der Erstellung von Zellen. void CreateCell (int x, int z, int i) { … cell.color = defaultColor; if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } Text label = Instantiate<Text>(cellLabelPrefab); … }
Die östlichen und westlichen Nachbarn sind miteinander verbunden.Wir müssen zwei weitere bidirektionale Verbindungen erstellen. Da dies die Verbindungen zwischen verschiedenen Linien des Gitters sind, können wir nur mit der vorherigen Linie kommunizieren. Dies bedeutet, dass wir die erste Zeile vollständig überspringen müssen. if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } if (z > 0) { }
Da die Linien im Zickzack sind, müssen sie unterschiedlich verarbeitet werden. Lassen Sie uns zuerst mit geraden Linien umgehen. Da alle Zellen in solchen Zeilen einen Nachbarn auf SE haben, können wir sie damit verbinden.Verbindung von NW nach SE für gerade Linien. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); } }
Was macht z & 1?&&
— , &
— . , . 1, 1. , 10101010 & 00001111
00001010
.
. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , 0.
, , . 0, .
Wir können uns mit Nachbarn in der SW verbinden, mit Ausnahme der ersten Zelle jeder Zeile, die diese nicht hat.Verbindung von NE nach SW für gerade Linien. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } }
Ungerade Linien folgen der gleichen Logik, jedoch spiegelbildlich. Nach Abschluss dieses Vorgangs sind alle Nachbarn in unserem Netz verbunden. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } else { cell.SetNeighbor(HexDirection.SW, cells[i - width]); if (x < width - 1) { cell.SetNeighbor(HexDirection.SE, cells[i - width + 1]); } } }
Alle Nachbarn sind verbunden.Natürlich ist nicht jede Zelle mit genau sechs Nachbarn verbunden. Zellen an der Gittergrenze haben mindestens zwei und nicht mehr als fünf Nachbarn. Und das muss berücksichtigt werden.Nachbarn für jede Zelle.EinheitspaketFarbmischung
Das Mischen von Farben erschwert die Triangulation jeder Zelle. Lassen Sie uns daher den Triangulationscode in einem separaten Teil trennen. Da wir jetzt Anweisungen haben, verwenden wir sie anstelle von numerischen Indizes, um Teile anzuzeigen. void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } } void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[(int)direction], center + HexMetrics.corners[(int)direction + 1] ); AddTriangleColor(cell.color); }
Wenn wir jetzt Richtungen verwenden, wäre es praktisch, Winkel mit Richtungen zu erhalten und nicht die Konvertierung in Indizes durchzuführen. AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) );
Dazu müssen Sie HexMetrics
zwei statische Methoden hinzufügen . Als Bonus können wir so die Auswahl der Winkel privat machen. static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) }; public static Vector3 GetFirstCorner (HexDirection direction) { return corners[(int)direction]; } public static Vector3 GetSecondCorner (HexDirection direction) { return corners[(int)direction + 1]; }
Mehrere Farben auf einem Dreieck
Bisher hat die Methode HexMesh.AddTriangleColor
nur ein Farbargument. Es kann nur ein einfarbiges Dreieck erstellt werden. Erstellen wir eine Alternative, die separate Farben für jeden Scheitelpunkt unterstützt. void AddTriangleColor (Color c1, Color c2, Color c3) { colors.Add(c1); colors.Add(c2); colors.Add(c3); }
Jetzt können wir anfangen, Farben zu mischen! Beginnen wir einfach mit der Nachbarfarbe für die beiden anderen Eckpunkte. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); HexCell neighbor = cell.GetNeighbor(direction); AddTriangleColor(cell.color, neighbor.color, neighbor.color); }
Dies führt leider dazu NullReferenceException
, dass die Zellen an der Grenze keine sechs Nachbarn haben. Was sollen wir tun, wenn ein Nachbar fehlt? Seien wir pragmatisch und verwenden die Zelle selbst als Ersatz. HexCell neighbor = cell.GetNeighbor(direction) ?? cell;
Was macht der Betreiber?null-coalescing operator. , a ?? b
— a != null ? a : b
.
, - Unity . null
. .
Es gibt eine Mischung von Farben, aber es wird falsch gemacht.Wohin gingen die Koordinatenbeschriftungen?, UI.
Farbmittelung
Das Mischen von Farben funktioniert, aber die Ergebnisse sind offensichtlich falsch. Die Farbe an den Rändern der Sechsecke sollte der Durchschnitt zweier benachbarter Zellen sein. HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Color edgeColor = (cell.color + neighbor.color) * 0.5f; AddTriangleColor(cell.color, edgeColor, edgeColor);
Rippen mischen.Obwohl wir an den Rändern mischen, erhalten wir immer noch scharfe Farbränder. Dies geschieht, weil jeder Scheitelpunkt des Sechsecks von drei Sechsecken geteilt wird.Drei Nachbarn, vier Farben.Dies bedeutet, dass wir auch Nachbarn in der vorherigen und nächsten Richtung berücksichtigen müssen. Das heißt, wir erhalten vier Farben in zwei Dreiergruppen.Fügen wir HexDirectionExtensions
zwei Additionsmethoden für einen bequemen Übergang zur vorherigen und nächsten Richtung hinzu. public static HexDirection Previous (this HexDirection direction) { return direction == HexDirection.NE ? HexDirection.NW : (direction - 1); } public static HexDirection Next (this HexDirection direction) { return direction == HexDirection.NW ? HexDirection.NE : (direction + 1); }
Jetzt können wir alle drei Nachbarn bekommen und Drei-Wege-Mischen durchführen. HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f );
An den Ecken mischen.So erhalten wir die richtigen Farbübergänge mit Ausnahme des Netzrandes. Die Randzellen stimmen nicht mit den Farben der fehlenden Nachbarn überein, daher sehen wir hier immer noch scharfe Ränder. Im Allgemeinen liefert unser derzeitiger Ansatz jedoch keine guten Ergebnisse. Wir brauchen eine bessere Strategie.EinheitspaketMischbereiche
Das Mischen über die gesamte Oberfläche des Sechsecks führt zu verschwommenem Chaos. Wir können einzelne Zellen nicht klar sehen. Die Ergebnisse können erheblich verbessert werden, indem nur neben den Kanten der Sechsecke gemischt wird. In diesem Fall behält der innere Bereich der Sechsecke eine feste Farbe.Kontinuierliche Schattierung von Cent mit Mischbereichen.Wie groß sollte der feste Bereich im Vergleich zum Mischbereich sein? Unterschiedliche Verteilungen führen zu unterschiedlichen Ergebnissen. Wir werden diesen Bereich als Bruchteil des Außenradius definieren. Lassen Sie es gleich 75% sein. Dies wird uns zu zwei neuen Metriken führen, die insgesamt 100% betragen. public const float solidFactor = 0.75f; public const float blendFactor = 1f - solidFactor;
Durch Erstellen dieses neuen festen Füllfaktors können wir Methoden schreiben, um die Winkel fester innerer Sechsecke zu erhalten. public static Vector3 GetFirstSolidCorner (HexDirection direction) { return corners[(int)direction] * solidFactor; } public static Vector3 GetSecondSolidCorner (HexDirection direction) { return corners[(int)direction + 1] * solidFactor; }
Ändern HexMesh.Triangulate
Sie es jetzt so, dass diese festen Schattierungswinkel anstelle der ursprünglichen Winkel verwendet werden. Wir lassen die Farben vorerst gleich. AddTriangle( center, center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) );
Feste Sechsecke ohne Kanten.Triangulation von Mischbereichen
Wir müssen den leeren Raum ausfüllen, den wir durch Reduzieren der Dreiecke erstellt haben. In jeder Richtung hat dieser Raum die Form eines Trapezes. Um es abzudecken, können Sie das Viereck (Quad) verwenden. Daher werden wir Methoden erstellen, um ein Viereck und seine Farben hinzuzufügen.Trapezrippe. void AddQuad (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); } void AddQuadColor (Color c1, Color c2, Color c3, Color c4) { colors.Add(c1); colors.Add(c2); colors.Add(c3); colors.Add(c4); }
Wir machen HexMesh.Triangulate
es neu, so dass das Dreieck eine Farbe erhält und das Viereck eine Mischung zwischen einer Volltonfarbe und den Farben zweier Winkel ausführt. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); Vector3 v3 = center + HexMetrics.GetFirstCorner(direction); Vector3 v4 = center + HexMetrics.GetSecondCorner(direction); AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor( cell.color, cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); }
Mischen mit Trapezrippen.Brücken zwischen Rippen
Das Bild wird besser, aber die Arbeit ist noch nicht abgeschlossen. Das Mischen von Farben zwischen zwei Nachbarn wird durch benachbarte Zellen kontaminiert. Um dies zu vermeiden, müssen wir die Ecken des Trapezes abschneiden und in ein Rechteck verwandeln. Danach wird er eine Brücke zwischen der Zelle und ihrem Nachbarn bauen und Lücken an den Seiten hinterlassen.Die Brücke zwischen den Rippen.Wir können neue Positionen finden v3
und v4
, beginnend mit v1
und v2
, und dann entlang der Brücke bis zum Rand der Zelle gehen. Wie wird die Brücke verschoben? Wir können es finden, indem wir den Mittelpunkt zwischen den beiden entsprechenden Winkeln nehmen und dann den Mischungskoeffizienten darauf anwenden. Das wird reichen HexMetrics
. public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * 0.5f * blendFactor; }
Zurück zu HexMesh
, es ist jetzt logisch, eine Option hinzuzufügen AddQuadColor
, die nur zwei Farben erfordert. void AddQuadColor (Color c1, Color c2) { colors.Add(c1); colors.Add(c1); colors.Add(c2); colors.Add(c2); }
Ändern Sie es Triangulate
so, dass richtig gemischte Brücken zwischen den Nachbarn entstehen. Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor(cell.color, (cell.color + neighbor.color) * 0.5f);
Richtig gestrichene Brücken mit Eckabständen.Lücken füllen
Jetzt haben wir eine dreieckige Lücke an der Verbindungsstelle von drei Zellen gebildet. Wir haben diese Lücken durch Ausschneiden der dreieckigen Seiten des Trapezes erhalten. Lassen Sie uns diese Dreiecke zurückbekommen.Stellen Sie sich zunächst ein Dreieck vor, das mit einem vorherigen Nachbarn verbunden ist. Sein erster Scheitelpunkt hat eine Zellfarbe. Die Farbe des zweiten Peaks ist eine Mischung aus drei Farben. Und der letzte Gipfel hat die gleiche Farbe wie der Punkt in der Mitte der Brücke. Color bridgeColor = (cell.color + neighbor.color) * 0.5f; AddQuadColor(cell.color, bridgeColor); AddTriangle(v1, center + HexMetrics.GetFirstCorner(direction), v3); AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, bridgeColor );
Fast alles ist fertig.Ein anderes Dreieck funktioniert auf die gleiche Weise, außer dass die Brücke nicht den dritten, sondern den zweiten Gipfel berührt. AddTriangle(v2, v4, center + HexMetrics.GetSecondCorner(direction)); AddTriangleColor( cell.color, bridgeColor, (cell.color + neighbor.color + nextNeighbor.color) / 3f );
Volle Färbung.Jetzt haben wir schöne Mischbereiche, die wir jeder Größe geben können. Die Kanten können nach Belieben verschwommen oder scharf gemacht werden. Sie können jedoch feststellen, dass das Mischen in der Nähe des Netzrandes immer noch nicht korrekt implementiert ist. Und wieder werden wir es für später belassen und uns vorerst auf ein anderes Thema konzentrieren.Aber die Übergänge zwischen den Farben sind immer noch hässlich. . .
EinheitspaketRippenfusion
Schauen Sie sich die Topologie unseres Rasters an. Welche Formen fallen hier auf? Wenn Sie die Grenze nicht beachten, können wir drei verschiedene Arten von Formularen unterscheiden. Es gibt einfarbige Sechsecke, zweifarbige Rechtecke und dreifarbige Dreiecke. Alle diese drei Farben erscheinen an der Verbindungsstelle der drei Zellen.Drei visuelle Strukturen.Alle zwei Sechsecke sind also durch eine rechteckige Brücke verbunden. Und alle drei Sechsecke sind durch ein Dreieck verbunden. Wir führen jedoch eine komplexere Triangulation durch. Jetzt verwenden wir zwei Vierecke anstelle von einem, um ein Paar Sechsecke zu verbinden. Und um die drei Sechsecke zu verbinden, verwenden wir sechs Dreiecke. Das ist zu redundant. Wenn wir uns direkt mit einer Form verbinden würden, müssten wir keine Farbmittelung vornehmen. Daher könnten wir mit weniger Komplexität, weniger Arbeit und weniger Dreiecken auskommen.Härter als nötig.Warum brauchen wir das überhaupt?, . , , . , , . , , .
Direkte Überbrückung
Jetzt bestehen unsere Brücken zwischen den Rippen aus zwei Vierecken. Um sie auf das nächste Sechseck auszudehnen, müssen wir die Länge der Brücke verdoppeln. Dies bedeutet, dass wir nicht mehr zwei Winkel mitteln müssen HexMetrics.GetBridge
. Stattdessen addieren wir sie einfach und multiplizieren sie dann mit dem Mischfaktor. public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * blendFactor; }
Die Brücken überspannten die gesamte Länge und überlappten sich.Brücken stellen jetzt direkte Verbindungen zwischen Sechsecken her. Wir erzeugen aber immer noch zwei Vierecke pro Verbindung, eines in jede Richtung. Das heißt, nur einer von ihnen sollte Brücken zwischen zwei Zellen schaffen.Vereinfachen wir zuerst unseren Triangulationscode. Wir löschen alles, was mit den Dreiecken der Kanten und der Farbmischung zu tun hat. Verschieben Sie dann den Code, der das Viereck der Brücke zur neuen Methode hinzufügt. Wir übergeben die ersten beiden Eckpunkte an diese Methode, damit wir sie nicht neu berechnen müssen. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); TriangulateConnection(direction, cell, v1, v2); } void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); }
Jetzt können wir die Triangulation von Verbindungen leicht begrenzen. Zunächst fügen wir die Bridge nur hinzu, wenn Sie mit der NE-Verbindung arbeiten. if (direction == HexDirection.NE) { TriangulateConnection(direction, cell, v1, v2); }
Brücken sind nur in Richtung NE.Es scheint, dass wir alle Verbindungen abdecken können, indem wir sie nur in die ersten drei Richtungen triangulieren: NE, E und SE. if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); }
Alle internen Brücken und Brücken an den Grenzen.Wir haben alle Verbindungen zwischen zwei benachbarten Zellen abgedeckt. Wir haben aber auch einige Brücken, die von der Zelle ins Nirgendwo führen. Lassen Sie uns sie loswerden und raus, TriangulateConnection
wenn die Nachbarn raus sind. Das heißt, wir müssen die fehlenden Nachbarn nicht mehr durch die Zelle selbst ersetzen. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } … }
Nur interne Brücken.Dreiecksgelenke
Jetzt müssen wir die dreieckigen Lücken wieder schließen. Lassen Sie uns dies für ein Dreieck tun, das mit dem nächsten Nachbarn verbunden ist. Und dies muss wieder nur getan werden, wenn ein Nachbar existiert. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { AddTriangle(v2, v4, v2); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } }
Wie wird die Position des dritten Gipfels sein? Ich habe als Ersatz eingefügt v2
, aber das ist offensichtlich falsch. Da jede Kante dieser Dreiecke mit der Brücke verbunden ist, können wir sie finden, indem wir entlang der Brücke zum nächsten Nachbarn gehen. AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next()));
Wir machen wieder die vollständige Triangulation.Sind wir fertig? Noch nicht, denn jetzt erstellen wir überlappende Dreiecke. Da die drei Zellen eine gemeinsame Dreiecksverbindung haben, müssen sie nur für zwei Verbindungen hinzugefügt werden. Daher werden NE und E. ausreichen. if (direction <= HexDirection.E && nextNeighbor != null) { AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); }
EinheitspaketTeil 3: Höhen
Inhaltsverzeichnis
- Zellenhöhe hinzufügen.
- Triangulieren Sie die Hänge.
- Setzen Sie die Leisten ein.
- Kombinieren Sie Felsvorsprünge und Klippen.
In diesem Teil des Tutorials werden wir Unterstützung für verschiedene Höhenstufen hinzufügen und spezielle Übergänge zwischen ihnen erstellen.Höhen und Leisten.Zellenhöhe
Wir haben unsere Karte in separate Zellen unterteilt, die einen flachen Bereich abdecken. Jetzt geben wir jeder Zelle ihre eigene Höhe. Wir werden diskrete Höhenstufen verwenden, um sie als ganzzahliges Feld in zu speichern HexCell
. public int elevation;
Wie groß kann jede nachfolgende Höhe sein? Wir können jeden Wert verwenden, also setzen wir ihn als eine andere Konstante HexMetrics
. Wir werden einen Schritt von fünf Einheiten verwenden, damit die Übergänge deutlich sichtbar sind. In einem echten Spiel würde ich einen kleineren Schritt verwenden. public const float elevationStep = 5f;
Zellen ändern
Bisher konnten wir nur die Farbe der Zelle ändern, jetzt können wir ihre Höhe ändern. Daher HexGrid.ColorCell
reicht uns die Methode nicht aus. Darüber hinaus können wir in Zukunft weitere Optionen für die Zellbearbeitung hinzufügen, sodass wir einen neuen Ansatz benötigen.Benennen Sie ColorCell
in um GetCell
und machen Sie es so, dass anstatt die Farbe der Zelle festzulegen, die Zelle an der angegebenen Position zurückgegeben wird. Da diese Methode nichts anderes ändert, müssen wir die Zellen sofort triangulieren. public HexCell GetCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; return cells[index]; }
Jetzt wird sich der Editor mit dem Zellwechsel befassen. Nach Abschluss der Arbeiten muss das Gitter erneut trianguliert werden. Fügen Sie dazu eine allgemeine Methode hinzu HexGrid.Refresh
. public void Refresh () { hexMesh.Triangulate(cells); }
Ändern Sie, HexMapEditor
damit er mit neuen Methoden arbeiten kann. Geben wir ihm eine neue Methode EditCell
, die alle Änderungen an der Zelle behandelt und anschließend das Raster aktualisiert. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCell(hexGrid.GetCell(hit.point)); } } void EditCell (HexCell cell) { cell.color = activeColor; hexGrid.Refresh(); }
Wir können die Höhen einfach ändern, indem wir der gewünschten Zelle die gewünschte Höhenstufe zuweisen. int activeElevation; void EditCell (HexCell cell) { cell.color = activeColor; cell.elevation = activeElevation; hexGrid.Refresh(); }
Wie bei Farben benötigen wir eine Methode zum Festlegen der aktiven Höhenstufe, die wir der Benutzeroberfläche zuordnen. Um Werte aus dem Höhenintervall auszuwählen, verwenden wir den Schieberegler. Da die Schieberegler mit float arbeiten, erfordert unsere Methode einen Parameter vom Typ float. Wir werden es einfach in eine Ganzzahl konvertieren. public void SetElevation (float elevation) { activeElevation = (int)elevation; }
Fügen Sie einen Schieberegler auf der Leinwand hinzu ( GameObject / Create / Slider ) und platzieren Sie ihn unter dem Farbbedienfeld . Wir machen es vertikal von unten nach oben, so dass es optisch den Höhen entspricht. Wir beschränken es auf Ganzzahlen und erstellen ein geeignetes Intervall, z. B. von 0 bis 6. Dann hängen wir das Ereignis On Value Changed an die Hex Map Editor-SetElevation
Objektmethode an . Die Methode muss aus der dynamischen Liste ausgewählt werden, damit sie mit dem Wert des Schiebereglers aufgerufen wird.Höhenregler.Höhenvisualisierung
Beim Ändern einer Zelle stellen wir jetzt sowohl Farbe als auch Höhe ein. Obwohl wir im Inspektor sehen können, dass sich die Höhe tatsächlich ändert, ignoriert der Triangulationsprozess sie immer noch.Es reicht aus, die vertikale lokale Position der Zelle zu ändern, wenn wir die Höhe ändern. Machen Sie die Methode der Einfachheit halber HexCell.elevation
privat und fügen Sie eine allgemeine Eigenschaft hinzu HexCell.Elevation
. public int Elevation { get { return elevation; } set { elevation = value; } } int elevation;
Jetzt können wir die vertikale Position der Zelle beim Bearbeiten der Höhe ändern. set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; }
Dies erfordert natürlich kleine Änderungen an HexMapEditor.EditCell
. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation; hexGrid.Refresh(); }
Zellen mit unterschiedlichen Höhen.Ändert sich der Mesh-Collider an die neue Höhe?Unity mesh collider null. , , null . . ( ) .
Zellenhöhen sind jetzt sichtbar, aber es gibt zwei Probleme. Erstens. Zellmarkierungen verschwinden unter den erhabenen Zellen. Zweitens ignorieren Verbindungen zwischen Zellen die Höhe. Lass es uns reparieren.Ändern Sie die Position der Zellbezeichnungen
Derzeit werden UI-Beschriftungen für Zellen nur einmal erstellt und platziert. Danach vergessen wir sie. Um ihre vertikalen Positionen zu aktualisieren, müssen wir sie verfolgen. Lassen Sie uns jedem einen HexCell
Link zu RectTransform
seinen UI-Labels geben, damit Sie ihn später aktualisieren können. public RectTransform uiRect;
Weisen Sie sie am Ende zu HexGrid.CreateCell
. void CreateCell (int x, int z, int i) { … cell.uiRect = label.rectTransform; }
Jetzt können wir die Eigenschaft HexCell.Elevation
so erweitern, dass sich auch die Position der Benutzeroberfläche der Zelle ändert. Da das Leinwandnetz der Sechsecke gedreht wird, müssen die Beschriftungen in negativer Richtung entlang der Z-Achse und nicht auf der positiven Seite der Y-Achse verschoben werden. set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = elevation * -HexMetrics.elevationStep; uiRect.localPosition = uiPosition; }
Tags mit Höhe.Schaffung von Pisten
Jetzt müssen wir die Flachzellenverbindungen in Steigungen umwandeln. Dies geschieht in HexMesh.TriangulateConnection
. Bei Kantenverbindungen müssen wir die Höhe des anderen Endes der Brücke neu definieren. Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep;
Bei Eckfugen müssen wir dasselbe mit der Brücke zum nächsten Nachbarn tun. if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; AddTriangle(v2, v4, v5); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); }
Verbindung unter Berücksichtigung der Höhe.Jetzt unterstützen wir Zellen in unterschiedlichen Höhen mit den richtigen Schrägfugen zwischen ihnen. Aber lasst uns hier nicht aufhören. Wir werden diese Pisten interessanter machen.EinheitspaketRippenfugen mit Leisten
Gerade Hänge sehen nicht sehr attraktiv aus. Wir können sie in mehrere Schritte unterteilen, indem wir Schritte hinzufügen. Dieser Ansatz wird im Spiel Endless Legend verwendet.Zum Beispiel können wir an jedem Hang zwei Leisten einfügen. Infolgedessen verwandelt sich ein großer Hang in drei kleine, zwischen denen sich zwei flache Bereiche befinden. Um ein solches Schema zu triangulieren, müssen wir jede Verbindung in fünf Stufen trennen.Zwei Vorsprünge am Hang.Wir können die Anzahl der Stufen für die Steigung einstellen HexMetrics
und daraus die Anzahl der Stufen berechnen. public const int terracesPerSlope = 2; public const int terraceSteps = terracesPerSlope * 2 + 1;
Im Idealfall könnten wir einfach jeden Schritt entlang der Steigung interpolieren. Dies ist jedoch nicht ganz trivial, da sich die Y-Koordinate nur in ungeraden Stufen ändern sollte. Andernfalls erhalten wir keine flachen Leisten. Fügen wir hierfür eine spezielle Interpolationsmethode hinzu HexMetrics
. public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { return a; }
Die horizontale Interpolation ist einfach, wenn wir die Größe des Interpolationsschritts kennen. public const float horizontalTerraceStepSize = 1f / terraceSteps; public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; return a; }
Wie funktioniert die Interpolation zwischen zwei Werten?a und b t . t 0, a . 1, b . t - 0 1, a und b . : (1−t)a+tb .
, (1−t)a+tb=a−ta+tb=a+t(b−a) . a b (b−a) . , .
Um Y nur in ungeraden Stadien zu ändern, können wir verwenden ( s t e p + 1 ) / 2 .
Wenn wir eine ganzzahlige Division verwenden, wird die Reihe 1, 2, 3, 4 in 1, 1, 2, 2 umgewandelt. public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1); public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize; ay += (by - ay) * v; return a; }
Fügen wir eine Methode zum Interpolieren von Leisten für Farben hinzu. Interpolieren Sie sie einfach so, als ob die Verbindungen flach wären. public static Color TerraceLerp (Color a, Color b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; return Color.Lerp(a, b, h); }
Triangulation
Wenn die Triangulation der Kantenverbindung komplizierter wird, entfernen wir den entsprechenden Code aus HexMesh.TriangulateConnection
und platzieren ihn in einer separaten Methode. In den Kommentaren werde ich den Quellcode speichern, um in Zukunft darauf zu verweisen. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor);
Beginnen wir mit dem ersten Schritt des Prozesses. Wir werden unsere speziellen Interpolationsmethoden verwenden, um das erste Quad zu erstellen. In diesem Fall sollte eine kurze Steigung erzeugt werden, die steiler als die ursprüngliche ist. void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1); Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); }
Der erste Schritt beim Erstellen einer Kante.Jetzt werden wir sofort zur letzten Stufe übergehen und alles dazwischen überspringen. Dies wird die Verbindung der Kanten vervollständigen, wenn auch bisher mit einer unregelmäßigen Form. AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color);
Der letzte Schritt beim Erstellen einer Kante.Zwischenschritte können durch die Schleife hinzugefügt werden. In jeder Phase werden die letzten beiden vorherigen Scheitelpunkte die neuen ersten. Gleiches gilt für Farbe. Nach der Berechnung der neuen Vektoren und Farben wird ein weiteres Quad hinzugefügt. AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c2; v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i); v4 = HexMetrics.TerraceLerp(beginRight, endRight, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2); } AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color);
Alle Zwischenschritte.Jetzt haben alle Kantenfugen zwei Leisten oder eine andere Zahl, die Sie in angeben HexMetrics.terracesPerSlope
. Bis wir Leisten für Eckverbindungen erstellt haben, werden wir dies natürlich für später belassen.Alle Kantenfugen haben Leisten.EinheitspaketVerbindungstypen
Das Umrüsten aller Kantenfugen in Leisten ist keine so gute Idee. Sie sehen nur dann gut aus, wenn der Höhenunterschied nur eine Ebene beträgt. Mit einem größeren Unterschied werden jedoch schmale Leisten mit großen Lücken zwischen ihnen erzeugt, und dies sieht nicht sehr schön aus. Außerdem müssen wir nicht für alle Verbindungen Leisten erstellen.Lassen Sie uns dies formalisieren und drei Arten von Kanten definieren: eine Ebene, eine Neigung und eine Klippe. Erstellen wir hierfür eine Aufzählung. public enum HexEdgeType { Flat, Slope, Cliff }
Wie kann man feststellen, um welche Art von Verbindung es sich handelt? Dazu können wir eine HexMetrics
Methode hinzufügen , die zwei Höhenstufen verwendet. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { }
Wenn die Höhen gleich sind, haben wir eine flache Rippe. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } }
Wenn der Pegelunterschied einem Schritt entspricht, ist dies eine Steigung. Es ist egal, ob es hoch oder runter geht. In allen anderen Fällen bekommen wir eine Pause. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } int delta = elevation2 - elevation1; if (delta == 1 || delta == -1) { return HexEdgeType.Slope; } return HexEdgeType.Cliff; }
Fügen wir auch eine bequeme Methode hinzu HexCell.GetEdgeType
, um den Typ der Zellkante in eine bestimmte Richtung zu bestimmen. public HexEdgeType GetEdgeType (HexDirection direction) { return HexMetrics.GetEdgeType( elevation, neighbors[(int)direction].elevation ); }
Müssen wir nicht prüfen, ob in dieser Richtung ein Nachbar existiert?, , . , NullReferenceException
. , , - . , . .
, , , . - , NullReferenceException
.
Erstellen Sie Leisten nur für Hänge
Nachdem wir die Art der Verbindung bestimmen können, können wir entscheiden, ob Leisten eingefügt werden sollen. Ändern Sie dies HexMesh.TriangulateConnection
so, dass er nur für die Pisten Leisten erstellt. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); }
An dieser Stelle können wir den zuvor auskommentierten Code auskommentieren, damit er Ebenen und Ausschnitte verarbeiten kann. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); }
Die Stufen werden nur an den Hängen erstellt.EinheitspaketLeisten mit Leisten
Eckfugen sind komplexer als Kantenfugen, da sie nicht an zwei, sondern an drei Zellen beteiligt sind. Jede Ecke ist mit drei Kanten verbunden, die Ebenen, Hänge oder Klippen sein können. Daher gibt es viele mögliche Konfigurationen. Wie bei Rippengelenken fügen wir der HexMesh
neuen Methode eine Triangulation hinzu .Unsere neue Methode erfordert die Eckpunkte eines eckigen Dreiecks und verbundener Zellen. Ordnen Sie die Verbindungen der Einfachheit halber so an, dass Sie wissen, welche Zelle die kleinste Höhe hat. Danach können wir von links unten und rechts mit der Arbeit beginnen.Eckverbindung. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); }
Jetzt TriangulateConnection
muss ich feststellen, welche der Zellen die niedrigste ist. Zuerst prüfen wir, ob sich die triangulierte Zelle unter ihren Nachbarn befindet oder auf dem gleichen Niveau wie die niedrigste. Wenn ja, können wir es als unterste Zelle verwenden. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } } } }
Wenn die tiefste Prüfung fehlschlägt, bedeutet dies, dass der nächste Nachbar die niedrigste Zelle ist. Für die richtige Ausrichtung müssen wir das Dreieck gegen den Uhrzeigersinn drehen. if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } }
Wenn der erste Test fehlschlägt, müssen Sie zwei benachbarte Zellen vergleichen. Wenn der Nachbar der Rippe der niedrigste ist, müssen Sie sich im Uhrzeigersinn drehen, andernfalls - gegen den Uhrzeigersinn. if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v4, neighbor, v5, nextNeighbor, v2, cell); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); }
Drehen Sie gegen den Uhrzeigersinn, keine Drehung, Drehung im Uhrzeigersinn.Hangtriangulation
Um zu wissen, wie man einen Winkel trianguliert, müssen wir verstehen, mit welchen Arten von Kanten wir es zu tun haben. Um diese Aufgabe zu vereinfachen, fügen wir eine HexCell
weitere bequeme Methode zum Erkennen der Steigung zwischen zwei beliebigen Zellen hinzu. public HexEdgeType GetEdgeType (HexCell otherCell) { return HexMetrics.GetEdgeType( elevation, otherCell.elevation ); }
Wir verwenden diese neue Methode HexMesh.TriangulateCorner
, um die Arten der linken und rechten Kante zu bestimmen. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); }
Wenn beide Rippen Steigungen sind, haben wir links und rechts Leisten. Da die untere Zelle die niedrigste ist, wissen wir außerdem, dass diese Steigungen ansteigen. Darüber hinaus haben die linke und die rechte Zelle die gleiche Höhe, dh die Verbindung der Oberkante ist flach. Wir können diesen Fall als "Hang-Hang-Ebene" oder MTP bezeichnen.Zwei Pisten und ein Flugzeug, SSP.Wir werden prüfen, ob wir uns in dieser Situation befinden, und wenn ja, werden wir eine neue Methode aufrufen TriangulateCornerTerraces
. Danach kehren wir von der Methode zurück. Fügen Sie diese Prüfung vor dem alten Triangulationscode ein, damit er das ursprüngliche Dreieck ersetzt. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } } AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { }
Da wir drinnen nichts tun TriangulateCornerTerraces
, werden einige Eckknotenpunkte mit zwei Hängen zu Hohlräumen. Ob die Verbindung leer wird oder nicht, hängt davon ab, welche der Zellen niedriger ist.Es gibt eine Leere.Um die Lücke zu füllen, müssen wir die linke und rechte Kante durch einen Raum verbinden. Der Ansatz hier ist der gleiche wie beim Verbinden von Kanten, jedoch innerhalb eines dreifarbigen Dreiecks anstelle eines zweifarbigen Vierecks. Beginnen wir noch einmal mit der ersten Stufe, die jetzt ein Dreieck ist. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1); AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); }
Die erste Stufe des Dreiecks.Und wir gehen wieder direkt zur letzten Etappe. Dies ist das Viereck, das ein Trapez bildet. Der einzige Unterschied zu den Kantenverbindungen besteht darin, dass es sich nicht um zwei, sondern um vier Farben handelt. AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color);
Die letzte Stufe des Vierecks.Alle Stufen zwischen ihnen sind ebenfalls Vierecke. AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2, c3, c4); } AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color);
Alle Stufen.Zwei Steigungsvarianten
Der Fall mit zwei Steigungen weist zwei Variationen mit unterschiedlichen Ausrichtungen auf, je nachdem, welche der Zellen der Boden ist. Wir können sie finden, indem wir die Links-Rechts-Kombinationen auf Steigungsebene und Ebenenneigung überprüfen.ATP und MSS.Wenn die rechte Kante flach ist, sollten wir links und nicht unten Leisten erstellen. Wenn der linke Rand flach ist, müssen Sie rechts beginnen. if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } }
Aus diesem Grund werden die Leisten ohne Unterbrechung um die Zellen herumgeführt, bis sie die Klippe oder das Ende der Karte erreichen.Feste Leisten.EinheitspaketVerschmelzung von Hängen und Klippen
Was ist mit der Verbindung von Hang und Klippe? Wenn wir wissen, dass der linke Rand ein Hang und der rechte Rand eine Klippe ist, was ist dann der obere Rand? Es kann nicht flach sein, aber es kann entweder ein Hang oder eine Klippe sein.SOS und COO.Fügen wir eine neue Methode hinzu, um alle Fälle von Hangklippen zu behandeln. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { }
Es sollte als letzte Option aufgerufen werden, TriangulateCorner
wenn der linke Rand eine Steigung ist. if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } }
Wie triangulieren wir das? Diese Aufgabe kann in zwei Teile unterteilt werden: untere und obere.Unterteil
Der untere Teil hat links Leisten und rechts eine Klippe. Wir müssen sie irgendwie kombinieren. Der einfachste Weg, dies zu tun, besteht darin, die Leisten so zusammenzudrücken, dass sie sich in der rechten Ecke treffen. Dadurch werden die Leisten angehoben.Kompression von Leisten.Tatsächlich möchten wir jedoch nicht, dass sie sich in der rechten Ecke treffen, da dies die oben möglicherweise vorhandenen Leisten beeinträchtigt. Außerdem können wir mit einer sehr hohen Klippe umgehen, wodurch wir sehr stark fallende und dünne Dreiecke bekommen. Stattdessen werden wir sie auf einen Grenzpunkt komprimieren, der entlang einer Klippe liegt.Kompression an der Grenze.Positionieren wir den Grenzpunkt eine Ebene über der unteren Zelle. Sie können es durch Interpolation basierend auf dem Höhenunterschied finden. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); }
Um sicherzustellen, dass wir es richtig verstanden haben, bedecken wir den gesamten unteren Teil mit einem Dreieck. float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); AddTriangle(begin, left, boundary); AddTriangleColor(beginCell.color, leftCell.color, boundaryColor);
Unteres Dreieck.Nachdem wir den Rand an der richtigen Stelle platziert haben, können wir mit der Triangulation der Leisten fortfahren. Beginnen wir erst wieder von der ersten Stufe. float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor);
Die erste Stufe der Komprimierung.Dieses Mal wird die letzte Stufe auch ein Dreieck sein. AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor);
Die letzte Stufe der Komprimierung.Und alle Zwischenschritte sind auch Dreiecke. AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor);
Komprimierte Leisten.Können wir die Kante nicht gerade halten?, , , . . , . .
Eckvervollständigung
Wenn Sie unten fertig sind, können Sie nach oben gehen. Wenn die Oberkante ein Hang ist, müssen wir wieder die Leisten und die Klippe verbinden. Verschieben wir diesen Code in eine separate Methode. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); } void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Jetzt wird es einfach sein, die Spitze zu vervollständigen. Wenn wir eine Neigung haben, fügen Sie das gedrehte Dreieck des Randes hinzu. Ansonsten reicht ein einfaches Dreieck. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } }
Vollständige Triangulation beider Teile.Gespiegelte Fälle
Wir haben die Fälle von „Hangklippen“ untersucht. Es gibt auch zwei Spiegelgehäuse, von denen jedes links eine Klippe hat.OSS und CCA.Wir werden den vorherigen Ansatz verwenden, mit geringfügigen Unterschieden aufgrund einer Änderung der Ausrichtung. Wir kopieren es TriangulateCornerTerracesCliff
und ändern es entsprechend. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, left, b); Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b); TriangulateBoundaryTriangle( right, rightCell, begin, beginCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } }
Fügen Sie diese Fälle hinzu TriangulateCorner
. if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; }
Trianguliertes OSS und CCA.Doppelklippen
Die einzigen verbleibenden nichtplanaren Fälle sind die unteren Zellen mit Klippen auf beiden Seiten. In diesem Fall kann die obere Rippe beliebig flach, geneigt oder klippenförmig sein. Wir interessieren uns nur für den Fall „Cliff-Cliff-Slope“, da er nur Vorsprünge haben wird.Tatsächlich gibt es zwei verschiedene Versionen des „Cliff-Cliff-Slope“, je nachdem welche Seite höher ist. Sie sind Spiegelbilder voneinander. Bezeichnen wir sie als OOSP und OOSL.OOSP und OOSL.Wir können beide Fälle TriangulateCorner
durch Aufrufen von Methoden TriangulateCornerCliffTerraces
und TriangulateCornerTerracesCliff
mit unterschiedlichen Zellrotationen abdecken . if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { … } if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } return; }
Dies erzeugt jedoch eine seltsame Triangulation. Dies liegt daran, dass wir jetzt von oben nach unten triangulieren. Aus diesem Grund wird unsere Grenze als negativ interpoliert, was falsch ist. Die Lösung besteht darin, immer positive Interpolatoren zu haben. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … }
Trianguliertes OOSP und OOSL.Fegen
Wir haben alle Fälle untersucht, die eine besondere Behandlung erfordern, um die korrekte Triangulation der Leisten sicherzustellen.Komplette Triangulation mit Leisten.Wir können ein bisschen aufräumen, indem TriangulateCorner
wir die Operatoren loswerden return
und stattdessen Blöcke verwenden else
. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } else if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); } else { TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } } else { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } }
Der letzte Block else
deckt alle verbleibenden Fälle ab, die noch nicht behandelt wurden. Diese Fälle sind RFP (Plane-Plane-Plane), OOP, LLC und LLC. Alle von ihnen sind von einem Dreieck bedeckt..unitypackage