Jobsystem und Suchpfad

    Karte


    Im vorigen Artikel habe ich herausgefunden, was das neue Jobsystem ist Job System , wie es funktioniert, wie man Aufgaben erstellt, sie mit Daten füllt und Multithread-Berechnungen durchführt. In wenigen Worten habe ich erklärt, wo man dieses System verwenden kann. In diesem Artikel werde ich versuchen, ein spezifisches Beispiel dafür zu finden, wo Sie dieses System verwenden können, um eine bessere Leistung zu erzielen.

    Da das System ursprünglich für die Arbeit mit Daten entwickelt wurde, eignet es sich hervorragend für die Lösung von Pfadproblemen.

    Die Einheit verfügt bereits über eine gute NavMesh- Suchmaschine , sie funktioniert jedoch nicht in 2D-Projekten, obwohl es im selben Asset-Store viele vorgefertigte Lösungen gibt. Nun, wir werden versuchen, nicht nur ein System zu erstellen, das nach Pfaden auf der erstellten Karte sucht, sondern wir werden diese Karte dynamisieren, sodass jedes Mal, wenn sich etwas ändert, das System eine neue Karte erstellt, und natürlich berechnen wir dies alles mit Das neue Task-System, um den Haupt-Thread nicht zu laden.

    Ein Beispiel für das System
    Bild

    Im Beispiel ist ein Gitter auf der Karte aufgebaut, es gibt einen Bot und ein Hindernis. Das Raster wird jedes Mal neu erstellt, wenn wir die Eigenschaften der Karte ändern (Größe oder Position).

    Für Flugzeuge, benutzen ich einen einfachen SpriteRenderer , in dieser Komponente hervorragende Eigenschaft hat Grenzen , mit denen Sie bequem die Größe der Karte herausfinden können.

    Aber im Allgemeinen und alles für den Anfang, aber wir werden nicht aufhören und auf den Punkt kommen.

    Beginnen wir mit den Skripten. Das Hindernis- Hindernis-Skript wird das erste sein .

    Hindernis
    publicclassObstacle : MonoBehaviour{
    }


    Innerhalb der Hindernisklasse werden alle Änderungen von Hindernissen auf der Karte erfasst, z. B. die Position oder Größe des Objekts.
    Dann können Sie eine Klasse Karte erstellen die Karte , die das Netz aufbauen und es von der Klasse erben Hindernis .

    Karte
    public sealed classMap : Obstacle{
    }


    Die Kartenklasse verfolgt auch alle Änderungen auf der Karte, um das Gitter bei Bedarf neu anzuordnen.

    Füllen Sie dazu das Basisklasse- Hindernis mit allen erforderlichen Variablen und Methoden, um Objektänderungen zu verfolgen.

    Hindernis
    publicclassObstacle : MonoBehaviour{
     publicnew SpriteRenderer renderer { get; privateset;}
     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;
       returntrue;
      }
      Vector2 newPos = this.position;
      diff = (newPos - this.tempPos).sqrMagnitude;
      if (diff > 0.01f) {
       this.tempPos = newPos;
       returntrue;
      }
      returnfalse;
     }
     public Vector2 size {
      get { returnthis.renderer.bounds.size;}
     }
     public Vector2 position {
      get { returnthis.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 Methode Awake wird zum Initialisieren der Variablen verwendet. Die virtuelle Methode CheckChanges verfolgt die aktuellen Änderungen in der Größe und Position des Objekts und gibt ein boolesches Ergebnis zurück.

    Verlassen Sie zunächst das Hindernisskript und gehen Sie zum Map Map- Skript selbst, wo wir es auch mit den für den Betrieb erforderlichen Parametern füllen.

    Karte
    public sealed classMap : 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 in der Karte an. Hier habe ich sie auf 0,1 bis 1 begrenzt, sodass die Zellen im Raster nicht zu klein, sondern zu groß sind. Der variable Versatz wird verwendet, um beim Erstellen des Gitters auf der Karte einzurücken, sodass das Gitter nicht entlang der Ränder der Karte erstellt wird.

    Da es jetzt zwei neue Variablen auf der Karte gibt, stellt sich heraus, dass ihre Änderungen ebenfalls überwacht werden müssen. Fügen Sie dazu ein paar Variablen hinzu und überladen Sie die CheckChanges- Methode in der Map- Klasse .

    Karte
    public sealed classMap : 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;
     protectedoverridevoid Awake() {
      base.Awake();
      this.tempNodeSize = this.nodeSize;
      this.tempOffset = this.offset;
     }
     publicoverride bool CheckChanges() {
      float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize);
      if (diff > 0.01f) {
       this.tempNodeSize = this.nodeSize;
       returntrue;
      }
      diff = (this.tempOffset - this.offset).sqrMagnitude;
      if (diff > 0.01f) {
       this.tempOffset = this.offset;
       returntrue;
      }
      return base.CheckChanges();
     }
    }


    Ist fertig Jetzt können Sie auf der Bühne eine Sprite-Map erstellen und das Map- Skript darauf werfen .

    Bild

    Machen Sie dasselbe mit dem Hindernis - erstellen Sie ein einfaches Sprite auf der Bühne und werfen Sie das Hindernisskript darauf .

    Bild

    Jetzt haben wir Kartenobjekte und Hindernisse auf der Bühne.

    Das Map- Skript ist dafür verantwortlich, alle Änderungen auf der Karte zu verfolgen, wobei in der Update- Methode jeder Frame nach diesen Änderungen sucht.

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/private bool requireRebuild;
     privatevoid Update() {
      UpdateChanges();
     }
     privatevoid UpdateChanges() {
      if (this.requireRebuild) {
       print(“Что то изменилось, необходимо перестроить карту!”);
       this.requireRebuild = false;
      } else {
       this.requireRebuild = CheckChanges();
      }
     }
     /*...остальной код…*/
    }


    Daher verfolgt die Map in der UpdateChanges- Methode nur ihre bisherigen Änderungen. Sie können das Spiel sogar jetzt starten und versuchen, die Kartengröße oder den Versatzabstand zu ändern , um sicherzustellen, dass alle Änderungen verfolgt werden.

    Nun müssen Sie die Änderungen selbst auf der Karte nachverfolgen. Dazu wird jedes Hindernis auf der Liste auf der Karte platziert, die wiederum für jeden Rahmen in dem Verfahren aktualisiert werden Update . Erstellen Sie

    in der Map- Klasse eine Liste aller möglichen Hindernisse auf der Karte und einige statische Methoden für ihre Registrierung.

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/privatestatic Map ObjInstance;
     private List<Obstacle> obstacles = new List<Obstacle>();
     /*...остальной код…*/publicstatic bool RegisterObstacle(Obstacle obstacle) {
      if (obstacle == Instance) returnfalse;
      elseif (Instance.obstacles.Contains(obstacle) == false) {
       Instance.obstacles.Add(obstacle);
       Instance.requireRebuild = true;
       returntrue;
      }
      returnfalse;
     }
     publicstatic bool UnregisterObstacle(Obstacle obstacle) {
      if (Instance.obstacles.Remove(obstacle)) {
       Instance.requireRebuild = true;
       returntrue;
      }
      returnfalse;
     }
     publicstatic Map Instance {
      get {
       if (ObjInstance == null) ObjInstance = FindObjectOfType<Map>();
       return ObjInstance;
      }
     }
    }


    In der statischen Methode RegisterObstacle wird ein neues Hindernis registriert Hindernis auf der Karte und die Liste hinzufügen, aber zunächst ist es wichtig , im Auge zu behalten , dass die Karte selbst auch von der Klasse geerbt Hindernisse und damit die erste Aktion in der Methode , die wir registrieren die Karte als ein Hindernis zu überprüfen , um nicht zu versuchen.

    Die statische Methode UnregisterObstacle hingegen entfernt ein Hindernis von der Karte und entfernt es aus der Liste, wenn wir davon ausgehen, dass wir es zerstören.

    Gleichzeitig müssen Sie jedes Mal, wenn wir ein Hindernis hinzufügen oder aus der Map entfernen, die Map selbst neu erstellen. Nach dem Ausführen dieser statischen Methoden setzen wir die Variable requiredRebuild auf true .

    Damit Sie von jedem Skript aus problemlos auf das Map- Skript zugreifen können, habe ich eine statische Instanzeigenschaft erstellt , die genau diese Instanz von Map zurückgibt .

    Nun , um das Skript zurück Hindernis , das auf der Karte ein Hindernis registriert, werden diese , um es ein paar Methoden hinzufügen OnEnable und EinschSperre .

    Hindernis
    publicclassObstacle : MonoBehaviour{
     /*...остальной код…*/protected virtual void OnEnable() {
      Map.RegisterObstacle(this);
     }
     protected virtual void OnDisable() {
      Map.UnregisterObstacle(this);
     }
    }


    Jedes Mal, wenn wir ein neues Hindernis erstellen, während Sie ein Spiel auf einer Karte spielen, wird es automatisch in der OnEnable- Methode registriert , wo es berücksichtigt wird, wenn Sie ein neues Gitter erstellen und sich in der OnDisable- Methode von der Karte entfernen , wenn es zerstört oder deaktiviert wird.

    Es bleibt nur noch die Änderung der Hindernisse selbst im Map- Skript in der überladenen CheckChanges- Methode zu verfolgen .

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/publicoverride bool CheckChanges() {
      float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize);
      if (diff > 0.01f) {
       this.tempNodeSize = this.nodeSize;
       returntrue;
      }
      diff = (this.tempOffset - this.offset).sqrMagnitude;
      if (diff > 0.01f) {
       this.tempOffset = this.offset;
       returntrue;
      }
      foreach(Obstacle obstacle inthis.obstacles) {
       if (obstacle.CheckChanges()) returntrue;
      }
      return base.CheckChanges();
     }
     /*...остальной код…*/
    }


    Jetzt haben wir eine Karte, Hindernisse - im Allgemeinen alles, was für den Bau des Netzes benötigt wird, und jetzt können Sie zum wichtigsten Teil übergehen.

    Mesh-Konstruktion


    Das Raster ist in seiner einfachsten Form eine zweidimensionale Anordnung von Punkten. Um es zu bauen, müssen Sie die Größe der Karte und die Größe der Punkte auf der Karte 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 ist es immer noch wichtig zu verstehen, wie die Fähigkeiten des Aufgabensystems richtig verwendet werden können. Daher werde ich hier nicht verschiedene Optionen zum Ermitteln des Pfads, seiner Vor- und Nachteile betrachten, sondern die einfachste Variante der Suche A * .

    In diesem Fall sollten alle Punkte des Gitters neben der Position, den Koordinaten und der Eigenschaft der Durchgängigkeit verfügen.

    Ich denke, mit der Durchgängigkeit ist alles klar, warum es notwendig ist, aber die Koordinaten geben die Reihenfolge des Punktes auf dem Gitter an, diese Koordinaten sind nicht speziell an die Position des Punktes im Raum gebunden. Das Bild unten zeigt ein einfaches Gitter, das die Unterschiede der Koordinaten von der Position zeigt.

    Bild
    Warum koordinieren?
    Tatsache ist, dass zur Vereinheitlichung der Position eines Objekts im Raum ein einfacher Float verwendet wird, der sehr ungenau ist und eine gebrochene oder negative Zahl sein kann. Daher ist es schwierig, die Suche nach einem Pfad auf der Karte zu implementieren. Koordinaten werden in Form eines klaren Intents gebildet, das immer positiv ist und mit dem die Suche nach benachbarten Punkten viel einfacher ist.

    Zuerst definieren wir den Objektpunkt, es werden einfache Strukturen seinDer Knoten .

    Knoten
    public struct Node {
     public int id;
     public Vector2 position;
     public Vector2Int coords;
    }


    Diese Struktur enthält die Position Position als Vector2 , wobei mit dieser Variablen ein Punkt im Raum gezeichnet wird. Teilweise koordiniert coords als Vector2Int werden die Koordinaten des Punktes auf der Karte, und die Variable angeben id seine numerische Nummer des Kontos mit Hilfe dessen werden wir die verschiedenen Punkte auf dem Netz vergleichen , und die Existenz eines Punktes überprüfen.

    Die Permeabilität des Punkts wird in Form seiner booleschen Eigenschaft angegeben. Da wir im Task-System jedoch keine konvertierbaren Datentypen verwenden können, geben wir die Permeabilität als Int- Zahlen an. Dafür habe ich die einfache Aufzählung NodeType verwendetDabei gilt: 0 ist kein passierbarer Punkt und 1 ist passierbar.

    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 { returnthis.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 erleichtern , werde ich die Equals- Methode überladen , um den Vergleich von Punkten zu erleichtern und die Testmethode für das Vorhandensein eines Punktes zu ergänzen.

    Knoten
    public struct Node {
     /*...остальной код…*/publicoverride bool Equals(object obj) {
      if (obj is Node) {
       Node other = (Node)obj;
       returnthis.id == other.id;
      } elsereturn base.Equals(obj);
     }
     publicstatic implicit operator bool(Node node) {
      return node.id > 0;
     }
    }


    Da die ID- Nummer eines Punkts im Raster mit einer Einheit beginnt, prüfe ich die Existenz eines Punkts als Bedingung, dass seine ID größer als 0 ist.

    Gehen Sie zur Map- Klasse, in der wir alles vorbereiten, um eine Karte zu erstellen.
    Wir haben bereits eine Überprüfung zum Ändern der Kartenparameter vorgenommen. Nun müssen wir ermitteln, wie genau der Netzerzeugungsprozess durchgeführt wird. Erstellen Sie dazu eine neue Variable und mehrere Methoden.

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/public bool rebuilding { get; privateset; }
     publicvoid Rebuild() {}
     privatevoid OnRebuildStart() {}
     privatevoid OnRebuildFinish() {}
     /*...остальной код…*/
    }


    Die Wiederherstellungs- Eigenschaft gibt an, ob das Gitter erstellt wird. Bei der Rebuild- Methode werden Daten und Aufgaben zum Erstellen eines Gitters erfasst. Anschließend startet die OnRebuildStart- Methode das Erstellen eines Gitters, und die OnRebuildFinish- Methode sammelt Daten von Aufgaben.

    Jetzt ändern wir die UpdateChanges- Methode ein wenig, damit die Netzbedingung berücksichtigt wird.

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/public bool rebuilding { get; privateset; }
     privatevoid UpdateChanges() {
      if (this.rebuilding) {
       print(“Идет построение сетки...”);
      } else {
       if (this.requireRebuild) {
        print(“Что то изменилось, необходимо перестроить карту!”);
        Rebuild();
       } else {
        this.requireRebuild = CheckChanges();
       }
      }
     }
     publicvoid Rebuild() {
      if (this.rebuilding) return;
      print(“Перестраиваю карту!”);
      OnRebuildStart();
     }
     privatevoid OnRebuildStart() {
      this.rebuilding = true;
     }
     privatevoid OnRebuildFinish() {
      this.rebuilding = false;
     }
     /*...остальной код…*/
    }


    Wie Sie jetzt in der UpdateChanges- Methode sehen können, gibt es eine Bedingung, dass, solange das alte Raster erstellt wird, kein neues Gitter erstellt wird. Außerdem wird in der Rebuild- Methode geprüft, ob der Aufbau des Rasters bereits ausgeführt wird.

    Problemlösung


    Nun ein wenig über den Prozess des Aufbaus einer Karte.
    Da wir das Aufgabensystem verwenden und das Raster parallel erstellen, um die Karte zu erstellen, habe ich den Aufgabentyp IJobParallelFor verwendet , der eine bestimmte Anzahl von Malen ausgeführt wird. Um den Konstruktionsprozess nicht mit einer einzelnen Aufgabe zu belasten, verwenden wir einen Pool von Aufgaben, die in einem JobHandle gepackt sind .

    Meistens verwenden sie zum Erstellen eines Gitters zwei ineinander verschachtelte Zyklen, um beispielsweise horizontal und vertikal zu bauen. In diesem Beispiel erstellen wir das Gitter auch zuerst horizontal und dann vertikal. Zu diesem Zweck wird bei dem Verfahren neu erstellen , die Anzahl der horizontalen und vertikalen Pixel berechnen, dann geht das Verfahren wird neu aufbauenLassen Sie uns die vertikalen Punkte durchlaufen, und die horizontalen Punkte werden parallel in der Task eingebaut. Sehen Sie sich die Animation unten an, um den Build-Prozess besser zu visualisieren.

    Mesh-Konstruktion
    Bild

    Die Anzahl der vertikalen Punkte gibt die Anzahl der Aufgaben an. Jede Aufgabe erstellt wiederum Punkte nur horizontal. Nachdem alle Aufgaben ausgeführt wurden, werden die Punkte in einer Liste zusammengefasst. Deshalb muss ich eine Task des Typs IJobParallelFor verwenden , um den Index eines Punktes im Raster horizontal an die Execute- Methode zu übertragen .

    Und so haben wir die Struktur von Punkten. Jetzt können Sie die Struktur der Job- Aufgabe selbst erstellen und über die IJobParallelFor- Schnittstelle erben. Alles ist einfach.

    Job Job
    public struct Job : IJobParallelFor {
     publicvoid Execute(int index) {}
    }


    Wir kehren in der Rebuild- Methode der Map- Klasse zurück , wo wir die notwendigen Berechnungen zum Messen des Gitters durchführen.

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код...*/publicvoid 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();
     }
     /*...остальной код...*/
    }


    Das Verfahren neu erstellen , die genaue Größe Karte berechnen Kartengrösse mit der Vertiefung in mehreren vertikalen Zählwert von Pixeln in der vertikalen und in der Schreib horizontals Anzahl von horizontalen Punkten. Wenn die Anzahl der vertikalen Punkte 0 ist, beenden wir das Erstellen der Karte und rufen die OnRebuildFinish- Methode auf, um den Vorgang abzuschließen. Die Ursprungsvariable gibt den Ort an, von dem aus wir das Gitter erstellen werden - im Beispiel ist dies der linke untere Punkt auf der Karte.

    Nun können Sie zu den Aufgaben selbst gehen und sie mit Daten füllen.
    Während des Aufbaus des Gitters in der Aufgabe wird ein Array von NativeArray benötigtWo wir die erhaltenen Punkte platzieren, auch weil wir Hindernisse auf der Karte haben, müssen wir sie auch in die Aufgabe einbinden. Dazu verwenden wir ein anderes NativeArray- Array . Dann benötigen wir die Größe der Punkte in der Aufgabe, die Startposition, aus der wir Punkte aufbauen die Anfangskoordinaten der Serie.

    Job Job
    public struct Job : IJobParallelFor {
     [WriteOnly]
     public NativeArray<Node> array;
     [ReadOnly]
     public NativeArray<Rect> bounds;
     public float nodeSize;
     public Vector2 startPos;
     public Vector2Int startCoords;
     publicvoid Execute(int index) {}
    }


    Ein Array von Pixeln Array markiert I Attribut Writeonly , weil es nur ein Problem , das Sie benötigen , wird zu „ schreiben “ , um die Daten auf ein Array, eine Reihe von Hindernissen vor den Grenzen markiert Attribut Readonly , weil das Problem werden wir „nur lesen “ , um die Daten aus dem Array.

    Nun, und während alles weitergeht, werden wir später zur Berechnung von Punkten übergehen.

    Gehen wir nun zurück zur Map- Klasse, in der wir alle an den Aufgaben beteiligten Variablen bezeichnen.
    Zunächst benötigen wir ein globales Task- Handle , eine Reihe von Hindernissen in Form von NativeArray , eine Liste von Tasks, die alle Punkte enthält, die im Raster und erhalten wurdenWörterbuch mit allen Koordinaten und Punkten auf der Karte, damit es später bequemer ist, nach ihnen zu suchen.

    Karte
    public sealed classMap : 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>();
     /*...остальной код...*/
    }


    Jetzt kehren wir wieder zur Rebuild- Methode zurück und bauen das Gitter weiter auf.
    Initialisieren Sie zunächst das Begrenzungsfeld von Hindernissen , um es an die Aufgabe zu übergeben.

    Wiederaufbau
    publicvoid 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 durch einen neuen Konstruktor mit drei Parametern. Ich habe die ersten beiden Parameter im letzten Artikel analysiert, aber der dritte Parameter hilft uns, beim Erstellen eines Arrays etwas Zeit zu sparen. Tatsache ist, dass wir unmittelbar nach der Erstellung Daten in das Array schreiben werden. Dies bedeutet, dass wir nicht sicherstellen müssen, dass die Daten gelöscht werden. Diese Option ist nützlich für NativeArray , die nur in „verwendet werden sollen Lese “ in dem vorliegenden Problem.

    Und so werden wir das Grenzen- Array weiter mit Daten füllen .

    Wiederaufbau
    publicvoid 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 werden wir alle vertikalen Reihen des Gitters durchlaufen.

    Wiederaufbau
    publicvoid 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();
    }


    Zu Beginn erhalten wir in xPos und yPos die Startposition der Zeile horizontal.

    Wiederaufbau
    publicvoid 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();
    }


    Erstellen Sie als Nächstes ein einfaches Array NativeArray, in dem die Punkte in der Task platziert werden: Hier müssen Sie für das Array- Array angeben, wie viele Punkte horizontal erstellt werden sollen, und den Typ der Zuweisung Persistent , da die Task länger als ein Frame ausführen kann.
    eine Kopie Aufgabe selbst nach dem Erstellen Job , haben wir es in einer Reihe von Anfangskoordinaten startCoords , die Anfangsposition einer Anzahl startPos , Punktgrße nodeSize , Hindernisse Array Grenzen und am Ende des Arrays Punkt Array .
    Es bleibt nur noch, die Aufgabe im Handle und in der globalen Aufgabenliste zu platzieren.

    Wiederaufbau
    publicvoid 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();
    }


    Ist fertig Wir haben eine Liste von Aufgaben und deren gemeinsames Handle . Jetzt können wir dieses Handle ausführen , indem Sie die Complete- Methode in der OnRebuildStart- Methode aufrufen .

    OnRebuildStart
    privatevoid OnRebuildStart() {
      this.rebuilding = true;
      this.handle.Complete();
     }


    Da die Variable Wiederaufbau würde bedeuten , dass es ein Eingriffsprozess ist, in dem Verfahren UpdateChanges auch eine Bedingung angeben müssen , wenn der Prozess des fertig ist mit Griff und seine Eigenschaften IsCompleted .

    Updateänderungen
    privatevoid UpdateChanges() {
      if (this.rebuilding) {
       print(“Идет построение сетки...”);
       if (this.handle.IsCompleted) OnRebuildFinish();
      } else {
       if (this.requireRebuild) {
        print(“Что то изменилось, необходимо перестроить карту!”);
        Rebuild();
       } else {
        this.requireRebuild = CheckChanges();
       }
      }
     }


    Nach der Wahrnehmung ihrer Aufgaben wird eine Methode aufrufen OnRebuildFinish wo wir bereits die Datenpunkte in einer einzigen Liste sammeln ein Wörterbuch , und vor allem - alle zugewiesenen Ressourcen aufzuräumen.

    OnRebuildFinish
    privatevoid OnRebuildFinish() {
      this.nodes.Clear();
      foreach (NativeArray<Node> array inthis.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. Dann verwenden Sie die foreach- Schleife, um alle erhaltenen Punkte aus den Aufgaben zu durchlaufen und in das Knotenwörterbuch zu legen , wobei der Schlüssel die Koordinaten ist ( NICHT die Position !). Punkte und der Wert ist der Punkt selbst. Mit diesem Wörterbuch können wir leichter nach benachbarten Punkten auf der Karte suchen. Nach dem Befüllen reinigen Array Array mit der Methode entsorgen und sich schließlich gereinigt , um die Aufgabenliste Jobs Benutzer .

    Sie müssen auch das Begrenzungs- Hindernis-Array löschen, wenn es zuvor erstellt wurde.

    Nach all diesen Aktionen erhalten wir eine Liste aller Punkte auf der Karte und jetzt können wir sie auf die Szene zeichnen.

    Ungefähr so
    Bild

    Erstellen Sie dazu in der Map- Klasse die OnDrawGizmos- Methode , in der die Punkte gezeichnet werden .

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/
     #if UNITY_EDITOR
     privatevoid OnDrawGizmos() {}
     #endif
    }


    Zeichne jetzt durch die Schleife jeden Punkt.

    Karte
    public sealed classMap : Obstacle{
     /*...остальной код…*/
     #if UNITY_EDITOR
     privatevoid OnDrawGizmos() {
      foreach (Node node inthis.nodes.Values) {
       Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f);
      }
     }
     #endif
    }


    Nach all diesen Aktionen sieht unsere Map irgendwie langweilig aus. Um ein echtes Raster zu erhalten, müssen Sie die Punkte miteinander verbinden.

    Gitter
    Bild

    Um benachbarte Punkte suchen wir müssen nur den gewünschten Punkt durch seine Koordinaten in 8 Richtungen finden, so dass die Klasse Map eingeleitet eine einfache statische Array Richtungen Richtungen und Verfahren für ihre Zelle des Suchens Koordinaten GetNode .

    Karte
    public sealed classMap : Obstacle{
     publicstatic 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
     privatevoid OnDrawGizmos() {}
     #endif
    }


    Die GetNode- Methode gibt einen Punkt nach Koordinaten aus der Liste der Knoten zurück . Dies sollte jedoch sorgfältig ausgeführt werden. Wenn die Koordinaten von Vector2Int nicht korrekt sind, wird ein Fehler auftreten. Daher verwenden wir den try catch- Ausnahmebypassblock , der die Ausnahme verhindert und die gesamte Anwendung nicht mit einem Fehler " hängt ".

    Gehen Sie als Nächstes durch den Zyklus in alle Richtungen und versuchen Sie, nach benachbarten Punkten in der OnDrawGizmos- Methode zu suchen. Vergessen Sie nicht, die Cross-Country-Fähigkeit zu berücksichtigen.

    OnDrawGizmos
     #if UNITY_EDITOR
     privatevoid OnDrawGizmos() {
      Color c = Gizmos.color;
      foreach (Node node inthis.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 ausführen und sehen, was passiert ist.

    Dynamische Karte
    Bild

    In diesem Beispiel haben wir nur den Graphen selbst mit Hilfe von Tasks erstellt. Dies ist jedoch der Fall, nachdem ich den A * -Algorithmus an dieses System geschraubt habe , das ebenfalls das Job-System verwendet , um den Pfad und den Quellcode am Ende des Artikels zu finden .

    Karte und Suchpfad
    Bild

    So können Sie das neue Aufgabensystem für Ihre Zwecke verwenden und ohne viel Aufwand interessante Systeme erstellen.

    Wie im vorherigen Artikel wird das Aufgabensystem hier ohne ECS verwendet . Wenn Sie dieses System jedoch zusammen mit ECS verwenden , können Sie ein enormes Ergebnis bei der Produktivitätssteigerung erzielen. Viel Glück !

    Pathfinder-Projektquelle

    Jetzt auch beliebt: