Karte
In einem
früheren Artikel habe ich mir angesehen, was das neue
Job-System ist , wie es funktioniert, wie Aufgaben erstellt, mit Daten gefüllt und Multithread-Berechnungen durchgeführt werden, und nur kurz erklärt, wo Sie dieses System verwenden können. In diesem Artikel werde ich versuchen, ein bestimmtes Beispiel zu analysieren, wo Sie dieses System verwenden können, um mehr Leistung zu erzielen.
Da das System ursprünglich mit dem Ziel entwickelt wurde, mit Daten zu arbeiten, eignet es sich hervorragend zur Lösung von Pfadfindungsaufgaben.
Unity verfügt bereits über einen guten
NavMesh-Pfadfinder , funktioniert jedoch nicht in 2D-Projekten, obwohl es viele vorgefertigte Lösungen für dasselbe
Asset gibt . Nun, und wir werden versuchen, nicht nur ein System zu erstellen, das nach Wegen auf der erstellten Karte sucht, sondern diese Karte auch dynamisch macht, so dass das System jedes Mal, wenn sich etwas daran ändert, eine neue Karte erstellt, und all dies werden wir natürlich mit berechnen ein neues System von Aufgaben, um den Haupt-Thread nicht zu laden.
Beispiel für einen Systembetrieb Im Beispiel wird ein Raster auf der Karte erstellt, es gibt einen Bot und ein Hindernis. Das Raster wird jedes Mal neu erstellt, wenn wir Eigenschaften der Karte ändern, unabhängig von ihrer Größe oder Position.
Für Flugzeuge habe ich einen einfachen
SpriteRenderer verwendet . Diese Komponente verfügt über eine hervorragende
Grenzeigenschaft , mit der Sie die Größe der Karte leicht ermitteln können.
Das ist im Grunde alles für den Anfang, aber wir werden nicht aufhören und sofort zur Sache kommen.
Beginnen wir mit den Skripten. Und das erste ist das
Hindernishindernis- Skript.
Hindernispublic class Obstacle : MonoBehaviour { }
Innerhalb der
Hindernisklasse erfassen wir alle Änderungen an den Hindernissen auf der Karte, z. B. das Ändern der Position oder Größe eines Objekts.
Als Nächstes können Sie die
Map- Map-Klasse erstellen, auf der das Raster erstellt wird, und es von der
Obstacle- Klasse erben.
Karte public sealed class Map : Obstacle { }
Die
Map- Klasse verfolgt auch alle Änderungen auf der Map, um das Raster bei Bedarf neu zu erstellen.
Füllen Sie dazu die
Hindernis- Basisklasse mit allen erforderlichen Variablen und Methoden, um
Objektänderungen zu verfolgen.
Hindernis public class Obstacle : MonoBehaviour { public new SpriteRenderer renderer { get; private set;} private Vector2 tempSize; private Vector2 tempPos; protected virtual void Awake() { this.renderer = GetComponent<SpriteRenderer>(); this.tempSize = this.size; this.tempPos = this.position; } public virtual bool CheckChanges() { Vector2 newSize = this.size; float diff = (newSize - this.tempSize).sqrMagnitude; if (diff > 0.01f) { this.tempSize = newSize; return true; } Vector2 newPos = this.position; diff = (newPos - this.tempPos).sqrMagnitude; if (diff > 0.01f) { this.tempPos = newPos; return true; } return false; } public Vector2 size { get { return this.renderer.bounds.size;} } public Vector2 position { get { return this.transform.position;} } }
Hier hat die
Renderer- Variable einen Verweis auf die
SpriteRenderer- Komponente, und die
Variablen tempSize und
tempPos werden verwendet, um Änderungen in der Größe und Position des Objekts zu verfolgen.
Die virtuelle
Awake- Methode wird zum Initialisieren der Variablen verwendet, und die virtuelle
CheckChanges- Methode verfolgt die aktuellen Änderungen der Größe und Position des Objekts und gibt ein
boolesches Ergebnis zurück.
Lassen Sie uns zunächst das
Hindernis- Skript und fahren Sie mit dem
Map- Map-Skript selbst fort, in dem wir es auch mit den für die Arbeit erforderlichen Parametern füllen.
Karte public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); }
Die Variable
nodeSize gibt die Größe der Zellen auf der Karte an. Hier habe ich ihre Größe von 0,1 auf 1 begrenzt, damit die Zellen im Raster nicht zu klein, sondern auch zu groß sind. Die
Versatzvariable wird verwendet, um die Karte beim Erstellen des Rasters einzurücken, sodass das Raster nicht entlang der Kanten der Karte erstellt wird.
Da es jetzt zwei neue Variablen auf der Karte gibt, stellt sich heraus, dass ihre Änderungen ebenfalls verfolgt werden müssen.
Fügen Sie dazu einige Variablen hinzu und überladen Sie die
CheckChanges- Methode in der
Map- Klasse.
Karte public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); private float tempNodeSize; private Vector2 tempOffset; protected override void Awake() { base.Awake(); this.tempNodeSize = this.nodeSize; this.tempOffset = this.offset; } public override bool CheckChanges() { float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize); if (diff > 0.01f) { this.tempNodeSize = this.nodeSize; return true; } diff = (this.tempOffset - this.offset).sqrMagnitude; if (diff > 0.01f) { this.tempOffset = this.offset; return true; } return base.CheckChanges(); } }
Fertig. Jetzt können Sie ein Kartensprite auf der Bühne erstellen und ein Kartenskript darauf werfen.

