Jobsystem und Suchpfad

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
Bild

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.

Hindernis
public 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.

Bild

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

Bild

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.

Bild
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.

Vernetzung
Bild

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.

Ungefähr so
Bild

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.

Mesh
Bild

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.

Dynamische Karte
Bild

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 .

Karten- und Pfadsuche
Bild

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

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


All Articles