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 16: Den Weg finden
- Markieren Sie Zellen
- Wählen Sie ein Suchziel
- Finde den kürzesten Weg
- Erstellen Sie eine Prioritätswarteschlange
Nachdem wir die Abstände zwischen den Zellen berechnet hatten, suchten wir die Pfade zwischen ihnen.
Ab diesem Teil werden in Unity 5.6.0 Hexagon Map-Tutorials erstellt. Es sollte beachtet werden, dass es in 5.6 einen Fehler gibt, der Arrays von Texturen in Assemblys für mehrere Plattformen zerstört. Sie können dies umgehen, indem Sie
Is Readable in den Texturarray-Inspektor aufnehmen.
Eine Reise planenMarkierte Zellen
Um den Pfad zwischen zwei Zellen zu durchsuchen, müssen wir zuerst diese Zellen auswählen. Es ist mehr als nur die Auswahl einer Zelle und die Überwachung der Suche auf der Karte. Zum Beispiel werden wir zuerst die erste Zelle und dann die letzte auswählen. In diesem Fall wäre es praktisch, wenn sie hervorgehoben würden. Fügen wir daher eine solche Funktionalität hinzu. Bis wir eine ausgefeilte oder effiziente Art der Hervorhebung erstellen, erstellen wir nur etwas, das uns bei der Entwicklung hilft.
Gliederung Textur
Eine einfache Möglichkeit, Zellen auszuwählen, besteht darin, ihnen einen Pfad hinzuzufügen. Der einfachste Weg, dies zu tun, ist mit einer Textur, die einen sechseckigen Umriss enthält.
Hier können Sie eine solche Textur herunterladen. Es ist bis auf den weißen Umriss des Sechsecks transparent. Nachdem wir es weiß gemacht haben, können wir es in Zukunft nach Bedarf einfärben.
Zellenumriss auf schwarzem HintergrundImportieren Sie die Textur und setzen Sie den
Texturtyp auf
Sprite . Ihr
Sprite-Modus wird mit den Standardeinstellungen auf
Single eingestellt . Da dies eine außergewöhnlich weiße Textur ist, müssen wir nicht in
sRGB konvertieren. Der Alpha-Kanal zeigt Transparenz an. Aktivieren Sie also
Alpha ist Transparenz . Ich habe auch die
Filtermodus- Textur auf
Trilinear eingestellt , da sonst die Mip-Übergänge für die Pfade möglicherweise zu auffällig werden.
TexturimportoptionenEin Sprite pro Zelle
Der schnellste Weg besteht darin, den Zellen eine mögliche Kontur hinzuzufügen und jedes eigene Sprite hinzuzufügen. Erstellen Sie ein neues Spielobjekt, fügen Sie die Image-Komponente (
Component / UI / Image ) hinzu und weisen Sie ihr unser Gliederungssprite zu. Fügen Sie dann die
Hex Cell Label- Fertighausinstanz in die Szene ein, machen Sie das Sprite-Objekt zu einem untergeordneten Objekt, übernehmen Sie die Änderungen auf das Fertighaus und entfernen Sie das Fertighaus.
Fertiges untergeordnetes AuswahlelementJetzt hat jede Zelle ein Sprite, aber es wird zu groß sein. Ändern Sie die
Breite und
Höhe der Transformationskomponente des Sprites auf 17, damit die Konturen mit den Zentren der Zellen übereinstimmen.
Auswahl-Sprites, die teilweise durch ein Relief verborgen sindAuf alles zeichnen
Da die Kontur dem Bereich der Zellenränder überlagert ist, erscheint sie häufig unter der Geometrie des Reliefs. Aus diesem Grund verschwindet ein Teil der Schaltung. Dies kann vermieden werden, indem die Sprites leicht vertikal angehoben werden, jedoch nicht bei Brüchen. Stattdessen können wir Folgendes tun: Zeichne immer Sprites über alles andere. Erstellen Sie dazu Ihren eigenen Sprite-Shader. Es wird ausreichen, den Standard-Unity-Sprite-Shader zu kopieren und einige Änderungen daran vorzunehmen.
Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM
Die erste Änderung besteht darin, dass wir den Tiefenpuffer ignorieren, sodass der Z-Test immer erfolgreich ist.
ZWrite Off ZTest Always
Die zweite Änderung besteht darin, dass wir nach dem Rest der transparenten Geometrie rendern. Genug, um der Transparenzwarteschlange 10 hinzuzufügen.
"Queue"="Transparent+10"
Erstellen Sie ein neues Material, das dieser Shader verwenden wird. Wir können alle seine Eigenschaften ignorieren und dabei die Standardwerte einhalten. Lassen Sie dann das Sprite-Fertighaus dieses Material verwenden.
Wir verwenden unser eigenes Sprite-MaterialJetzt sind die Konturen der Auswahl immer sichtbar. Selbst wenn die Zelle unter einem höheren Relief verborgen ist, wird ihr Umriss immer noch über alles andere gezeichnet. Es mag nicht schön aussehen, aber die ausgewählten Zellen sind immer sichtbar, was für uns nützlich ist.
Ignorieren Sie den TiefenpufferAuswahlkontrolle
Wir möchten nicht, dass alle Zellen gleichzeitig hervorgehoben werden. Tatsächlich sollten sie zunächst alle nicht ausgewählt sein. Wir können dies implementieren, indem wir die Image-Komponente des Prelight-Objekts
Highlight deaktivieren.
Bildkomponente deaktiviertHexCell
EnableHighlight
die
HexCell
Methode hinzu, um die
HexCell
zu
EnableHighlight
. Es sollte das einzige Kind seines
uiRect
und seine Image-Komponente enthalten. Wir werden auch die
DisableHighlight
Methode erstellen.
public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; }
Schließlich können wir die Farbe so festlegen, dass die Hintergrundbeleuchtung beim Einschalten einen Farbton erhält.
public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; }
EinheitspaketDen Weg finden
Nachdem wir die Zellen auswählen können, müssen wir zwei Zellen auswählen und dann den Pfad zwischen ihnen finden. Zuerst müssen wir die Zellen auswählen, dann die Suche auf einen Pfad zwischen ihnen beschränken und schließlich diesen Pfad anzeigen.
Suche starten
Wir müssen zwei verschiedene Zellen auswählen, den Start- und den Endpunkt der Suche. Angenommen, um die erste Suchzelle auszuwählen, halten Sie die linke Umschalttaste gedrückt, während Sie mit der Maus klicken. In diesem Fall wird die Zelle blau hervorgehoben. Wir müssen den Link zu dieser Zelle für die weitere Suche speichern. Außerdem muss bei der Auswahl einer neuen Startzelle die Auswahl der alten deaktiviert werden. Daher fügen wir
searchFromCell
das Feld
searchFromCell
.
HexCell previousCell, searchFromCell;
In
HandleInput
wir
Input.GetKey(KeyCode.LeftShift)
, um die gedrückte Umschalttaste zu testen.
if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); }
Wo soll man suchen?Endpunkt suchen
Anstatt nach allen Abständen zu einer Zelle zu suchen, suchen wir jetzt nach einem Pfad zwischen zwei bestimmten Zellen.
HexGrid.FindDistancesTo
HexGrid.FindPath
daher
HexGrid.FindDistancesTo
in
HexGrid.FindPath
und geben Sie ihm den zweiten
HexCell
Parameter.
HexGrid.FindPath
Sie außerdem die
HexCell
.
public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … }
Jetzt sollte
HexMapEditor.HandleInput
die geänderte Methode aufrufen und
currentCell
und
currentCell
als Argumente verwenden. Außerdem können wir nur suchen, wenn wir wissen, aus welcher Zelle wir suchen sollen. Und wir müssen uns nicht die Mühe machen, zu suchen, ob Start- und Endpunkt übereinstimmen.
if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); }
Wenn wir uns der Suche zuwenden, müssen wir zuerst alle vorherigen Auswahlen entfernen.
HexGrid.Search
daher
HexGrid.Search
, dass
HexGrid.Search
die Auswahl
HexGrid.Search
, wenn Sie Entfernungen zurücksetzen. Da dies auch die Beleuchtung der Ausgangszelle ausschaltet, schalten Sie sie wieder ein. In dieser Phase können wir auch den Endpunkt hervorheben. Lass sie rot werden.
IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … }
Endpunkte eines möglichen PfadesSuche einschränken
Zu diesem Zeitpunkt berechnet unser Suchalgorithmus noch die Entfernungen zu allen Zellen, die von der Startzelle aus erreichbar sind. Aber wir brauchen es nicht mehr. Wir können anhalten, sobald wir die endgültige Entfernung zur endgültigen Zelle gefunden haben. Das heißt, wenn die aktuelle Zelle endlich ist, können wir die Algorithmusschleife verlassen.
while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } }
Halten Sie am Endpunkt anWas passiert, wenn der Endpunkt nicht erreicht werden kann?Dann arbeitet der Algorithmus weiter, bis alle erreichbaren Zellen gefunden sind. Ohne die Möglichkeit eines vorzeitigen Ausstiegs funktioniert es wie die alte FindDistancesTo
Methode.
Pfadanzeige
Wir können den Abstand zwischen dem Anfang und dem Ende des Pfades finden, wissen aber noch nicht, wie der tatsächliche Pfad aussehen wird. Um es zu finden, müssen Sie verfolgen, wie jede Zelle erreicht wird. Aber wie geht das?
Wenn Sie dem Rand eine Zelle hinzufügen, tun wir dies, weil sie ein Nachbar der aktuellen Zelle ist. Die einzige Ausnahme ist die Startzelle. Alle anderen Zellen wurden über die aktuelle Zelle erreicht. Wenn wir verfolgen, von welcher Zelle aus jede Zelle erreicht wurde, erhalten wir ein Netzwerk von Zellen. Genauer gesagt, ein baumartiges Netzwerk, dessen Wurzel der Ausgangspunkt ist. Wir können es verwenden, um den Pfad nach Erreichen des Endpunkts zu erstellen.
Baumnetzwerk, das Pfade zum Zentrum beschreibtWir können diese Informationen speichern, indem wir einen Link zu einer anderen Zelle in
HexCell
. Wir müssen diese Daten nicht serialisieren, daher verwenden wir hierfür die Standardeigenschaft.
public HexCell PathFrom { get; set; }
HexGrid.Search
in
HexGrid.Search
den
PathFrom
Wert des Nachbarn auf die aktuelle Zelle fest, wenn Sie ihn dem Rahmen hinzufügen. Außerdem müssen wir diesen Link ändern, wenn wir einen kürzeren Weg zum Nachbarn finden.
if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; }
Nachdem wir den Endpunkt erreicht haben, können wir den Pfad visualisieren, indem wir diesen Links zurück zur Startzelle folgen und sie auswählen.
if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
Pfad gefundenEs ist zu bedenken, dass es oft mehrere kürzeste Wege gibt. Die gefundene hängt von der Verarbeitungsreihenfolge der Zellen ab. Einige Pfade mögen gut aussehen, andere mögen schlecht sein, aber es gibt nie einen kürzeren Pfad. Wir werden später darauf zurückkommen.
Start der Suche ändern
Nach Auswahl des Startpunkts wird durch Ändern des Endpunkts eine neue Suche ausgelöst. Das gleiche sollte passieren, wenn Sie eine neue Startzelle auswählen. Um dies zu ermöglichen, muss sich
HexMapEditor
auch den Endpunkt merken.
HexCell previousCell, searchFromCell, searchToCell;
Über dieses Feld können wir auch eine neue Suche starten, wenn wir einen neuen Anfang auswählen.
else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); }
Außerdem müssen wir gleiche Start- und Endpunkte vermeiden.
if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … }
EinheitspaketIntelligentere Suche
Obwohl unser Algorithmus den kürzesten Weg findet, verbringt er viel Zeit damit, Punkte zu erkunden, die offensichtlich nicht Teil dieses Weges werden. Zumindest ist es für uns offensichtlich. Der Algorithmus kann nicht auf die Karte herabblicken und nicht erkennen, dass eine Suche in einige Richtungen bedeutungslos ist. Er zieht es vor, auf den Straßen zu fahren, obwohl sie vom Endpunkt in die entgegengesetzte Richtung fahren. Ist es möglich, die Suche intelligenter zu gestalten?
Im Moment berücksichtigen wir bei der Auswahl der Zelle, die als nächstes verarbeitet werden soll, nur den Abstand von der Zelle zum Anfang. Wenn wir klüger machen wollen, müssen wir auch den Abstand zum Endpunkt berücksichtigen. Leider kennen wir ihn noch nicht. Wir können jedoch eine Schätzung der verbleibenden Entfernung erstellen. Wenn wir diese Schätzung zur Entfernung zur Zelle hinzufügen, erhalten wir ein Verständnis der Gesamtlänge des Pfades, der durch diese Zelle verläuft. Dann können wir damit die Zellensuche priorisieren.
Suchheuristik
Wenn wir anstelle genau bekannter Daten Schätzungen oder Vermutungen verwenden, spricht man von Suchheuristiken. Diese Heuristik gibt die beste Schätzung der verbleibenden Entfernung wieder. Wir müssen diesen Wert für jede Zelle bestimmen, nach der wir suchen, und fügen daher eine
HexCell
Ganzzahl-Eigenschaft hinzu. Wir müssen es nicht serialisieren, daher reicht eine andere Standardeigenschaft aus.
public int SearchHeuristic { get; set; }
Wie nehmen wir eine Annahme über die verbleibende Entfernung an? Im Idealfall haben wir eine Straße, die direkt zum Endpunkt führt. Wenn ja, ist der Abstand gleich dem unveränderten Abstand zwischen den Koordinaten dieser Zelle und der endgültigen Zelle. Nutzen wir dies in unserer Heuristik.
Da die Heuristik nicht von einem zuvor zurückgelegten Pfad abhängt, ist sie im Suchprozess konstant. Daher müssen wir es nur einmal berechnen, wenn
HexGrid.Search
dem Rand eine Zelle hinzufügt.
if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); }
Suchpriorität
Von nun an bestimmen wir die Priorität der Suche anhand der Entfernung zur Zelle und ihrer Heuristiken.
HexCell
wir in
HexCell
eine Eigenschaft für diesen Wert
HexCell
.
public int SearchPriority { get { return distance + SearchHeuristic; } }
Damit dies funktioniert,
HexGrid.Search
so, dass diese Eigenschaft zum Sortieren des
HexGrid.Search
verwendet wird.
frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) );
Suche ohne Heuristik und mit HeuristikGültige Heuristik
Dank der neuen Suchprioritäten werden wir dadurch tatsächlich weniger Zellen besuchen. Auf einer einheitlichen Karte verarbeitet der Algorithmus jedoch immer noch Zellen, die in die falsche Richtung weisen. Dies liegt daran, dass die Kosten für jeden Bewegungsschritt standardmäßig 5 betragen und die Heuristik pro Schritt nur 1 addiert. Das heißt, der Einfluss der Heuristik ist nicht sehr stark.
Wenn die Kosten für das Weiterziehen aller Karten gleich sind, können wir bei der Ermittlung der Heuristik dieselben Kosten verwenden. In unserem Fall ist dies die aktuelle Heuristik multipliziert mit 5. Dies reduziert die Anzahl der verarbeiteten Zellen erheblich.
Verwenden von Heuristiken × 5Wenn sich jedoch Straßen auf der Karte befinden, können wir die verbleibende Entfernung überschätzen. Infolgedessen kann der Algorithmus Fehler machen und einen Pfad erstellen, der eigentlich nicht der kürzeste ist.
Überbewertete und gültige HeuristikenUm sicherzustellen, dass der kürzeste Weg gefunden wird, müssen wir sicherstellen, dass wir die verbleibende Entfernung niemals überschätzen. Dieser Ansatz wird als gültige Heuristik bezeichnet. Da die minimalen Umzugskosten 1 betragen, haben wir keine andere Wahl, als die gleichen Kosten für die Bestimmung der Heuristik zu verwenden.
Genau genommen ist es ganz normal, noch niedrigere Kosten zu verwenden, aber dies wird die Heuristik nur schwächer machen. Die minimal mögliche Heuristik ist Null, was uns nur den Dijkstra-Algorithmus gibt. Bei einer Heuristik ungleich Null heißt der Algorithmus A
* (ausgesprochen "A-Stern").
Warum heißt es A *?Die Idee, dem Dijkstra-Algorithmus Heuristiken hinzuzufügen, wurde zuerst von Niels Nilsson vorgeschlagen. Er nannte seine Version A1. Bertram Rafael fand später die beste Version, die er A2 nannte. Dann hat Peter Hart bewiesen, dass mit einer guten Heuristik A2 optimal ist, das heißt, es kann keine bessere Version geben. Dies zwang ihn, den Algorithmus A * aufzurufen, um zu zeigen, dass er nicht verbessert werden konnte, dh A3 oder A4 würden nicht erscheinen. Ja, der A * -Algorithmus ist der beste, den wir bekommen können, aber er ist so gut wie seine Heuristik.
EinheitspaketPrioritätswarteschlange
Obwohl A
* ein guter Algorithmus ist, ist unsere Implementierung nicht so effektiv, da wir eine Liste zum Speichern des Rahmens verwenden, der bei jeder Iteration sortiert werden muss. Wie im vorherigen Teil erwähnt, benötigen wir eine Prioritätswarteschlange, deren Standardimplementierung jedoch nicht vorhanden ist. Lassen Sie es uns deshalb selbst erstellen.
Unser Zug sollte die Einstellung und den Ausschluss aus der Warteschlange basierend auf der Priorität unterstützen. Es sollte auch das Ändern der Priorität einer Zelle unterstützen, die sich bereits in der Warteschlange befindet. Im Idealfall implementieren wir es und minimieren die Suche nach Sortierung und zugewiesenem Speicher. Außerdem sollte es einfach bleiben.
Erstellen Sie Ihre eigene Warteschlange
Erstellen Sie eine neue
HexCellPriorityQueue
Klasse mit den erforderlichen allgemeinen Methoden. Wir verwenden eine einfache Liste, um den Inhalt einer Warteschlange zu verfolgen. Außerdem fügen wir die
Clear
Methode hinzu, um die Warteschlange zu löschen, damit sie wiederholt verwendet werden kann.
using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } }
Wir speichern Zellprioritäten in den Zellen selbst. Das heißt, bevor eine Zelle zur Warteschlange hinzugefügt wird, muss ihre Priorität festgelegt werden. Im Falle einer Prioritätsänderung ist es wahrscheinlich hilfreich zu wissen, wie die alte Priorität lautete. Fügen wir dies also als Parameter zu
Change
.
public void Change (HexCell cell, int oldPriority) { }
Es ist auch nützlich zu wissen, wie viele Zellen sich in der Warteschlange befinden. Fügen Sie daher die
Count
Eigenschaft hinzu. Verwenden Sie einfach das Feld, für das wir das entsprechende Inkrementieren und Dekrementieren durchführen.
int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; }
Zur Warteschlange hinzufügen
Wenn eine Zelle zur Warteschlange hinzugefügt wird, verwenden wir zunächst ihre Priorität als Index und behandeln die Liste als einfaches Array.
public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; }
Dies funktioniert jedoch nur, wenn die Liste lang genug ist, da wir sonst die Grenzen überschreiten. Sie können dies vermeiden, indem Sie der Liste leere Elemente hinzufügen, bis die erforderliche Länge erreicht ist. Diese leeren Elemente verweisen nicht auf die Zelle, sodass Sie sie erstellen können, indem Sie der Liste
null
hinzufügen.
int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell;
Liste mit LöchernAuf diese Weise speichern wir jedoch nur eine Zelle pro Priorität, und höchstwahrscheinlich wird es mehrere geben. Um alle Zellen mit derselben Priorität zu verfolgen, müssen wir eine andere Liste verwenden. Obwohl wir für jede Priorität eine echte Liste verwenden können, können wir
HexCell
auch eine Eigenschaft
HexCell
, um sie miteinander zu verbinden. Auf diese Weise können wir eine Zellkette erstellen, die als verknüpfte Liste bezeichnet wird.
public HexCell NextWithSamePriority { get; set; }
Lassen Sie
HexCellPriorityQueue.Enqueue
die neu hinzugefügte Zelle zwingen, auf den aktuellen Wert mit derselben Priorität zu verweisen, bevor Sie sie löschen, um eine Kette zu erstellen.
cell.NextWithSamePriority = list[priority]; list[priority] = cell;
Liste der verknüpften ListenAus der Warteschlange entfernen
Um eine Zelle aus einer Prioritätswarteschlange abzurufen, müssen wir auf die verknüpfte Liste am niedrigsten nicht leeren Index zugreifen. Daher werden wir die Liste in einer Schleife durchlaufen, bis wir sie finden. Wenn wir nicht finden, ist die Warteschlange leer und wir geben
null
.
Aus der gefundenen Kette können wir jede Zelle zurückgeben, da sie alle die gleiche Priorität haben. Am einfachsten ist es, die Zelle vom Anfang der Kette zurückzugeben.
public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; }
Verwenden Sie die nächste Zelle mit der gleichen Priorität wie der Neustart, um die Verbindung zur verbleibenden Kette aufrechtzuerhalten. Wenn es auf dieser Prioritätsstufe nur eine Zelle gab, wird das Element
null
und wird in Zukunft übersprungen.
if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; }
Minimale Verfolgung
Dieser Ansatz funktioniert, durchläuft die Liste jedoch jedes Mal, wenn eine Zelle empfangen wird. Wir können es nicht vermeiden, den kleinsten nicht leeren Index zu finden, aber wir müssen nicht jedes Mal von vorne anfangen. Stattdessen können wir die Mindestpriorität verfolgen und die Suche damit starten. Anfangs ist das Minimum im Wesentlichen gleich unendlich.
int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; }
Wenn Sie der Warteschlange eine Zelle hinzufügen, ändern wir das Minimum nach Bedarf.
public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … }
Und wenn wir uns aus der Warteschlange zurückziehen, verwenden wir mindestens die Liste für Iterationen und beginnen nicht bei Null.
public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; }
Dies reduziert die Zeit, die zum Umgehen der Prioritätslistenschleife benötigt wird, erheblich.
Prioritäten ändern
Wenn Sie die Priorität einer Zelle ändern, muss diese aus der verknüpften Liste entfernt werden, zu der sie gehört. Dazu müssen wir der Kette folgen, bis wir sie finden.
Beginnen wir damit, dass der Kopf der alten Prioritätsliste die aktuelle Zelle ist und wir auch die nächste Zelle verfolgen. Wir können sofort die nächste Zelle nehmen, da wir wissen, dass dieser Index mindestens eine Zelle enthält.
public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; }
Wenn die aktuelle Zelle eine geänderte Zelle ist, ist dies die Kopfzelle, und wir können sie abschneiden, als hätten wir sie aus der Warteschlange gezogen.
HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; }
Wenn dies nicht der Fall ist, müssen wir der Kette folgen, bis wir uns in der Zelle vor der veränderten Zelle befinden. Es enthält einen Link zu der Zelle, die geändert wurde.
if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } }
Zu diesem Zeitpunkt können wir die geänderte Zelle aus der verknüpften Liste entfernen und überspringen.
while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority;
Nachdem Sie eine Zelle gelöscht haben, müssen Sie sie erneut hinzufügen, damit sie in der Liste ihrer neuen Priorität angezeigt wird.
public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); }
Die
Enqueue
Methode erhöht den Zähler, aber in Wirklichkeit fügen wir keine neue Zelle hinzu. Um dies zu kompensieren, müssen wir daher den Zähler dekrementieren.
Enqueue(cell); count -= 1;
Verwendung der Warteschlange
Jetzt können wir unsere Prioritätswarteschlange bei
HexGrid
. Dies kann mit einer einzigen Instanz erfolgen, die für alle Suchvorgänge wiederverwendbar ist.
HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … }
Vor dem Starten der Schleife muss die Methode Search
zuerst zur Warteschlange hinzugefügt werden fromCell
, und jede Iteration beginnt mit der Ausgabe der Zelle aus der Warteschlange. Dies ersetzt den alten Grenzcode. WaitForSeconds delay = new WaitForSeconds(1 / 60f);
Ändern Sie den Code so, dass er den Nachbarn hinzufügt und ändert. Vor der Änderung werden wir uns an die alte Priorität erinnern. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates);
Außerdem müssen wir die Grenze nicht mehr sortieren.
Suche mit einer PrioritätswarteschlangeWie bereits erwähnt, hängt der kürzeste gefundene Pfad von der Verarbeitungsreihenfolge der Zellen ab. Unser Zug erstellt eine andere Reihenfolge als die sortierte Liste, damit wir andere Wege finden können. Da wir für jede Priorität den Kopf der verknüpften Liste hinzufügen und daraus entfernen, ähneln sie eher Stapeln als Warteschlangen. Zuletzt hinzugefügte Zellen werden zuerst verarbeitet. Ein Nebeneffekt dieses Ansatzes ist, dass der Algorithmus zickzackanfällig ist. Daher steigt auch die Wahrscheinlichkeit von Zickzackpfaden. Glücklicherweise sehen solche Pfade normalerweise besser aus, so dass dieser Nebeneffekt zu unseren Gunsten ist.Sortierte Liste und Warteschlange mit Unitypackage- PrioritätTeil 17: Bewegungseinschränkung
- Wir finden Wege für eine schrittweise Bewegung.
- Zeigen Sie den Pfad sofort an.
- Wir schaffen eine effektivere Suche.
- Wir visualisieren nur den Weg.
In diesem Teil werden wir die Bewegung in Bewegungen unterteilen und die Suche so weit wie möglich beschleunigen.Reisen Sie aus mehreren ZügenSchritt für Schritt Bewegung
Strategiespiele mit Sechsecknetzen sind fast immer rundenbasiert. Einheiten, die sich auf der Karte bewegen, haben eine begrenzte Geschwindigkeit, die die in einer Runde zurückgelegte Strecke begrenzt.Geschwindigkeit
Um die eingeschränkte Bewegung zu unterstützen, fügen wir HexGrid.FindPath
den HexGrid.Search
Integer-Parameter hinzu speed
. Es bestimmt den Bewegungsbereich für eine Bewegung. public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … }
Verschiedene Arten von Einheiten im Spiel verwenden unterschiedliche Geschwindigkeiten. Kavallerie ist schnell, Infanterie ist langsam und so weiter. Wir haben noch keine Einheiten, daher werden wir vorerst eine konstante Geschwindigkeit verwenden. Nehmen wir einen Wert von 24. Dies ist ein ziemlich großer Wert, der nicht durch 5 teilbar ist (die Standardkosten für den Umzug). Fügen Sie für ein Argument FindPath
bei HexMapEditor.HandleInput
einer konstanten Geschwindigkeit. if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); }
Bewegt sich
Zusätzlich zur Verfolgung der Gesamtkosten für die Bewegung entlang des Pfades müssen wir jetzt auch wissen, wie viele Bewegungen erforderlich sind, um sich entlang des Pfades zu bewegen. Wir müssen diese Informationen jedoch nicht in jeder Zelle speichern. Sie kann durch Teilen der zurückgelegten Strecke durch die Geschwindigkeit erhalten werden. Da es sich um Ganzzahlen handelt, verwenden wir die Ganzzahldivision. Das heißt, die Gesamtentfernungen von nicht mehr als 24 entsprechen dem Kurs 0. Dies bedeutet, dass der gesamte Pfad im aktuellen Kurs abgeschlossen werden kann. Wenn sich der Endpunkt in einem Abstand von 30 befindet, muss dies Runde 1 sein. Um zum Endpunkt zu gelangen, muss die Einheit ihre gesamte Bewegung in der aktuellen Runde und in einem Teil der nächsten Runde ausführen.Lassen Sie uns den Verlauf der aktuellen Zelle und aller darin befindlichen Nachbarn bestimmenHexGrid.Search
. Der Verlauf der aktuellen Zelle kann nur einmal berechnet werden, bevor der Nachbarzyklus ausgeführt wird. Die Bewegung des Nachbarn kann bestimmt werden, sobald wir die Entfernung zu ihm finden. int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … }
Verlorene Bewegung
Wenn der Zug des Nachbarn größer ist als der aktuelle Zug, haben wir die Grenze des Zuges überschritten. Wenn die Bewegung, die notwendig ist, um einen Nachbarn zu erreichen, 1 war, ist alles in Ordnung. Wenn der Umzug in die nächste Zelle jedoch teurer ist, wird alles komplizierter.Angenommen, wir bewegen uns entlang einer homogenen Karte, dh um in jede Zelle zu gelangen, benötigen Sie 5 Bewegungseinheiten. Unsere Geschwindigkeit beträgt 24. Nach vier Schritten haben wir 20 Einheiten aus unserem Bewegungsbestand ausgegeben, und es sind noch 4 übrig. Im nächsten Schritt werden wieder 5 Einheiten benötigt, dh eine mehr als die verfügbaren. Was müssen wir in dieser Phase tun?Es gibt zwei Ansätze für diese Situation. Die erste besteht darin, der Einheit zu erlauben, in der aktuellen Runde die fünfte Zelle zu betreten, selbst wenn wir nicht genug Bewegung haben. Die zweite besteht darin, die Bewegung während der aktuellen Bewegung zu verbieten, dh die verbleibenden Bewegungspunkte können nicht verwendet werden und gehen verloren.Die Wahl der Option hängt vom Spiel ab. Im Allgemeinen ist der erste Ansatz besser für Spiele geeignet, bei denen sich Einheiten nur wenige Schritte pro Spielzug bewegen können, z. B. für Spiele der Civilization-Reihe. Dies stellt sicher, dass Einheiten immer mindestens eine Zelle pro Runde bewegen können. Wenn Einheiten viele Zellen pro Spielzug bewegen können, wie in Age of Wonders oder in Battle for Wesnoth, ist die zweite Option besser.Da wir Geschwindigkeit 24 verwenden, wählen wir den zweiten Ansatz. Damit es funktioniert, müssen wir die Kosten für den Einstieg in die nächste Zelle isolieren, bevor wir sie zur aktuellen Entfernung hinzufügen.
Wenn wir als Ergebnis die Grenze der Bewegung überschreiten, verwenden wir zuerst alle Bewegungspunkte der aktuellen Bewegung. Wir können dies tun, indem wir einfach die Bewegung mit der Geschwindigkeit multiplizieren. Danach addieren wir die Umzugskosten. int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; }
Infolgedessen werden wir den ersten Zug in der vierten Zelle mit 4 nicht verwendeten Bewegungspunkten abschließen. Diese verlorenen Punkte werden zu den Kosten der fünften Zelle addiert, sodass ihre Entfernung 29 und nicht 25 beträgt. Infolgedessen sind die Entfernungen größer als zuvor. Zum Beispiel hatte die zehnte Zelle einen Abstand von 50. Um jetzt hineinzukommen, müssen wir die Grenzen von zwei Zügen überschreiten und 8 Bewegungspunkte verlieren, dh der Abstand dazu wird jetzt 58.Länger als erwartetDa nicht verwendete Bewegungspunkte zu den Abständen zu den Zellen hinzugefügt werden, werden sie bei der Bestimmung des kürzesten Pfades berücksichtigt. Am effektivsten ist es, so wenig Punkte wie möglich zu verschwenden. Daher können wir bei unterschiedlichen Geschwindigkeiten unterschiedliche Pfade erhalten.Bewegungen statt Entfernungen anzeigen
Wenn wir das Spiel spielen, sind wir nicht sehr an den Entfernungswerten interessiert, die verwendet werden, um den kürzesten Weg zu finden. Wir sind an der Anzahl der Bewegungen interessiert, die erforderlich sind, um den Endpunkt zu erreichen. Lassen Sie uns daher anstelle von Entfernungen die Bewegungen anzeigen.Befreien Sie sich zuerst von UpdateDistanceLabel
seinem Anruf HexCell
. public int Distance { get { return distance; } set { distance = value;
Stattdessen fügen wir der HexCell
allgemeinen Methode hinzu SetLabel
, die eine beliebige Zeichenfolge empfängt. public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; }
Wir verwenden diese neue Methode zur HexGrid.Search
Reinigung von Zellen. Um Zellen auszublenden, weisen Sie sie einfach zu null
. for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); }
Dann weisen wir dem Nachbarn den Wert seines Zuges zu. Danach können wir sehen, wie viele zusätzliche Züge erforderlich sind, um den gesamten Weg zu gehen. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); }
Die Anzahl der Bewegungen, die erforderlich sind, um sich entlang des Unitypackage- Pfads zu bewegenSofortige Pfade
Außerdem ist es uns beim Spielen des Spiels egal, wie der Pfad-Suchalgorithmus den Weg findet. Wir möchten den angeforderten Pfad sofort sehen. Im Moment können wir sicher sein, dass der Algorithmus funktioniert. Lassen Sie uns also die Suchvisualisierung loswerden.Ohne Corutin
Für einen langsamen Durchgang durch den Algorithmus verwendeten wir Corutin. Wir müssen dies nicht mehr tun, damit wir Anrufe loswerden StartCoroutine
und StopAllCoroutines
c HexGrid
. Stattdessen rufen wir es einfach Search
als reguläre Methode auf. public void Load (BinaryReader reader, int header) {
Da wir es nicht mehr Search
als Coroutine verwenden, benötigt es keine Ausbeute, sodass wir diesen Operator loswerden. Dies bedeutet, dass wir auch die Deklaration entfernen WaitForSeconds
und den Rückgabetyp der Methode in ändern void
. void Search (HexCell fromCell, HexCell toCell, int speed) { …
Sofortige ErgebnisseSuchzeitdefinition
Jetzt können wir die Pfade sofort abrufen, aber wie schnell werden sie berechnet? Kurze Pfade erscheinen fast sofort, aber lange Pfade auf großen Karten scheinen etwas langsam zu sein.Lassen Sie uns messen, wie lange es dauert, den Pfad zu finden und anzuzeigen. Wir können einen Profiler verwenden, um die Suchzeit zu bestimmen, aber dies ist etwas zu viel und verursacht zusätzliche Kosten. Verwenden wir stattdessen Stopwatch
den Namespace System.Diagnostics
. Da wir es nur vorübergehend verwenden, werde ich das Konstrukt nicht using
am Anfang des Skripts hinzufügen .Erstellen Sie kurz vor der Suche eine neue Stoppuhr und starten Sie sie. Stoppen Sie nach Abschluss der Suche die Stoppuhr und zeigen Sie die verstrichene Zeit in der Konsole an. public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
Wählen wir den schlechtesten Fall für unseren Algorithmus - eine Suche von links unten nach rechts oben auf einer großen Karte. Das Schlimmste ist eine einheitliche Karte, da der Algorithmus alle 4.800 Kartenzellen verarbeiten muss.Suche im schlimmsten Fall DieSuchzeit kann unterschiedlich sein, da der Unity-Editor nicht der einzige Prozess ist, der auf Ihrem Computer ausgeführt wird. Testen Sie es also mehrmals, um die durchschnittliche Dauer zu verstehen. In meinem Fall dauert die Suche ungefähr 45 Millisekunden. Dies ist nicht sehr viel und entspricht 22,22 Pfaden pro Sekunde; bezeichnen dies als 22 pps (Pfade pro Sekunde). Dies bedeutet, dass die Framerate des Spiels in diesem Frame bei der Berechnung dieses Pfades ebenfalls um maximal 22 fps abnimmt. Und dies ohne Berücksichtigung aller anderen Arbeiten, zum Beispiel des Renderns des Rahmens selbst. Das heißt, wir bekommen eine ziemlich starke Abnahme der Bildrate, sie wird auf 20 fps fallen.Wenn Sie einen solchen Leistungstest durchführen, müssen Sie berücksichtigen, dass die Leistung des Unity-Editors nicht so hoch ist wie die Leistung der fertigen Anwendung. Wenn ich den gleichen Test mit der Baugruppe durchführe, dauert es im Durchschnitt nur 15 ms. Das sind 66 pps, was viel besser ist. Dies ist jedoch immer noch ein großer Teil der pro Frame zugewiesenen Ressourcen, sodass die Framerate unter 60 fps liegt.Wo kann ich das Debug-Protokoll für die Assembly sehen?Unity , . . , , Unity
Log Files .
Suchen Sie nur bei Bedarf.
Wir können eine einfache Optimierung vornehmen - führen Sie eine Suche nur dann durch, wenn sie benötigt wird. Während wir in jedem Frame, in dem die Maustaste gedrückt gehalten wird, eine neue Suche starten. Daher wird die Bildrate beim Ziehen und Ablegen ständig unterschätzt. Wir können dies vermeiden, HexMapEditor.HandleInput
indem wir eine neue Suche nur dann einleiten, wenn es sich wirklich um einen neuen Endpunkt handelt. Wenn nicht, ist der aktuell sichtbare Pfad weiterhin gültig. if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } }
Beschriftungen nur für den Pfad anzeigen
Das Anzeigen von Reisemarken ist ein ziemlich teurer Vorgang, insbesondere weil wir einen nicht optimierten Ansatz verwenden. Das Ausführen dieses Vorgangs für alle Zellen verlangsamt definitiv die Ausführung. Überspringen wir also die Beschriftung HexGrid.Search
. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance;
Wir müssen diese Informationen nur für den gefundenen Pfad sehen. Daher werden wir nach Erreichen des Endpunkts den Kurs berechnen und nur die Beschriftungen der Zellen festlegen, die sich auf dem Weg befinden. if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
Anzeigen von Beschriftungen nur für PfadzellenJetzt schließen wir nur Zellbeschriftungen zwischen Anfang und Ende ein. Aber der Endpunkt ist das Wichtigste, wir müssen auch ein Label dafür setzen. Sie können dies tun, indem Sie den Pfadzyklus von der Zielzelle aus und nicht von der Zelle davor aus starten. In diesem Fall ändert sich die Beleuchtung des Endpunkts von Rot zu Weiß, sodass die Hintergrundbeleuchtung unter dem Zyklus entfernt wird. fromCell.EnableHighlight(Color.blue);
Fortschrittsinformationen sind für den Endpunkt am wichtigsten.Nach diesen Änderungen wird die Worst-Case-Zeit im Editor auf 23 Millisekunden und in der fertigen Baugruppe auf bis zu 6 Millisekunden reduziert. Dies sind 43 pps und 166 pps - viel besser.EinheitspaketDie klügste Suche
Im vorherigen Teil haben wir das Suchverfahren durch Implementierung des A * -Algorithmus intelligenter gestaltet . In der Realität führen wir die Suche jedoch immer noch nicht optimal durch. In jeder Iteration berechnen wir die Abstände von der aktuellen Zelle zu allen Nachbarn. Dies gilt für Zellen, die noch nicht oder derzeit Teil des Suchrahmens sind. Die Zellen, die bereits von der Grenze entfernt wurden, müssen jedoch nicht mehr berücksichtigt werden, da wir bereits den kürzesten Weg zu diesen Zellen gefunden haben. Bei der korrekten Implementierung von A * werden diese Zellen übersprungen, sodass wir dasselbe tun können.Zellensuchphase
Woher wissen wir, ob eine Zelle die Grenze bereits verlassen hat? Wir können dies zwar nicht feststellen. Daher müssen Sie verfolgen, in welcher Phase der Suche sich die Zelle befindet. Sie war noch nicht an der Grenze oder ist jetzt dort oder ist im Ausland. Wir können dies verfolgen, indem wir eine HexCell
einfache Ganzzahl-Eigenschaft hinzufügen . public int SearchPhase { get; set; }
Zum Beispiel bedeutet 0, dass die Zellen noch nicht erreicht sind, 1 - dass sich die Zelle jetzt im Rand befindet und 2 - dass sie bereits vom Rand entfernt wurde.Die Grenze treffen
In können HexGrid.Search
wir alle Zellen auf 0 zurücksetzen und immer 1 für den Rand verwenden. Oder wir können die Anzahl der Rahmen mit jeder neuen Suche erhöhen. Dank dessen müssen wir uns nicht mit dem Dumping von Zellen befassen, wenn wir die Anzahl der Grenzen jedes Mal um zwei erhöhen. int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … }
Jetzt müssen wir die Phase der Zellsuche festlegen, wenn wir sie dem Rand hinzufügen. Der Prozess beginnt mit einer Anfangszelle, die dem Rand hinzugefügt wird. fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell);
Und auch jedes Mal, wenn wir der Grenze einen Nachbarn hinzufügen. if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); }
Grenzkontrolle
Um zu überprüfen, ob die Zelle noch nicht zum Rand hinzugefügt wurde, haben wir bisher einen Abstand von verwendet int.MaxValue
. Jetzt können wir die Phase der Zellsuche mit dem aktuellen Rand vergleichen.
Dies bedeutet, dass wir vor der Suche keine Zellabstände mehr zurücksetzen müssen, dh weniger arbeiten müssen, was gut ist. for (int i = 0; i < cells.Length; i++) {
Die Grenze verlassen
Wenn eine Zelle von der Grenze entfernt wird, bezeichnen wir dies durch eine Erhöhung ihrer Suchphase. Dies bringt sie über die aktuelle Grenze hinaus und vor die nächste. while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … }
Jetzt können wir Zellen überspringen, die vom Rand entfernt wurden, wodurch sinnlose Berechnungen und der Vergleich von Entfernungen vermieden werden. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … }
Zu diesem Zeitpunkt liefert unser Algorithmus immer noch die gleichen Ergebnisse, jedoch effizienter. Auf meinem Computer dauert die Worst-Case-Suche im Editor 20 ms und in der Assembly 5 ms.Wir können auch berechnen, wie oft die Zelle vom Algorithmus verarbeitet wurde, und den Zähler bei der Berechnung des Abstands zur Zelle erhöhen. Zuvor berechnete unser Algorithmus im schlimmsten Fall 28.239 Entfernungen. Im vorgefertigten A * -Algorithmus berechnen wir seine 14.120 Entfernungen. Die Menge verringerte sich um 50%. Der Grad der Auswirkung dieser Indikatoren auf die Produktivität hängt von den Kosten bei der Berechnung der Umzugskosten ab. In unserem Fall gibt es hier nicht viel Arbeit, daher ist die Verbesserung in der Baugruppe nicht sehr groß, aber im Editor sehr auffällig.EinheitspaketDen Weg frei machen
Wenn Sie eine neue Suche starten, müssen Sie zuerst die Visualisierung des vorherigen Pfads löschen. Deaktivieren Sie dabei die Auswahl und entfernen Sie die Beschriftungen aus jeder Rasterzelle. Dies ist ein sehr schwieriger Ansatz. Im Idealfall müssen wir nur die Zellen verwerfen, die Teil des vorherigen Pfads waren.Nur suchen
Beginnen wir damit, den Visualisierungscode vollständig zu entfernen Search
. Er muss nur eine Pfadsuche durchführen und muss nicht wissen, was wir mit diesen Informationen machen werden. void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); }
Um zu melden, dass Search
wir einen Weg gefunden haben, werden wir boolean zurückgeben. bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; }
Erinnere dich an den Weg
Wenn der Pfad gefunden ist, müssen wir uns daran erinnern. Dank dessen können wir es in Zukunft reinigen. Daher werden wir die Endpunkte verfolgen und feststellen, ob zwischen ihnen ein Pfad besteht. HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
Zeigen Sie den Pfad erneut an
Wir können die von uns aufgezeichneten Suchdaten verwenden, um den Pfad erneut zu visualisieren. Erstellen wir hierfür eine neue Methode ShowPath
. Es durchläuft den Zyklus vom Ende bis zum Anfang des Pfads, hebt die Zellen hervor und weist ihren Beschriftungen einen Strichwert zu. Dazu müssen wir die Geschwindigkeit kennen, also machen Sie es zu einem Parameter. Wenn wir keinen Pfad haben, wählt die Methode einfach die Endpunkte aus. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); }
Rufen Sie diese Methode FindPath
nach der Suche auf. currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed);
Fegen
Wir sehen den Weg wieder, aber jetzt bewegt er sich nicht weg. Erstellen Sie zum Löschen eine Methode ClearPath
. Tatsächlich handelt es sich um eine Kopie ShowPath
, mit der Ausnahme, dass die Auswahl und die Beschriftungen deaktiviert werden, diese jedoch nicht enthalten sind. Danach muss er die aufgezeichneten Pfaddaten löschen, die nicht mehr gültig sind. void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; }
Mit dieser Methode können wir die Visualisierung des alten Pfades löschen, indem wir nur die erforderlichen Zellen besuchen. Die Größe der Karte ist nicht mehr wichtig. Wir werden es aufrufen, FindPath
bevor wir eine neue Suche starten. sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop();
Außerdem wird der Pfad beim Erstellen einer neuen Karte gelöscht. public bool CreateMap (int x, int z) { … ClearPath(); if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
Und auch vor dem Laden einer weiteren Karte. public void Load (BinaryReader reader, int header) { ClearPath(); … }
Die Pfadvisualisierung wird wie vor dieser Änderung wieder gelöscht. Aber jetzt verwenden wir einen effizienteren Ansatz, und im schlimmsten Fall der Suche hat sich die Zeit auf 14 Millisekunden verringert. Genug ernsthafte Verbesserung nur durch intelligentere Reinigung. Die Montagezeit verringerte sich auf 3 ms, was 333 pps entspricht. Dank dessen ist die Suche nach Pfaden genau in Echtzeit anwendbar.Nachdem wir schnell nach Pfaden gesucht haben, können wir den temporären Debugging-Code entfernen. public void FindPath (HexCell fromCell, HexCell toCell, int speed) {
EinheitspaketTeil 18: Einheiten
- Wir platzieren die Trupps auf der Karte.
- Speichern und laden Sie Trupps.
- Wir finden Wege für die Truppen.
- Wir bewegen die Einheiten.
Nachdem wir herausgefunden haben, wie Sie nach einem Pfad suchen können, platzieren wir die Trupps auf der Karte.Verstärkungen kamen anSquads erstellen
Bisher haben wir uns nur mit Zellen und ihren festen Objekten befasst. Einheiten unterscheiden sich von ihnen dadurch, dass sie mobil sind. Ein Trupp kann alles in jeder Größenordnung bedeuten, von einer Person oder einem Fahrzeug bis zu einer ganzen Armee. In diesem Tutorial beschränken wir uns auf einen einfachen verallgemeinerten Einheitentyp. Danach werden wir Kombinationen verschiedener Arten von Einheiten unterstützen.Fertighaus-Trupp
Erstellen Sie einen neuen Komponententyp, um mit Trupps zu arbeiten HexUnit
. Beginnen wir zunächst mit einem leeren MonoBehaviour
und fügen später Funktionen hinzu. using UnityEngine; public class HexUnit : MonoBehaviour { }
Erstellen Sie mit dieser Komponente ein leeres Spielobjekt, das zu einem Fertighaus werden soll. Dies wird das Stammobjekt des Trupps sein.Fertighaus.Fügen Sie ein 3D-Modell hinzu, das die Ablösung als untergeordnetes Objekt symbolisiert. Ich habe einen einfachen skalierten Würfel verwendet, für den ich blaues Material erstellt habe. Das Wurzelobjekt bestimmt das Bodenniveau der Ablösung, daher verschieben wir das untergeordnete Element entsprechend.Untergeordnetes WürfelelementFügen Sie dem Trupp einen Collider hinzu, damit die Auswahl in Zukunft einfacher wird. Der Collider des Standardwürfels ist für uns sehr gut geeignet. Passen Sie den Collider einfach in eine Zelle an.Erstellen von Squad-Instanzen
Da wir noch kein Gameplay haben, erfolgt die Erstellung von Einheiten im Bearbeitungsmodus. Daher sollte dies angegangen werden HexMapEditor
. Dazu benötigt er ein Fertighaus. Fügen Sie also ein Feld hinzu HexUnit unitPrefab
und verbinden Sie es. public HexUnit unitPrefab;
Anschließen des FertighausesBeim Erstellen von Einheiten platzieren wir diese in der Zelle unter dem Cursor. Es HandleInput
gibt einen Code zum Auffinden dieser Zelle beim Bearbeiten eines Geländes. Jetzt brauchen wir es auch für die Trupps, also verschieben wir den entsprechenden Code in eine separate Methode. HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; }
Jetzt können wir diese Methode verwenden, HandleInput
um sie zu vereinfachen. void HandleInput () {
Fügen Sie als Nächstes eine neue Methode hinzu CreateUnit
, die ebenfalls verwendet wird GetCellUnderCursor
. Wenn es eine Zelle gibt, erstellen wir einen neuen Kader. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } }
Um die Hierarchie sauber zu halten, verwenden wir das Raster als übergeordnetes Element für alle Spielobjekte in den Trupps. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } }
Der einfachste Weg, HexMapEditor
Unterstützung für das Erstellen von Einheiten hinzuzufügen , ist das Drücken einer Taste. Ändern Sie die Methode Update
so, dass sie CreateUnit
beim Drücken der U-Taste aufgerufen wird. Wie bei c HandleInput
sollte dies geschehen, wenn sich der Cursor nicht über dem GUI-Element befindet. Zuerst prüfen wir, ob wir die Karte bearbeiten sollen, und wenn nicht, prüfen wir, ob wir einen Trupp hinzufügen sollen. Wenn ja, dann rufen Sie an CreateUnit
. void Update () {
Instanz des Trupps erstelltTruppenplatzierung
Jetzt können wir Einheiten erstellen, die jedoch am Ursprung der Karte angezeigt werden. Wir müssen sie an den richtigen Ort bringen. Dazu ist es notwendig, dass sich die Truppen ihrer Position bewusst sind. Daher fügen wir der HexUnit
Eigenschaft hinzu, Location
die die Zelle angibt, die sie belegen. Wenn Sie die Eigenschaft festlegen, ändern wir die Position des Teams so, dass sie mit der Position der Zelle übereinstimmt. public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location;
Jetzt HexMapEditor.CreateUnit
muss ich die Position der Squad-Zelle unter dem Cursor zuweisen. Dann werden die Einheiten dort sein, wo sie sollten. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } }
Trupps auf der KarteEinheitenorientierung
Bisher haben alle Einheiten die gleiche Ausrichtung, was ziemlich unnatürlich aussieht. Fügen Sie der HexUnit
Eigenschaft hinzu, um sie wiederzubeleben Orientation
. Dies ist ein Float-Wert, der die Drehung des Trupps entlang der Y-Achse in Grad angibt. Beim Einstellen ändern wir entsprechend die Drehung des Spielobjekts. public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation;
Die HexMapEditor.CreateUnit
assign Zufallsrotation von 0 bis 360 Grad. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
Unterschiedliche EinheitenorientierungenEin Trupp pro Zelle
Einheiten sehen gut aus, wenn sie nicht in einer Zelle erstellt werden. In diesem Fall erhalten wir eine Gruppe seltsam aussehender Würfel.Überlagerte Einheiten Beieinigen Spielen können mehrere Einheiten an einem Ort platziert werden, bei anderen nicht. Da es einfacher ist, mit einem Trupp pro Zelle zu arbeiten, werde ich diese Option wählen. Dies bedeutet, dass wir nur dann einen neuen Kader erstellen sollten, wenn die aktuelle Zelle nicht besetzt ist. Fügen Sie der HexCell
Standardeigenschaft hinzu, damit Sie dies herausfinden können Unit
. public HexUnit Unit { get; set; }
Wir verwenden diese Eigenschaft in HexUnit.Location
, um der Zelle mitzuteilen, ob sich das Gerät darauf befindet. public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } }
Jetzt HexMapEditor.CreateUnit
kann überprüft werden, ob die aktuelle Zelle frei ist. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
Besetzte Zellen bearbeiten
Anfangs werden Einheiten korrekt platziert, aber alles kann sich ändern, wenn ihre Zellen später bearbeitet werden. Wenn sich die Höhe der Zelle ändert, hängt die Einheit, die sie besetzt, entweder darüber oder stürzt hinein.Hängende und ertrunkene TruppsDie Lösung besteht darin, die Position des Trupps nach Änderungen zu überprüfen. Fügen Sie dazu die Methode hinzu HexUnit
. Bisher interessiert uns nur die Position des Kaders, also fragen Sie es einfach noch einmal. public void ValidateLocation () { transform.localPosition = location.Position; }
Wir müssen die Position der Ablösung beim Aktualisieren der Zelle koordinieren, was passiert, wenn die Methoden Refresh
oder das RefreshSelfOnly
Objekt HexCell
aufgerufen werden. Dies ist natürlich nur dann notwendig, wenn sich tatsächlich eine Ablösung in der Zelle befindet. void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } }
Trupps entfernen
Zusätzlich zum Erstellen von Einheiten wäre es nützlich, diese zu zerstören. Fügen Sie daher der HexMapEditor
Methode hinzu DestroyUnit
. Er muss prüfen, ob sich in der Zelle unter dem Cursor eine Ablösung befindet, und in diesem Fall das Spielobjekt der Ablösung zerstören. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } }
Bitte beachten Sie, dass wir durch die Zelle gehen, um zum Kader zu gelangen. Um mit dem Trupp zu interagieren, bewegen Sie einfach die Maus über die Zelle. Damit dies funktioniert, muss der Trupp keinen Collider haben. Das Hinzufügen eines Colliders erleichtert jedoch die Auswahl, da er die Strahlen blockiert, die sonst mit der Zelle hinter dem Trupp kollidieren würden. Verwendenwir Update
eine Kombination aus Links-Umschalt + U , um den Trupp zu zerstören . if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; }
Wenn wir mehrere Einheiten erstellen und zerstören, gehen wir vorsichtig vor und löschen die Eigenschaft, wenn wir die Einheit entfernen. Das heißt, wir löschen explizit die Zellverbindung zum Kader. Fügen Sie der HexUnit
Methode Die
, die sich damit befasst, sowie der Zerstörung Ihres eigenen Spielobjekts hinzu. public void Die () { location.Unit = null; Destroy(gameObject); }
Wir werden diese Methode aufrufen HexMapEditor.DestroyUnit
und den Trupp nicht direkt zerstören. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) {
EinheitspaketTrupps speichern und laden
Jetzt, da wir Einheiten auf der Karte haben können, müssen wir sie in den Speicher- und Ladevorgang einbeziehen. Wir können diese Aufgabe auf zwei Arten angehen. Die erste besteht darin, Squad-Daten beim Aufzeichnen einer Zelle aufzuzeichnen, so dass die Zellen- und Squad-Daten gemischt werden. Die zweite Möglichkeit besteht darin, Zellen- und Squad-Daten getrennt zu speichern. Obwohl es den Anschein hat, dass der erste Ansatz einfacher zu implementieren ist, liefert der zweite Ansatz strukturiertere Daten. Wenn wir die Daten teilen, wird es in Zukunft einfacher sein, mit ihnen zu arbeiten.Einheitenverfolgung
Um alle Einheiten zusammenzuhalten, müssen wir sie verfolgen. Wir werden dies tun, indem wir der HexGrid
Liste der Einheiten hinzufügen . Diese Liste sollte alle Einheiten auf der Karte enthalten. List<HexUnit> units = new List<HexUnit>();
Beim Erstellen oder Laden einer neuen Karte müssen alle Einheiten auf der Karte entfernt werden. Um diesen Prozess zu vereinfachen, erstellen Sie eine Methode ClearUnits
, mit der alle Personen in der Liste getötet und gelöscht werden. void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); }
Wir nennen diese Methode in CreateMap
und in Load
. Lass es uns tun, nachdem wir den Weg gereinigt haben. public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … }
Hinzufügen von Trupps zum Raster
Wenn wir jetzt neue Einheiten erstellen, müssen wir sie der Liste hinzufügen. Legen wir hierfür eine Methode fest AddUnit
, die sich auch mit der Position des Trupps und den Parametern seines übergeordneten Objekts befasst. public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; }
Jetzt reicht es HexMapEditor.CreatUnit
aus, AddUnit
mit einer neuen Instanz der Abteilung, ihrer Position und zufälligen Ausrichtung aufzurufen . void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) {
Squads aus dem Raster entfernen
Fügen Sie eine Methode zum Entfernen des Trupps hinzu und c HexGrid
. Entferne einfach den Trupp von der Liste und befehle ihm zu sterben. public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); }
Wir rufen diese Methode auf HexMapEditor.DestroyUnit
, anstatt den Trupp direkt zu zerstören. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) {
Einheiten speichern
Da wir alle Einheiten zusammenhalten, müssen wir uns daran erinnern, welche Zellen sie besetzen. Am zuverlässigsten ist es, die Koordinaten ihres Standorts zu speichern. Um dies zu ermöglichen, fügen wir der HexCoordinates
Methode Save
, die es schreibt , die Felder X und Z hinzu. using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } }
Die Methode Save
für HexUnit
kann jetzt die Koordinaten und die Ausrichtung des Teams aufzeichnen. Dies sind alle Daten der Einheiten, die wir im Moment haben. using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } }
Da es HexGrid
Einheiten verfolgt, zeichnet seine Methode Save
die Daten von Einheiten auf. Schreiben Sie zuerst die Gesamtzahl der Einheiten auf und gehen Sie dann alle in einer Schleife um. public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } writer.Write(units.Count); for (int i = 0; i < units.Count; i++) { units[i].Save(writer); } }
Wir haben die gespeicherten Daten geändert, sodass wir die Versionsnummer SaveLoadMenu.Save
auf 2 erhöhen . Der alte Startcode funktioniert weiterhin, da die Squad-Daten einfach nicht gelesen werden. Sie müssen jedoch die Versionsnummer erhöhen, um anzuzeigen, dass die Datei Geräteinformationen enthält. void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } }
Ladetrupps
Da es sich HexCoordinates
um eine Struktur handelt, ist es wenig sinnvoll, die übliche Methode hinzuzufügen Load
. Machen wir es zu einer statischen Methode, die gespeicherte Koordinaten liest und zurückgibt. public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; }
Da die Anzahl der Einheiten variabel ist, haben wir keine bereits vorhandenen Einheiten, in die Daten geladen werden können. Wir können neue Instanzen von Einheiten erstellen, bevor wir ihre Daten laden. Dies erfordert jedoch, dass wir HexGrid
beim Booten Instanzen neuer Einheiten erstellen. Also ist es besser, es zu verlassen HexUnit
. Wir verwenden auch die statische Methode HexUnit.Load
. Beginnen wir damit, diese Trupps einfach zu lesen. Um den Wert des Orientierungs-Floats zu lesen, verwenden wir die Methode BinaryReader.ReadSingle
.Warum Single?float
, . , double
, . Unity .
public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); }
Der nächste Schritt besteht darin, eine Instanz eines neuen Teams zu erstellen. Dazu benötigen wir jedoch einen Link zum Fertighaus des Geräts. Um dies noch nicht zu komplizieren, fügen wir hierfür eine HexUnit
statische Methode hinzu. public static HexUnit unitPrefab;
Um diesen Link zu setzen, verwenden wir ihn erneut HexGrid
, wie wir es mit der Rauschtextur getan haben. Wenn wir viele Arten von Einheiten unterstützen müssen, werden wir zu einer besseren Lösung übergehen. public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } }
Wir passieren das Fertighaus des Geräts.Nach dem Anschließen des Feldes benötigen wir keine direkte Verbindung mehr zu HexMapEditor
. Stattdessen kann er verwenden HexUnit.unitPrefab
.
Jetzt können wir eine Instanz des neuen Teams in erstellen HexUnit.Load
. Anstatt es zurückzugeben, können wir die geladenen Koordinaten und die Ausrichtung verwenden, um es dem Raster hinzuzufügen. Fügen Sie dazu einen Parameter hinzu HexGrid
. public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); }
Am Ende HexGrid.Load
zählen wir die Anzahl der Einheiten und laden damit alle gespeicherten Einheiten, wobei wir uns als zusätzliches Argument übergeben. public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
Dies funktioniert natürlich nur für Sicherungsdateien mit einer Version von nicht weniger als 2, in jüngeren Versionen müssen keine Einheiten geladen werden. if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
Jetzt können wir Dateien der Version 2 korrekt hochladen, also SaveLoadMenu.Load
erhöhen Sie die Anzahl der unterstützten Versionen auf 2. void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 2) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
EinheitspaketTruppenbewegung
Trupps sind mobil, daher müssen wir sie auf der Karte bewegen können. Wir haben bereits einen Pfad-Suchcode, aber bisher haben wir ihn nur für beliebige Stellen getestet. Jetzt müssen wir die alte Test-Benutzeroberfläche entfernen und eine neue Benutzeroberfläche für das Squad-Management erstellen.Bereinigung des Karteneditors
Das Verschieben von Einheiten entlang von Pfaden ist Teil des Spiels und gilt nicht für den Karteneditor. Daher werden wir den HexMapEditor
gesamten Code entfernen, der mit dem Finden des Pfades verbunden ist.
Nach dem Entfernen dieses Codes ist es nicht mehr sinnvoll, den Editor aktiv zu lassen, wenn wir uns nicht im Bearbeitungsmodus befinden. Daher können wir anstelle eines Modusverfolgungsfelds die Komponente einfach aktivieren oder deaktivieren HexMapEditor
. Außerdem muss sich der Editor jetzt nicht mehr mit UI-Labels befassen.
Da wir uns standardmäßig nicht im Kartenbearbeitungsmodus befinden, deaktivieren wir in Awake den Editor. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); SetEditMode(false); }
Verwenden Sie Raycast, um beim Bearbeiten der Karte nach der aktuellen Zelle unter dem Cursor zu suchen und Einheiten zu verwalten. Vielleicht wird es uns in Zukunft für etwas anderes nützlich sein. Verschieben wir die Raycasting-Logik von HexGrid
einer neuen Methode GetCell
mit einem Strahlparameter. public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; }
HexMapEditor.GetCellUniderCursor
kann diese Methode einfach mit dem Cursorstrahl aufrufen. HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
Spiel-Benutzeroberfläche
Um die Benutzeroberfläche des Spielmodus zu steuern, verwenden wir eine neue Komponente. Während er sich nur mit der Auswahl und Bewegung von Einheiten befasst. Erstellen Sie einen neuen Komponententyp dafür HexGameUI
. Eine Verbindung zum Stromnetz reicht aus, um seine Arbeit zu erledigen. using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; }
Fügen Sie diese Komponente dem neuen Spielobjekt in der UI-Hierarchie hinzu. Er muss kein eigenes Objekt haben, aber es wird uns klar sein, dass es eine separate Benutzeroberfläche für das Spiel gibt.Game UI ObjectFügen Sie eine HexGameUI
Methode SetEditMode
wie in hinzu HexMapEditor
. Die Benutzeroberfläche des Spiels sollte aktiviert sein, wenn wir uns nicht im Bearbeitungsmodus befinden. Außerdem müssen hier Beschriftungen eingefügt werden, da die Benutzeroberfläche des Spiels mit Pfaden arbeitet. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); }
Fügen Sie die UI-Methode des Spiels mit der Ereignisliste des Bearbeitungsmodusschalters hinzu. Dies bedeutet, dass beide Methoden aufgerufen werden, wenn der Spieler den Modus ändert.Mehrere Ereignismethoden.Verfolgen Sie die aktuelle Zelle
Je nach Situation müssen HexGameUI
Sie wissen, welche Zelle sich derzeit unter dem Cursor befindet. Deshalb fügen wir ein Feld hinzu currentCell
. HexCell currentCell;
Erstellen Sie eine Methode UpdateCurrentCell
, die den HexGrid.GetCell
Cursorstrahl verwendet, um dieses Feld zu aktualisieren. void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
Wenn wir die aktuelle Zelle aktualisieren, müssen wir möglicherweise herausfinden, ob sie sich geändert hat. Erzwingen Sie die UpdateCurrentCell
Rückgabe dieser Informationen. bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; }
Einheitenauswahl
Bevor ein Trupp bewegt wird, muss er ausgewählt und verfolgt werden. Fügen Sie daher ein Feld hinzu selectedUnit
. HexUnit selectedUnit;
Wenn wir versuchen, eine Auswahl zu treffen, müssen wir zunächst die aktuelle Zelle aktualisieren. Wenn sich die aktuelle Zelle befindet, wird die Einheit, die diese Zelle belegt, zur ausgewählten Einheit. Befindet sich keine Einheit in der Zelle, wird keine Einheit ausgewählt. Lassen Sie uns eine Methode dafür erstellen DoSelection
. void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
Wir erkennen die Auswahl der Einheiten mit einem einfachen Mausklick. Daher fügen wir eine Methode hinzu Update
, die eine Auswahl trifft, wenn die Maustaste aktiviert ist. Natürlich müssen wir sie nur ausführen, wenn sich der Cursor nicht über dem GUI-Element befindet. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } }
Zu diesem Zeitpunkt haben wir gelernt, wie Sie jeweils eine Einheit per Mausklick auswählen. Wenn Sie auf eine leere Zelle klicken, wird die Auswahl einer Einheit entfernt. Wir erhalten jedoch keine visuelle Bestätigung dafür.Squad-Suche
Wenn eine Einheit ausgewählt ist, können wir ihre Position als Ausgangspunkt für die Suche nach einem Pfad verwenden. Um dies zu aktivieren, benötigen wir keinen weiteren Mausklick. Stattdessen finden und zeigen wir automatisch den Pfad zwischen der Truppposition und der aktuellen Zelle. Wir werden dies immer tun Update
, außer wenn die Wahl getroffen wird. Um dies zu tun, rufen wir die Methode auf, wenn wir eine Ablösung haben DoPathfinding
. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } }
DoPathfinding
Aktualisiert einfach die aktuelle Zelle und ruft an, HexGrid.FindPath
wenn ein Endpunkt vorhanden ist. Wir verwenden wieder eine konstante Geschwindigkeit von 24. void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); }
Bitte beachten Sie, dass wir nicht jedes Mal, wenn wir aktualisieren, einen neuen Pfad finden sollten, sondern nur, wenn sich die aktuelle Zelle ändert. void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } }
Suchen eines Pfads für einen TruppJetzt sehen wir die Pfade, die angezeigt werden, wenn Sie den Cursor nach Auswahl eines Trupps bewegen. Dank dessen ist es offensichtlich, welches Gerät ausgewählt ist. Die Pfade werden jedoch nicht immer korrekt gelöscht. Lassen Sie uns zunächst den alten Pfad löschen, wenn sich der Cursor außerhalb der Karte befindet. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
Dies setzt natürlich voraus, dass es HexGrid.ClearPath
üblich ist, also nehmen wir eine solche Änderung vor. public void ClearPath () { … }
Zweitens werden wir den alten Weg frei machen, wenn wir eine Abteilung wählen. void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
Schließlich werden wir den Pfad löschen, wenn wir den Bearbeitungsmodus ändern. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); }
Suchen Sie nur nach gültigen Endpunkten
Wir können nicht immer den Weg finden, weil es manchmal unmöglich ist, die letzte Zelle zu erreichen. Es ist in Ordnung.
Aber manchmal ist die letzte Zelle selbst nicht akzeptabel. Zum Beispiel haben wir entschieden, dass Pfade keine Unterwasserzellen enthalten dürfen. Dies kann jedoch vom Gerät abhängen. Fügen wir eine HexUnit
Methode hinzu, die uns sagt, ob eine Zelle ein gültiger Endpunkt ist. Unterwasserzellen gibt es nicht. public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater; }
Außerdem haben wir nur eine Einheit in der Zelle stehen lassen. Daher ist die letzte Zelle nicht gültig, wenn sie beschäftigt ist. public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater && !cell.Unit; }
Wir verwenden diese Methode HexGameUI.DoPathfinding
, um ungültige Endpunkte zu ignorieren. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
Zum Endpunkt gehen
Wenn wir einen gültigen Pfad haben, können wir den Trupp zum Endpunkt bewegen. HexGrid
weiß, wann dies möglich ist. Wir lassen diese Informationen in einer neuen schreibgeschützten Eigenschaft weitergeben HasPath
. public bool HasPath { get { return currentPathExists; } }
Fügen Sie der HexGameUI
Methode hinzu, um einen Trupp zu verschieben DoMove
. Diese Methode wird aufgerufen, wenn ein Befehl ausgegeben wird und wenn eine Einheit ausgewählt ist. Daher muss er prüfen, ob es einen Weg gibt, und wenn ja, den Ort der Abteilung ändern. Während wir den Trupp sofort zum Endpunkt teleportieren. In einem der folgenden Tutorials werden wir den Kader dazu bringen, den ganzen Weg zu gehen. void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } }
Verwenden Sie die Maustaste 1 (Rechtsklick), um den Befehl zu senden. Wir werden dies überprüfen, wenn eine Abteilung ausgewählt ist. Wenn die Taste nicht gedrückt wird, suchen wir nach dem Pfad. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } }
Jetzt können wir Einheiten bewegen! Aber manchmal weigern sie sich, einen Weg zu einigen Zellen zu finden. Insbesondere für jene Zellen, in denen sich die Ablösung befand. Dies liegt daran, HexUnit
dass der alte Speicherort beim Festlegen eines neuen nicht aktualisiert wird. Um dies zu beheben, werden wir den Link zum Kader an seinem alten Standort löschen. public HexCell Location { get { return location; } set { if (location) { location.Unit = null; } location = value; value.Unit = this; transform.localPosition = value.Position; } }
Vermeiden Sie Trupps
Das Finden des Weges funktioniert jetzt korrekt und Einheiten können sich auf der Karte teleportieren. Obwohl sie sich nicht in Zellen bewegen können, in denen sich bereits ein Trupp befindet, werden im Weg stehende Abteilungen ignoriert.Einheiten auf dem Weg werden ignoriert.Einheiten derselben Fraktion können sich normalerweise gegenseitig bewegen, aber bisher haben wir keine Fraktionen. Betrachten wir daher alle Einheiten als voneinander getrennt und blockieren die Pfade. Dies kann implementiert werden, indem ausgelastete Zellen übersprungen werden HexGrid.Search
. if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; }
Vermeiden Sie AblösungenunitypackageTeil 19: Bewegungsanimation
- Wir bewegen die Einheiten zwischen den Zellen.
- Visualisieren Sie den zurückgelegten Weg.
- Wir bewegen die Truppen entlang der Kurven.
- Wir zwingen die Truppen, in Bewegungsrichtung zu schauen.
In diesem Teil werden wir Einheiten anstelle von Teleportation zwingen, sich entlang der Gleise zu bewegen.Trupps unterwegsBewegung auf dem Weg
Im vorherigen Teil haben wir Einheiten hinzugefügt und die Möglichkeit, sie zu bewegen. Obwohl wir die Suche nach dem Pfad verwendet haben, um die gültigen Endpunkte zu bestimmen, teleportierten sich die Truppen nach Erteilung des Befehls einfach in die letzte Zelle. Um dem gefundenen Pfad tatsächlich zu folgen, müssen wir diesen Pfad verfolgen und einen Animationsprozess erstellen, der den Trupp zwingt, sich von Zelle zu Zelle zu bewegen. Da es bei den Animationen schwierig ist zu bemerken, wie sich der Trupp bewegt hat, visualisieren wir auch den zurückgelegten Weg mit Hilfe von Gizmos. Aber bevor wir weitermachen, müssen wir den Fehler beheben.Fehler beim Abbiegen
Aufgrund eines Versehens berechnen wir den Kurs, auf dem die Zelle erreicht wird, falsch. Jetzt bestimmen wir den Kurs, indem wir die Gesamtdistanz durch die Geschwindigkeit des Trupps dividierent = d / s und Verwerfen des Restes. Der Fehler tritt auf, wenn Sie zum Betreten der Zelle genau alle verbleibenden Bewegungspunkte pro Bewegung ausgeben müssen. Wenn zum Beispiel jeder Schritt 1 kostet und die Geschwindigkeit 3 beträgt, können wir drei Zellen pro Runde bewegen. Mit vorhandenen Berechnungen können wir jedoch nur zwei Schritte im ersten Schritt ausführen, da für den dritten Schrittt = d / s = 3 / 3 = 1 .
Die summierten Kosten für das Bewegen mit falsch definierten Zügen, Geschwindigkeit 3Für die korrekte Berechnung der Züge müssen wir den Rand einen Schritt von der Anfangszelle entfernen. Wir können dies tun, indem wir den Abstand um 1 verringern, bevor wir die Bewegung berechnen. Dann wird die Bewegung für den dritten Schritt seint = 2 / 3 = 0Richtige BewegungenWir können dies tun, indem wir die Berechnungsformel auf ändernt = ( d - 1 ) / s .
Wir werden diese Änderung an vornehmen HexGrid.Search
. bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; }
Wir ändern auch die Noten der Züge. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … }
Beachten Sie, dass bei diesem Ansatz der anfängliche Zellenpfad -1 ist. Dies ist normal, da wir es nicht anzeigen und der Suchalgorithmus funktionsfähig bleibt.Weg bekommen
Sich auf dem Weg zu bewegen, ist die Aufgabe des Teams. Damit er dies tun kann, muss er den Weg kennen. Wir haben diese Informationen HexGrid
, also fügen wir eine Methode hinzu, um den aktuellen Pfad in Form einer Liste von Zellen zu erhalten. Er kann es aus dem Listenpool nehmen und zurückkehren, wenn es wirklich einen Weg gibt. public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; }
Die Liste wird gefüllt, indem Sie dem Verknüpfungspfad von der letzten zur ersten Zelle folgen, wie dies bei der Visualisierung des Pfads der Fall ist. List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path;
In diesem Fall benötigen wir den gesamten Pfad, der die ursprüngliche Zelle enthält. for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path;
Jetzt haben wir den Pfad in umgekehrter Reihenfolge. Wir können mit ihm arbeiten, aber es wird nicht sehr intuitiv sein. Lassen Sie uns die Liste so umdrehen, dass sie von Anfang bis Ende verläuft. path.Add(currentPathFrom); path.Reverse(); return path;
Bewegungsanfrage
Jetzt können wir die HexUnit
Methode ergänzen und ihm befehlen, dem Pfad zu folgen. Zunächst lassen wir ihn einfach in die letzte Zelle teleportieren. Wir werden die Liste nicht sofort an den Pool zurückgeben, da dies für eine Weile nützlich sein wird. using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … }
Um eine Bewegung anzufordern, ändern wir sie HexGameUI.DoMove
so, dass sie eine neue Methode mit dem aktuellen Pfad aufruft und nicht nur den Standort der Einheit festlegt. void DoMove () { if (grid.HasPath) {
Pfadvisualisierung
Bevor wir mit der Animation des Teams beginnen, überprüfen wir, ob die Pfade korrekt sind. Wir werden dies tun, indem wir HexUnit
befehlen , uns den Pfad zu merken, auf dem es sich bewegen muss, damit es mit Gizmos visualisiert werden kann. List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; }
Fügen Sie eine Methode hinzu OnDrawGizmos
, um den letzten Pfad anzuzeigen (falls vorhanden). Wenn sich das Gerät noch nicht bewegt hat, sollte der Pfad gleich sein null
. Aufgrund der Serialisierung von Unity während der Bearbeitung nach der Neukompilierung im Wiedergabemodus kann es sich jedoch auch um eine leere Liste handeln. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } }
Der einfachste Weg, den Pfad anzuzeigen, besteht darin, für jede Zelle des Pfads eine Gizmo-Kugel zu zeichnen. Eine Kugel mit einem Radius von 2 Einheiten ist für uns geeignet. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } }
Da wir die Pfade für die Ablösung zeigen, können wir gleichzeitig alle ihre letzten Pfade sehen.Gizmos zeigen die zuletzt zurückgelegten Pfade an.Um die Zellverbindungen besser darzustellen , zeichnen Sie mehrere Kugeln in einer Schleife auf einer Linie zwischen der vorherigen und der aktuellen Zelle. Dazu müssen wir den Prozess von der zweiten Zelle aus starten. Kugeln können durch lineare Interpolation mit einem Inkrement von 0,1 Einheiten angeordnet werden, so dass wir zehn Kugeln pro Segment erhalten. for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
Offensichtlichere WegeGleiten Sie den Weg entlang
Sie können dieselbe Methode verwenden, um Einheiten zu verschieben. Lassen Sie uns eine Coroutine dafür erstellen. Anstatt ein Gizmo zu zeichnen, legen wir die Position des Trupps fest. Anstatt zu erhöhen, verwenden wir das Zeitdelta von 0,1 und führen für jede Iteration eine Ausbeute durch. In diesem Fall bewegt sich der Trupp in einer Sekunde von einer Zelle zur nächsten. using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … }
Beginnen wir am Ende der Methode mit Coroutine Travel
. Aber zuerst werden wir alle vorhandenen Coroutinen stoppen. Wir garantieren also, dass zwei Coroutinen nicht gleichzeitig starten, da dies sonst zu sehr seltsamen Ergebnissen führen würde. public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); }
Das Verschieben einer Zelle pro Sekunde ist ziemlich langsam. Der Spieler wird während des Spiels nicht so lange warten wollen. Sie können die Bewegungsgeschwindigkeit des Trupps zu einer Konfigurationsoption machen, aber jetzt verwenden wir eine Konstante. Ich habe ihr einen Wert von 4 Zellen pro Sekunde zugewiesen. Es ist ziemlich schnell, aber lassen Sie uns bemerken, was passiert. const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } }
So wie wir mehrere Pfade gleichzeitig visualisieren können, können wir mehrere Einheiten gleichzeitig fahren lassen. Aus Sicht des Spielzustands ist die Bewegung immer noch Teleportation, die Animationen sind ausschließlich visuell. Einheiten besetzen sofort die letzte Zelle. Sie können sogar Wege finden und einen neuen Zug beginnen, bevor sie eintreffen. In diesem Fall werden sie visuell an den Anfang eines neuen Pfades teleportiert. Dies kann vermieden werden, indem Einheiten oder sogar die gesamte Benutzeroberfläche blockiert werden, während sie sich bewegen. Eine so schnelle Reaktion ist jedoch beim Entwickeln und Testen von Bewegungen sehr praktisch.Einheiten bewegen.Was ist mit dem Höhenunterschied?, . , . , . , . , Endless Legend, , . , .
Position nach der Kompilierung
Einer der Nachteile von Corutin ist, dass sie beim erneuten Kompilieren im Wiedergabemodus nicht „überleben“. Obwohl der Spielstatus immer wahr ist, kann dies dazu führen, dass Trupps irgendwo auf ihrem letzten Pfad stecken bleiben, wenn die Neukompilierung gestartet wird, während sie sich noch bewegen. Um die Konsequenzen abzuschwächen, stellen wir sicher, dass sich die Einheiten nach der Neukompilierung immer in der richtigen Position befinden. Dies kann durch Aktualisieren ihrer Position in erfolgen OnEnable
. void OnEnable () { if (location) { transform.localPosition = location.Position; } }
EinheitspaketReibungslose Bewegung
Die Bewegung von der Mitte zur Mitte der Zelle wirkt zu mechanistisch und führt zu scharfen Richtungsänderungen. Für viele Spiele ist dies normal, aber nicht akzeptabel, wenn Sie zumindest eine leicht realistische Bewegung benötigen. Ändern wir also die Bewegung, damit sie etwas organischer aussieht.Bewegung von Rippe zu Rippe
Der Trupp beginnt seine Reise von der Mitte der Zelle aus. Es geht in die Mitte des Zellenrandes und tritt dann in die nächste Zelle ein. Anstatt sich in Richtung Zentrum zu bewegen, kann er geradewegs zur nächsten Kante gehen, die er überqueren muss. Tatsächlich schneidet das Gerät den Pfad, wenn es die Richtung ändern muss. Dies ist für alle Zellen außer den Endpunkten des Pfades möglich.Drei Möglichkeiten, sich von Kante zu Kante zu bewegenLassen Sie uns die OnDrawGizmos
auf diese Weise erzeugten Pfade anzeigen. Es muss zwischen den Kanten der Zellen interpolieren, was durch Mitteln der Positionen benachbarter Zellen ermittelt werden kann. Es reicht aus, eine Kante pro Iteration zu berechnen und den Wert aus der vorherigen Iteration wiederzuverwenden. Auf diese Weise können wir die Methode für die ursprüngliche Zelle verwenden, aber anstelle der Kante nehmen wir ihre Position ein. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) {
Um die Mitte der Endzelle zu erreichen, müssen wir die Zellenposition als letzten Punkt und nicht als Kante verwenden. Sie können der Schleife eine Überprüfung dieses Falls hinzufügen, aber es ist ein so einfacher Code, dass es offensichtlicher ist, den Code einfach zu duplizieren und leicht zu ändern. void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
RippenbasiertePfade Die resultierenden Pfade ähneln weniger Zickzackpfaden, und der maximale Drehwinkel wird von 120 ° auf 90 ° verringert. Dies kann als Verbesserung angesehen werden. Daher wenden wir dieselben Änderungen in der Coroutine TravelPath
an, um zu sehen, wie sie in der Animation aussehen. IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) {
Bewegen mit wechselnder GeschwindigkeitNach dem Schneiden der Winkel wurde die Länge der Pfadsegmente von der Richtungsänderung abhängig. Aber wir stellen die Geschwindigkeit in Zellen pro Sekunde ein. Infolgedessen ändert sich die Ablösegeschwindigkeit zufällig.Kurven folgen
Sofortige Richtungs- und Geschwindigkeitsänderungen beim Überschreiten von Zellgrenzen sehen hässlich aus. Verwenden Sie besser eine allmähliche Richtungsänderung. Wir können dies unterstützen, indem wir die Truppen zwingen, eher Kurven als geraden Linien zu folgen. Hierfür können Sie Bezier-Kurven verwenden. Insbesondere können wir quadratische Bezier-Kurven nehmen, bei denen der Mittelpunkt der Zellen die mittleren Kontrollpunkte sind. In diesem Fall sind die Tangenten benachbarter Kurven spiegelbildlich zueinander, dh der gesamte Pfad wird zu einer kontinuierlichen glatten Kurve.Kurven von Kante zu KanteErstellen Sie eine Hilfsklasse Bezier
mit einer Methode zum Erhalten von Punkten auf einer quadratischen Bezier-Kurve. Wie im Tutorial Kurven und Splines erläutert , wird hierfür die Formel verwendet(1−t)2A+2(1−t)tB+t2C wo
A ,
B und
C — , t — .
using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } }
GetPoint 0-1?0-1, . . , GetPointClamped
, t
. , GetPointUnclamped
.
OnDrawGizmos
, , . — , ,
i - 1
, 1. ,
Vector3.Lerp
Bezier.GetPoint
.
.
void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } }
Mit Bezier-Kurven erstellte Pfade Ein gekrümmterPfad sieht viel besser aus. Wir wenden die gleichen Änderungen an TravelPath
und sehen, wie die Einheiten mit diesem Ansatz animiert werden. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
Wir bewegen uns entlang der Kurven. DieAnimation wurde auch dann flüssig, wenn die Geschwindigkeit der Ablösung instabil ist. Da die Tangenten der Kurve der benachbarten Segmente zusammenfallen, ist die Geschwindigkeit kontinuierlich. Die Geschwindigkeitsänderung erfolgt allmählich und tritt auf, wenn eine Ablösung die Zelle passiert, und verlangsamt sich beim Richtungswechsel. Wenn er geradeaus fährt, bleibt die Geschwindigkeit konstant. Außerdem beginnt und endet der Trupp mit null Geschwindigkeit. Dies ahmt die natürliche Bewegung nach, also lass es so.Zeiterfassung
Bis zu diesem Punkt haben wir begonnen, über jedes der Segmente von 0 zu iterieren, bis wir 1 erreicht haben. Dies funktioniert gut, wenn wir um einen konstanten Wert erhöhen, aber unsere Iteration hängt vom Zeitdelta ab. Wenn die Iteration über ein Segment abgeschlossen ist, werden wir wahrscheinlich 1 um einen gewissen Betrag überschreiten, abhängig vom Delta der Zeit. Dies ist bei hohen Bildraten unsichtbar, kann jedoch bei niedrigen Bildraten zu Ruckeln führen.Um Zeitverlust zu vermeiden, müssen wir die verbleibende Zeit von einem Segment zum nächsten übertragen. Dies kann durch Verfolgen t
des gesamten Pfads und nicht nur in jedem Segment erfolgen. Am Ende jedes Segments subtrahieren wir dann 1 davon. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
Wenn wir dies bereits tun, stellen wir sicher, dass das Zeitdelta am Anfang des Pfads berücksichtigt wird. Dies bedeutet, dass wir uns sofort bewegen und nicht für einen Frame untätig bleiben. float t = Time.deltaTime * travelSpeed;
Außerdem beenden wir nicht genau zu dem Zeitpunkt, an dem der Pfad enden soll, sondern kurz zuvor. Hier kann der Unterschied auch von der Bildrate abhängen. Lassen Sie den Trupp daher den Pfad genau am Endpunkt vervollständigen. IEnumerator TravelPath () { … transform.localPosition = location.Position; }
EinheitspaketOrientierungsanimation
Die Einheiten begannen sich entlang einer glatten Kurve zu bewegen, änderten jedoch nicht die Ausrichtung entsprechend der Bewegungsrichtung. Infolgedessen scheinen sie zu gleiten. Damit die Bewegung wie eine echte Bewegung aussieht, müssen wir sie drehen.Ich freue mich darauf
Wie im Tutorial "Kurven und Splines" können wir die Ableitung der Kurve verwenden, um die Ausrichtung der Einheit zu bestimmen. Die Formel für die Ableitung einer quadratischen Bezier-Kurve:2 ( ( 1 - t ) ( B - A ) + t ( C - B ) ) .
Fügen Sie der Bezier
Berechnungsmethode hinzu. public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); }
Der Ableitungsvektor befindet sich auf einer geraden Linie mit der Bewegungsrichtung. Wir können die Methode verwenden Quaternion.LookRotation
, um sie in eine Squad-Runde umzuwandeln. Wir werden es bei jedem Schritt durchführen HexUnit.TravelPath
. transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null;
Gibt es am Anfang des Pfades keinen Fehler?, . A und B , . , t=0 , , Quaternion.LookRotation
. , , t=0 . . , t>0 .
, t<1 .
Im Gegensatz zur Position der Ablösung ist die Nichtidealität ihrer Ausrichtung am Ende des Pfades nicht wichtig. Wir müssen jedoch sicherstellen, dass seine Ausrichtung der endgültigen Drehung entspricht. Dazu setzen wir nach Abschluss seine Ausrichtung mit seiner Drehung in Y gleich. transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y;
Jetzt schauen die Einheiten genau in die Bewegungsrichtung, sowohl horizontal als auch vertikal. Dies bedeutet, dass sie sich vorwärts und rückwärts lehnen, von den Hängen absteigen und sie erklimmen. Um sicherzustellen, dass sie immer gerade stehen, zwingen wir die Komponente Y des Richtungsvektors auf Null, bevor wir sie zur Bestimmung der Drehung der Einheit verwenden. Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d);
Ich freue mich darauf, mich zu bewegenWir schauen uns den Punkt an
Während des gesamten Weges blicken die Einheiten nach vorne, aber bevor sie sich bewegen, können sie in die andere Richtung schauen. In diesem Fall ändern sie sofort ihre Ausrichtung. Es ist besser, wenn sie sich vor Beginn der Bewegung in Richtung des Pfades drehen.Ein Blick in die richtige Richtung kann in anderen Situationen hilfreich sein. Erstellen wir also eine Methode LookAt
, mit der der Trupp gezwungen wird, die Ausrichtung zu ändern, um einen bestimmten Punkt zu betrachten. Die erforderliche Drehung kann mithilfe der Methode eingestellt werden Transform.LookAt
, indem zunächst der Punkt in derselben vertikalen Position wie die Ablösung platziert wird. Danach können wir die Ausrichtung des Teams abrufen. void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
Damit sich die Ablösung tatsächlich dreht, verwandeln wir die Methode in ein anderes Corutin, das sie mit konstanter Geschwindigkeit dreht. Die Drehgeschwindigkeit kann ebenfalls angepasst werden, aber wir werden die Konstante wieder verwenden. Die Drehung sollte schnell sein, ungefähr 180 ° pro Sekunde. const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … }
Es ist nicht notwendig, an der Beschleunigung der Kurve zu basteln, da dies nicht wahrnehmbar ist. Es wird für uns ausreichen, einfach zwischen den beiden Orientierungen zu interpolieren. Leider ist dies nicht so einfach wie bei zwei Zahlen, da die Winkel kreisförmig sind. Zum Beispiel sollte ein Übergang von 350 ° zu 10 ° zu einer Drehung um 20 ° im Uhrzeigersinn führen, aber eine einfache Interpolation erzwingt eine Drehung um 340 ° gegen den Uhrzeigersinn.Der einfachste Weg, eine korrekte Rotation zu erstellen, besteht darin, zwischen zwei Quaternionen mithilfe der sphärischen Interpolation zu interpolieren. Dies führt zur kürzesten Kurve. Dazu erhalten wir die Quaternionen von Anfang und Ende und machen dann mit einen Übergang zwischen ihnen Quaternion.Slerp
. IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
Dies funktioniert, aber die Interpolation geht unabhängig vom Drehwinkel immer von 0 auf 1. Um eine gleichmäßige Winkelgeschwindigkeit zu gewährleisten, müssen wir die Interpolation mit zunehmendem Drehwinkel verlangsamen. Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; }
Wenn wir den Winkel kennen, können wir die Kurve vollständig überspringen, wenn sich herausstellt, dass sie Null ist. float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } }
Jetzt können wir die Rotation der Einheit hinzufügen, TravelPath
indem wir einfach die Ausbeute ausführen, bevor wir die LookAt
Position der zweiten Zelle verschieben. Unity startet Coroutine automatisch LookAt
und TravelPath
wartet auf den Abschluss. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … }
Wenn Sie den Code überprüfen, teleportiert sich der Trupp in die letzte Zelle, dreht sich dort um und teleportiert sich dann zurück zum Anfang des Pfades und beginnt, sich von dort aus zu bewegen. Dies geschieht, weil wir einer Eigenschaft Location
vor Beginn der Coroutine einen Wert zuweisen TravelPath
. Um die Teleportation loszuwerden, können wir zu Beginn TravelPath
die Position der Abteilung in die ursprüngliche Zelle zurückbringen. Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position);
Vor dem Bewegen drehenFegen
Nachdem wir die Bewegung erhalten haben, die wir brauchen, können wir die Methode loswerden OnDrawGizmos
. Löschen Sie es oder kommentieren Sie es aus, falls wir in Zukunft Pfade sehen müssen.
Da wir uns nicht mehr merken müssen, in welche Richtung wir uns bewegt haben, TravelPath
können Sie am Ende die Liste der Zellen freigeben. IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; }
Was ist mit echten Squad-Animationen?, . 3D- . . , . Mecanim, TravelPath
.
Einheitspaket