Wir machen dasselbe mit einem Hindernis - erstellen Sie ein einfaches Sprite auf der Bühne und werfen Sie das
Hindernis- Skript darauf.

Jetzt haben wir Kartenobjekte und Hindernisse auf der Bühne.
Das Kartenskript ist für die Verfolgung aller Änderungen auf der Karte verantwortlich. In der
Aktualisierungsmethode überprüfen wir jeden Frame auf Änderungen.
Karte public sealed class Map : Obstacle { private bool requireRebuild; private void Update() { UpdateChanges(); } private void UpdateChanges() { if (this.requireRebuild) { print(“ , !”); this.requireRebuild = false; } else { this.requireRebuild = CheckChanges(); } } }
Daher
verfolgt die Karte in der
UpdateChanges- Methode nur die
bisherigen Änderungen. Sie können das Spiel jetzt sogar starten und versuchen, die Größe der Karte oder den
Versatz zu ändern, um sicherzustellen, dass alle Änderungen nachverfolgt werden.
Jetzt müssen Sie die Änderungen der Hindernisse selbst auf der Karte irgendwie verfolgen. Dazu setzen wir jedes Hindernis in eine Liste auf der Karte, die wiederum jeden Frame in der
Update- Methode aktualisiert.
Erstellen Sie in der
Map- Klasse eine Liste aller möglichen Hindernisse auf der Map und einige statische Methoden, um sie zu registrieren.
Karte public sealed class Map : Obstacle { private static Map ObjInstance; private List<Obstacle> obstacles = new List<Obstacle>(); public static bool RegisterObstacle(Obstacle obstacle) { if (obstacle == Instance) return false; else if (Instance.obstacles.Contains(obstacle) == false) { Instance.obstacles.Add(obstacle); Instance.requireRebuild = true; return true; } return false; } public static bool UnregisterObstacle(Obstacle obstacle) { if (Instance.obstacles.Remove(obstacle)) { Instance.requireRebuild = true; return true; } return false; } public static Map Instance { get { if (ObjInstance == null) ObjInstance = FindObjectOfType<Map>(); return ObjInstance; } } }
Bei der statischen
RegisterObstacle- Methode registrieren wir ein neues Hindernishindernis auf der Karte und fügen es der Liste hinzu. Zunächst ist jedoch zu berücksichtigen, dass die Karte selbst auch von der
Hindernisklasse geerbt wird. Daher sollten wir prüfen, ob wir versuchen, die Karte selbst als Hindernis zu registrieren.
Die statische
UnregisterObstacle- Methode beseitigt im Gegenteil das Hindernis aus der Karte und entfernt es aus der Liste, wenn wir zulassen, dass es zerstört wird.
Gleichzeitig muss jedes Mal, wenn wir ein Hindernis zur Karte hinzufügen oder daraus entfernen, die Karte selbst neu erstellt werden. Setzen
Sie daher nach Ausführung dieser statischen Methoden die Variable
requireRebuild auf
true .
Um von jedem Skript aus problemlos auf das
Map- Skript zugreifen zu können, habe ich eine statische
Instanzeigenschaft erstellt , die mir genau diese Instanz der
Map zurückgibt .
Kehren wir nun zum
Hindernis- Skript zurück, in dem wir ein Hindernis auf der Karte registrieren.
Fügen Sie dazu einige
OnEnable- und
OnDisable- Methoden hinzu.
Hindernis public class Obstacle : MonoBehaviour { protected virtual void OnEnable() { Map.RegisterObstacle(this); } protected virtual void OnDisable() { Map.UnregisterObstacle(this); } }
Jedes Mal, wenn wir beim Spielen auf der Karte ein neues Hindernis erstellen, wird es automatisch in der
OnEnable- Methode registriert, wobei es beim
Erstellen eines neuen Rasters berücksichtigt wird und wir uns in der
OnDisable- Methode von der Karte
entfernen , wenn es zerstört oder deaktiviert wird.
Es bleibt nur die Verfolgung der Änderungen der Hindernisse selbst im
Map- Skript in der überladenen
CheckChanges- Methode.
Karte public sealed class Map : Obstacle { public override bool CheckChanges() { float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize); if (diff > 0.01f) { this.tempNodeSize = this.nodeSize; return true; } diff = (this.tempOffset - this.offset).sqrMagnitude; if (diff > 0.01f) { this.tempOffset = this.offset; return true; } foreach(Obstacle obstacle in this.obstacles) { if (obstacle.CheckChanges()) return true; } return base.CheckChanges(); } }
Jetzt haben wir eine Karte, Hindernisse - im Allgemeinen alles, was Sie zum Aufbau eines Gitters benötigen, und jetzt können Sie zum Wichtigsten übergehen.
Vernetzung
Das Gitter ist in seiner einfachsten Form eine zweidimensionale Anordnung von Punkten. Um es zu erstellen, müssen Sie die Größe der Karte und die Größe der Punkte darauf kennen. Nach einigen Berechnungen erhalten wir die Anzahl der Punkte horizontal und vertikal. Dies ist unser Raster.
Es gibt viele Möglichkeiten, einen Pfad in einem Raster zu finden. In diesem Artikel geht es jedoch hauptsächlich darum, zu verstehen, wie die Funktionen des Task-Systems korrekt verwendet werden. Daher werde ich hier nicht auf verschiedene Optionen zum Auffinden des Pfads sowie auf deren Vor- und Nachteile eingehen, sondern auf die einfachste Suchoption
A * .
In diesem Fall sollten alle Punkte im Raster zusätzlich zur Position die Koordinaten- und Durchgängigkeitseigenschaft aufweisen.
Mit der Durchgängigkeit denke ich, dass alles klar ist, warum es benötigt wird, aber die Koordinaten geben die Reihenfolge des Punktes auf dem Gitter an. Diese Koordinaten sind nicht spezifisch an die Position des Punktes im Raum gebunden. Das Bild unten zeigt ein einfaches Raster, das die Unterschiede der Koordinaten von einer Position zeigt.
Warum die Koordinaten?Tatsache ist, dass in der Einheit ein einfaches
Float verwendet wird , das sehr ungenau ist und eine gebrochene oder negative Zahl sein kann, um die Position eines Objekts im Raum anzuzeigen.
Daher ist es schwierig, damit eine Pfadsuche auf der Karte zu implementieren. Die Koordinaten werden in Form eines klaren
Int erstellt, das immer positiv ist und mit dem bei der Suche nach benachbarten Punkten viel einfacher gearbeitet werden kann.
Definieren wir zunächst ein
Punktobjekt . Dies ist eine einfache
Knotenstruktur .
Knoten public struct Node { public int id; public Vector2 position; public Vector2Int coords; }
Diese Struktur enthält die Positionsposition in Form von
Vector2 , wobei wir mit dieser Variablen einen Punkt im Raum zeichnen. Die Koordinatenkoordinatenvariable in Form von
Vector2Int gibt die Koordinaten eines Punkts auf der Karte an, und die
ID- Variable, ihre numerische Kontonummer, vergleicht verschiedene Punkte im Raster und prüft, ob ein Punkt vorhanden ist.
Die Durchgängigkeit des Punktes wird in Form seiner
booleschen Eigenschaft angegeben. Da wir die
konvertierbaren Datentypen im Task-System jedoch nicht verwenden können, geben wir seine Durchgängigkeit in Form einer
int- Zahl an. Dazu habe ich eine einfache Aufzählung
NodeType verwendet , wobei: 0 kein passierbarer Punkt ist. und 1 ist passabel.
NodeType und Node public enum NodeType { NonWalkable = 0, Walkable = 1 } public struct Node { public int id; public Vector2 position; public Vector2Int coords; private int nodeType; public bool isWalkable { get { return this.nodeType == (int)NodeType.Walkable;} } public Node(int id, Vector2 position, Vector2Int coords, NodeType type) { this.id = id; this.position = position; this.coords = coords; this.nodeType = (int)type; } }
Um die Arbeit mit einem Punkt zu vereinfachen, werde ich die
Equals- Methode überladen, um den Vergleich von Punkten zu vereinfachen und die Überprüfungsmethode für die Existenz eines Punkts zu ergänzen.
Knoten public struct Node { public override bool Equals(object obj) { if (obj is Node) { Node other = (Node)obj; return this.id == other.id; } else return base.Equals(obj); } public static implicit operator bool(Node node) { return node.id > 0; } }
Da die
ID- Nummer des Punkts im Raster mit 1 Einheit beginnt, überprüfe ich die Existenz des Punkts als Bedingung, dass seine
ID größer als 0 ist.
Gehen Sie zur
Map- Klasse, wo wir alles für die Erstellung einer Map vorbereiten.
Wir haben bereits eine Überprüfung für die Änderung der Parameter der Karte, jetzt müssen wir bestimmen, wie der Prozess des Aufbaus des Gitters ausgeführt wird. Erstellen Sie dazu eine neue Variable und mehrere Methoden.
Karte public sealed class Map : Obstacle { public bool rebuilding { get; private set; } public void Rebuild() {} private void OnRebuildStart() {} private void OnRebuildFinish() {} }
Die
Neuerstellungseigenschaft gibt an, ob der
Vernetzungsprozess ausgeführt wird. Die
Rebuild- Methode sammelt Daten und Aufgaben zum
Erstellen des Rasters, dann
startet die
OnRebuildStart- Methode den Grid-
Erstellungsprozess und die
OnRebuildFinish- Methode sammelt Daten aus den Aufgaben.
Lassen Sie uns nun die
UpdateChanges- Methode ein
wenig ändern, damit die
Rasterbedingung berücksichtigt wird.
Karte public sealed class Map : Obstacle { public bool rebuilding { get; private set; } private void UpdateChanges() { if (this.rebuilding) { print(“ ...”); } else { if (this.requireRebuild) { print(“ , !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } } public void Rebuild() { if (this.rebuilding) return; print(“ !”); OnRebuildStart(); } private void OnRebuildStart() { this.rebuilding = true; } private void OnRebuildFinish() { this.rebuilding = false; } }
Wie Sie jetzt in der
UpdateChanges- Methode sehen können,
gibt es eine Bedingung, dass beim
Erstellen des alten Netzes nicht mit dem
Erstellen eines neuen Netzes begonnen wird. Auch bei der
Rebuild- Methode prüft die erste Aktion, ob der Vernetzungsprozess bereits ausgeführt wird.
Problemlösung
Nun ein wenig über den Prozess des Erstellens einer Karte.
Da wir das Task-System verwenden und das Raster parallel erstellen, um die Map zu erstellen, habe ich den Typ der
IJobParallelFor- Task verwendet, der eine bestimmte Anzahl von Malen ausgeführt wird. Um den Konstruktionsprozess nicht mit einer separaten Aufgabe zu laden, verwenden wir den in einem
JobHandle gepackten Aufgabenpool.
Verwenden Sie zum Erstellen eines Rasters meistens zwei ineinander verschachtelte Zyklen, um beispielsweise horizontal und vertikal zu erstellen. In diesem Beispiel wird das Raster auch zuerst horizontal und dann vertikal erstellt. Dazu berechnen wir die Anzahl der horizontalen und vertikalen Punkte in der
Rebuild- Methode. In der
Rebuild- Methode durchlaufen wir den Zyklus entlang der vertikalen Punkte und erstellen in der Aufgabe parallel horizontale Punkte. Schauen Sie sich die Animation unten an, um sich den Bauprozess besser vorzustellen.
Die Anzahl der vertikalen Punkte gibt die Anzahl der Aufgaben an. Jede Aufgabe erstellt Punkte nur horizontal. Nach Abschluss aller Aufgaben werden die Punkte in einer Liste zusammengefasst. Aus diesem Grund muss ich eine Aufgabe wie
IJobParallelFor verwenden , um den Index des Punkts im Raster horizontal an die
Execute- Methode zu übergeben.
Und so haben wir die Punktstruktur. Jetzt können Sie die Struktur der
Jobaufgabe erstellen und von der
IJobParallelFor- Schnittstelle erben.
Hier ist alles einfach.
Job public struct Job : IJobParallelFor { public void Execute(int index) {} }
Wir kehren zur
Rebuild- Methode der
Map- Klasse zurück, in der wir die erforderlichen Berechnungen für die Gittermessung durchführen.
Karte public sealed class Map : Obstacle { public void Rebuild() { if (this.rebuilding) return; print(“ !”); Vector2 mapSize = this.size - this.offset * 2f; int horizontals = Mathf.RoundToInt(mapSize.x / this.nodeSize); int verticals = Mathf.RoundToInt(mapSize.y / this.nodeSize); if (horizontals <= 0) { OnRebuildFinish(); return; } Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); OnRebuildStart(); } }
Bei der
Rebuild- Methode berechnen wir die genaue Größe der
mapSize- Karte unter Berücksichtigung der Einrückung. In
Vertikalen schreiben wir dann die Anzahl der Punkte vertikal und in
Horizontalen die Anzahl der Punkte horizontal. Wenn die Anzahl der vertikalen Punkte 0 ist, beenden wir die
Erstellung der Karte und rufen die
OnRebuildFinish- Methode auf, um den Vorgang abzuschließen. Die
Ursprungsvariable gibt den Ort an, von dem aus wir mit dem Erstellen des Rasters beginnen. Im Beispiel ist dies der untere linke Punkt auf der Karte.
Jetzt können Sie zu den Aufgaben selbst gehen und sie mit Daten füllen.
Während des Aufbaus des Rasters benötigt die Aufgabe ein
NativeArray- Array, in dem wir die Punkte platzieren. Da wir Hindernisse auf der Karte haben, müssen wir sie auch an die Aufgabe übergeben. Dazu verwenden wir ein anderes
NativeArray- Array. Dann benötigen wir die Größe der Punkte im Problem , die Anfangsposition, von der aus wir die Punkte erstellen, sowie die Anfangskoordinaten der Reihe.
Job public struct Job : IJobParallelFor { [WriteOnly] public NativeArray<Node> array; [ReadOnly] public NativeArray<Rect> bounds; public float nodeSize; public Vector2 startPos; public Vector2Int startCoords; public void Execute(int index) {} }
Ich habe das Array von Punkten mit dem Attribut
WriteOnly markiert, da in der Aufgabe nur die empfangenen Punkte in das Array "
geschrieben " werden müssen. Im Gegensatz dazu wird das Array der Hindernisgrenzen mit dem Attribut
ReadOnly markiert, da in der Aufgabe nur Daten aus diesem Array "
gelesen " werden.
Lassen Sie uns zunächst mit der Berechnung der Punkte selbst fortfahren.
Nun zurück zur
Map- Klasse, in der wir alle an den Aufgaben beteiligten Variablen bezeichnen.
Hier benötigen wir zunächst ein globales
Handle von Aufgaben, eine Reihe von Hindernissen in Form von
NativeArray , eine Liste von Aufgaben, die alle im Raster und im
Wörterbuch empfangenen Punkte mit allen Koordinaten und Punkten auf der Karte enthalten, damit es später bequemer ist, nach ihnen zu suchen.
Karte public sealed class Map : Obstacle { private JobHandle handle; private NativeArray<Rect> bounds; private HashSet<NativeArray<Node>> jobs = new HashSet<NativeArray<Node>>(); private Dictionary<Vector2Int, Node> nodes = new Dictionary<Vector2Int, Node>(); }
Nun kehren wir wieder zur
Rebuild- Methode zurück und bauen das Grid weiter auf.
Initialisieren Sie zunächst das
Begrenzungsarray von Hindernissen, um es an die Aufgabe weiterzuleiten.
Wiederaufbauen public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); } OnRebuildStart(); }
Hier erstellen wir eine Instanz von
NativeArray über einen neuen Konstruktor mit drei Parametern. Ich habe die ersten beiden Parameter in einem früheren Artikel untersucht, aber der dritte Parameter hilft uns, ein wenig Zeit beim Erstellen eines Arrays zu sparen. Tatsache ist, dass wir Daten unmittelbar nach ihrer Erstellung in das Array schreiben, was bedeutet, dass wir nicht sicherstellen müssen, dass es gelöscht wird. Dieser Parameter ist nützlich für
NativeArray, das in der Task nur im Lesemodus verwendet wird.
Und so füllen wir das
Bounds- Array mit Daten.
Wiederaufbauen public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } OnRebuildStart(); }
Jetzt können wir mit dem Erstellen von Aufgaben fortfahren. Dazu durchlaufen wir einen Zyklus durch alle vertikalen Zeilen des Rasters.
Wiederaufbauen public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; } OnRebuildStart(); }
Zunächst
erhalten wir in
xPos und
yPos die anfängliche horizontale Position der Serie.
Wiederaufbauen public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; NativeArray<Node> array = new NativeArray<Node>(horizontals, Allocator.Persistent); Job job = new Job(); job.startCoords = new Vector2Int(i * horizontals, i); job.startPos = new Vector2(xPos, yPos); job.nodeSize = this.nodeSize; job.bounds = this.bounds; job.array = array; } OnRebuildStart(); }
Als Nächstes erstellen wir ein einfaches
NativeArray- Array, in dem die Punkte in der Aufgabe platziert werden. Hier müssen Sie für das Array-
Array angeben, wie viele Punkte horizontal erstellt werden und welche Art der Zuordnung
dauerhaft ist , da die Aufgabe länger als ein Frame dauern kann.
Erstellen Sie anschließend die
Job- Task-Instanz selbst, setzen Sie die Anfangskoordinaten der
startCoords- Reihe, die Anfangsposition der
startPos- Reihe, die Größe der
nodeSize- Punkte, das
Begrenzungsarray von Hindernissen und das Array von Punkten selbst am Ende.
Es bleibt nur die Aufgabe in
Handle und die globale Aufgabenliste zu setzen.
Wiederaufbauen public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; NativeArray<Node> array = new NativeArray<Node>(horizontals, Allocator.Persistent); Job job = new Job(); job.startCoords = new Vector2Int(i * horizontals, i); job.startPos = new Vector2(xPos, yPos); job.nodeSize = this.nodeSize; job.bounds = this.bounds; job.array = array; this.handle = job.Schedule(horizontals, 3, this.handle); this.jobs.Add(array); } OnRebuildStart(); }
Fertig. Wir haben eine Liste von Aufgaben und deren allgemeinem
Handle . Jetzt können wir dieses
Handle ausführen
, indem wir die
Complete- Methode in der
OnRebuildStart- Methode
aufrufen .
Onrebuildstart private void OnRebuildStart() { this.rebuilding = true; this.handle.Complete(); }
Da die
Wiederherstellungsvariable anzeigt, dass der
Vernetzungsprozess ausgeführt wird, muss die
UpdateChanges- Methode
selbst auch die Bedingung angeben, wann dieser Prozess unter Verwendung des
Handles und seiner
IsCompleted- Eigenschaft beendet wird.
Update-Änderungen private void UpdateChanges() { if (this.rebuilding) { print(“ ...”); if (this.handle.IsCompleted) OnRebuildFinish(); } else { if (this.requireRebuild) { print(“ , !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } }
Nach Abschluss der Aufgaben wird die
OnRebuildFinish- Methode aufgerufen, bei der die empfangenen Punkte bereits in einer allgemeinen
Wörterbuchliste gesammelt werden und vor allem die belegten Ressourcen
gelöscht werden.
OnRebuildFinish private void OnRebuildFinish() { this.nodes.Clear(); foreach (NativeArray<Node> array in this.jobs) { foreach (Node node in array) this.nodes.Add(node.coords, node); array.Dispose(); } this.jobs.Clear(); if (this.bounds.IsCreated) this.bounds.Dispose(); this.requireRebuild = this.rebuilding = false; }
Zuerst löschen wir das Knotenwörterbuch von den vorherigen Punkten, sortieren dann mithilfe der foreach-Schleife alle Punkte, die wir von den Aufgaben erhalten haben, und fügen sie in das Knotenwörterbuch ein , wobei der Schlüssel die Koordinaten ( NICHT die Position !) Des Punkts sind und der Wert der Punkt selbst ist. Mit Hilfe dieses Wörterbuchs können wir leichter nach benachbarten Punkten auf der Karte suchen. Nach dem Füllen löschen wir das Array- Array mit der Dispose- Methode und am Ende löschen wir die Job- Aufgabenliste selbst .Sie müssen auch die Grenzen der Hindernisse beseitigen, wenn diese zuvor erstellt wurden.Nach all diesen Aktionen erhalten wir eine Liste aller Punkte auf der Karte und jetzt können Sie sie auf der Bühne zeichnen.Erstellen Sie dazu in der Map- Klasse die OnDrawGizmos- Methode , in der die Punkte gezeichnet werden .Karte public sealed class Map : Obstacle { #if UNITY_EDITOR private void OnDrawGizmos() {} #endif }
Nun zeichnen wir durch die Schleife jeden Punkt.Karte public sealed class Map : Obstacle { #if UNITY_EDITOR private void OnDrawGizmos() { foreach (Node node in this.nodes.Values) { Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f); } } #endif }
Nach all diesen Aktionen sieht unsere Karte irgendwie langweilig aus. Um wirklich ein Raster zu erhalten, müssen die Punkte miteinander verbunden sein.Für die Suche nach benachbarten Punkten müssen wir nur noch den gewünschten Punkt durch seine Koordinaten in 8 Richtungen finden, so dass die Klasse Karte eine einfache statische Array Richtungen eingeleitet Wegbeschreibung und Zellsuchverfahren für seine Koordinaten GetNode .Karte public sealed class Map : Obstacle { public static readonly Vector2Int[] Directions = { Vector2Int.up, new Vector2Int(1, 1), Vector2Int.right, new Vector2Int(1, -1), Vector2Int.down, new Vector2Int(-1, -1), Vector2Int.left, new Vector2Int(-1, 1), }; public Node GetNode(Vector2Int coords) { Node result = default(Node); try { result = this.nodes[coords]; } catch {} return result; } #if UNITY_EDITOR private void OnDrawGizmos() {} #endif }
Verfahren GetNode kehrt Punkt zu den Koordinaten der Liste der Knoten , aber es sollte , weil sorgfältig durchgeführt werden , wenn die Koordinaten Vector2Int falscher Fehler auftritt, werden also hier verwenden wir die Gruppenfrei Bypass try catch - Anweisung , die Bypass - Ausnahme wird und nicht „ hängen “ die gesamte Anwendung zum Scheitern verurteilt.Als nächstes durchlaufen wir einen Zyklus in alle Richtungen und versuchen, benachbarte Punkte in der OnDrawGizmos- Methode zu finden. Vergessen Sie vor allem nicht, die Durchgängigkeit des Punkts zu berücksichtigen.Ondrawgizmos #if UNITY_EDITOR private void OnDrawGizmos() { Color c = Gizmos.color; foreach (Node node in this.nodes.Values) { Color newColor = Color.white; if (node.isWalkable) newColor = new Color32(153, 255, 51, 255); else newColor = Color.red; Gizmos.color = newColor; Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f); newColor = Color.green; Gizmos.color = newColor; if (node.isWalkable) { for (int i = 0; i < Directions.Length; i++) { Vector2Int coords = node.coords + Directions[i]; Node connection = GetNode(coords); if (connection) { if (connection.isWalkable) Gizmos.DrawLine(node.position, connection.position); } } } } Gizmos.color = c; } #endif
Jetzt können Sie das Spiel sicher starten und sehen, was passiert ist.In diesem Beispiel haben wir nur das Diagramm selbst mithilfe von Aufgaben erstellt. Dies ist jedoch geschehen, nachdem ich den A * -Algorithmus selbst auf das System geschraubt habe , der auch das Job-System verwendet , um den Pfad und die Quelle am Ende des Artikels zu finden .So können Sie das neue Aufgabensystem für Ihre Ziele verwenden und ohne großen Aufwand interessante Systeme erstellen.Wie im vorherigen Artikel wird das Task-System ohne ECS verwendet . Wenn Sie dieses System jedoch in Verbindung mit ECS verwenden , können Sie einfach erstaunliche Ergebnisse bei der Leistungssteigerung erzielen. Viel Glück !Pfadfinder-Projektquelle