Système d'emploi et chemin de recherche

La carte


Dans un article précédent, j'ai examiné ce qu'est le nouveau système de travail , comment il fonctionne, comment créer des tâches, les remplir avec des données et effectuer des calculs multithread, et je n'ai expliqué que brièvement où vous pouvez utiliser ce système. Dans cet article, je vais essayer d'analyser un exemple spécifique de l'endroit où vous pouvez utiliser ce système pour obtenir plus de performances.

Étant donné que le système a été initialement développé dans le but de travailler avec des données, il est idéal pour résoudre les tâches de recherche de chemin.

Unity possède déjà un bon explorateur NavMesh , mais il ne fonctionne pas dans les projets 2D, bien qu'il existe de nombreuses solutions prêtes à l'emploi sur le même actif . Eh bien, et nous essaierons de créer non seulement un système qui cherchera des moyens sur la carte créée, mais rendra cette carte très dynamique, de sorte que chaque fois que quelque chose change, le système crée une nouvelle carte, et tout cela, bien sûr, nous calculerons en utilisant un nouveau système de tâches, afin de ne pas charger le thread principal.

Exemple de fonctionnement du système
image

Dans l'exemple, une grille est construite sur la carte, il y a un bot et un obstacle. La grille est reconstruite chaque fois que nous modifions les propriétés de la carte, que ce soit sa taille ou sa position.

Pour les avions, j'ai utilisé un simple SpriteRenderer , ce composant a une excellente propriété de limites avec laquelle vous pouvez facilement trouver la taille de la carte.

C'est essentiellement tout pour commencer, mais nous ne nous arrêterons pas et nous passerons immédiatement aux choses sérieuses.

Commençons par les scripts. Et le premier est le script d'obstruction Obstacle .

Obstacle
public class Obstacle : MonoBehaviour { } 


À l'intérieur de la classe Obstacle , nous verrons tous les changements dans les obstacles sur la carte, par exemple, en changeant la position ou la taille d'un objet.
Ensuite, vous pouvez créer la classe Map map, sur laquelle la grille sera construite, et l'hériter de la classe Obstacle .

La carte
 public sealed class Map : Obstacle { } 


La classe Map suivra également toutes les modifications sur la carte afin de reconstruire la grille si nécessaire.

Pour ce faire, remplissez la classe de base Obstacle avec toutes les variables et méthodes nécessaires pour suivre les modifications d'objets.

Obstacle
 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;} } } 


Ici, la variable de rendu aura une référence au composant SpriteRenderer , et les variables tempSize et tempPos seront utilisées pour suivre les changements de taille et de position de l'objet.

La méthode virtuelle Awake sera utilisée pour initialiser les variables, et la méthode virtuelle CheckChanges suivra les changements actuels dans la taille et la position de l'objet et retournera un résultat booléen .

Pour l'instant, laissons le script Obstacle et passons au script Map map lui-même, où nous le remplissons également avec les paramètres nécessaires pour le travail.

La carte
 public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); } 


La variable nodeSize indiquera la taille des cellules sur la carte, ici j'ai limité sa taille de 0,1 à 1 pour que les cellules de la grille ne soient pas trop petites, mais aussi trop grandes. La variable de décalage sera utilisée pour indenter la carte lors de la construction de la grille afin que la grille ne se construise pas le long des bords de la carte.

Comme il y a maintenant deux nouvelles variables sur la carte, il s'avère que leurs changements devront également être suivis. Pour ce faire, ajoutez quelques variables et surchargez la méthode CheckChanges dans la classe Map .

La carte
 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(); } } 


C'est fait. Vous pouvez maintenant créer un sprite de carte sur la scène et lancer un script de carte dessus.

image

Nous ferons de même avec un obstacle - créez un simple sprite sur scène et lancez le script Obstacle dessus.

image

Nous avons maintenant des objets cartographiques et des obstacles sur scène.

Le script de carte sera responsable du suivi de toutes les modifications sur la carte, où dans la méthode de mise à jour , nous vérifierons chaque cadre pour les modifications.

La carte
 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(); } } /*... …*/ } 


Ainsi, dans la méthode UpdateChanges, la carte ne suivra que ses modifications jusqu'à présent. Vous pouvez même démarrer le jeu maintenant et essayer de modifier la taille de la carte ou le décalage de décalage pour vous assurer que tous les changements sont suivis.

Maintenant, vous devez en quelque sorte suivre les changements des obstacles eux-mêmes sur la carte. Pour ce faire, nous mettrons chaque obstacle dans une liste sur la carte, qui à son tour mettra à jour chaque cadre dans la méthode Update .

Dans la classe Map , créez une liste de tous les obstacles possibles sur la carte et quelques méthodes statiques pour les enregistrer.

La carte
 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; } } } 


Dans la méthode RegisterObstacle statique, nous allons enregistrer un nouvel obstacle Obstacle sur la carte et l'ajouter à la liste, mais tout d'abord, il est important de considérer que la carte elle-même est également héritée de la classe Obstacle et nous devons donc vérifier si nous essayons d'enregistrer la carte elle-même comme obstacle.

La méthode statique UnregisterObstacle , au contraire, élimine l'obstacle de la carte et le supprime de la liste lorsque nous permettons qu'il soit détruit.

Dans le même temps, chaque fois que nous ajoutons ou supprimons un obstacle de la carte, il est nécessaire de recréer la carte elle- même.Ainsi , après avoir exécuté ces méthodes statiques, définissez la variable requireRebuild sur true .

De plus, pour avoir un accès facile au script Map à partir de n'importe quel script, j'ai créé une propriété Instance statique qui me rendra cette instance même de Map .

Revenons maintenant au script Obstacle où nous allons enregistrer un obstacle sur la carte. Pour ce faire, ajoutez-y quelques méthodes OnEnable et OnDisable .

Obstacle
 public class Obstacle : MonoBehaviour { /*... …*/ protected virtual void OnEnable() { Map.RegisterObstacle(this); } protected virtual void OnDisable() { Map.UnregisterObstacle(this); } } 


Chaque fois que nous créons un nouvel obstacle en jouant sur la carte, il s'enregistrera automatiquement dans la méthode OnEnable , où il sera pris en compte lors de la construction d'une nouvelle grille et nous supprimera de la carte dans la méthode OnDisable lorsqu'il sera détruit ou désactivé.

Il ne reste plus qu'à suivre les changements des obstacles eux-mêmes dans le script Map dans la méthode CheckChanges surchargée.

La carte
 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(); } /*... …*/ } 


Nous avons maintenant une carte, des obstacles - en général, tout ce dont vous avez besoin pour construire une grille et maintenant vous pouvez passer à la chose la plus importante.

Maillage


La grille, dans sa forme la plus simple, est un tableau bidimensionnel de points. Pour le construire, vous devez connaître la taille de la carte et la taille des points dessus, après quelques calculs, nous obtenons le nombre de points horizontalement et verticalement, voici notre grille.

Il existe de nombreuses façons de trouver un chemin sur une grille. Dans cet article, cependant, l'essentiel est de comprendre comment utiliser correctement les capacités du système de tâches, donc ici je ne considérerai pas différentes options pour trouver le chemin, leurs avantages et leurs inconvénients, mais je prendrai l'option de recherche la plus simple A * .

Dans ce cas, tous les points de la grille doivent avoir, en plus de la position, les coordonnées et la propriété de perméabilité.

Avec perméabilité, je pense que tout est clair pourquoi il est nécessaire, mais les coordonnées indiqueront l'ordre du point sur la grille, ces coordonnées ne sont pas liées spécifiquement à la position du point dans l'espace. L'image ci-dessous montre une grille simple montrant les différences de coordonnées d'une position.

image
Pourquoi les coordonnées?
Le fait est que dans l'unité, pour indiquer la position d'un objet dans l'espace, un flottant simple est très inexact et peut être un nombre fractionnaire ou négatif, il sera donc difficile de l'utiliser pour implémenter une recherche de chemin sur la carte. Les coordonnées sont faites sous la forme d'un int clair qui sera toujours positif et avec lequel il est beaucoup plus facile de travailler lors de la recherche de points voisins.

Tout d'abord, définissons un objet point, ce sera une structure de nœud simple.

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


Cette structure contiendra la position position sous la forme de Vector2 , où avec cette variable nous dessinerons un point dans l'espace. La variable de coordonnées coords sous la forme de Vector2Int indiquera les coordonnées d'un point sur la carte, et la variable id , son numéro de compte numérique, en l'utilisant, nous comparerons différents points sur la grille et vérifierons l'existence d'un point.

La perméabilité du point sera indiquée sous la forme de sa propriété booléenne , mais comme nous ne pouvons pas utiliser les types de données convertibles dans le système de tâches, nous indiquerons sa perméabilité sous la forme d'un nombre entier , pour cela j'ai utilisé une énumération simple NodeType , où: 0 n'est pas un point passable, et 1 est passable.

NodeType et 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; } } 


De plus, pour la commodité de travailler avec un point, je surchargerai la méthode Equals pour faciliter la comparaison des points et compléter la méthode de vérification de l'existence d'un point.

Noeud
 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; } } 


Étant donné que le numéro d' identification du point sur la grille commencera par 1 unité, je vérifierai l'existence du point comme condition que son identifiant soit supérieur à 0.

Allez dans la classe Map où nous allons tout préparer pour créer une carte.
Nous avons déjà une vérification pour changer les paramètres de la carte, maintenant nous devons déterminer comment le processus de construction de la grille sera exécuté. Pour ce faire, créez une nouvelle variable et plusieurs méthodes.

La carte
 public sealed class Map : Obstacle { /*... …*/ public bool rebuilding { get; private set; } public void Rebuild() {} private void OnRebuildStart() {} private void OnRebuildFinish() {} /*... …*/ } 


La propriété de reconstruction indiquera si le processus de maillage est en cours. La méthode Rebuild collectera les données et les tâches pour la construction de la grille, puis la méthode OnRebuildStart démarrera le processus de construction de la grille et la méthode OnRebuildFinish collectera les données des tâches.

Modifions maintenant un peu la méthode UpdateChanges pour que l'état de la grille soit pris en compte.

La carte
 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; } /*... …*/ } 


Comme vous pouvez le voir maintenant dans la méthode UpdateChanges , il y a une condition que lors de la construction de l'ancien maillage ne commence pas à commencer à en créer un nouveau, et également dans la méthode Rebuild , la première action vérifie si le processus de maillage est déjà en cours.

Résolution de problèmes


Maintenant, un peu sur le processus de construction d'une carte.
Comme nous allons utiliser le système de tâches et construire la grille en parallèle pour construire la carte, j'ai utilisé le type de la tâche IJobParallelFor , qui sera exécutée un certain nombre de fois. Afin de ne pas charger le processus de construction avec une seule tâche distincte, nous utiliserons le pool de tâches regroupées dans un JobHandle .

Le plus souvent, pour construire une grille, utilisez deux cycles imbriqués l'un dans l'autre pour construire, par exemple, horizontalement et verticalement. Dans cet exemple, nous allons également construire la grille d'abord horizontalement puis verticalement. Pour ce faire, nous calculons le nombre de points horizontaux et verticaux dans la méthode Reconstruire , puis dans la méthode Reconstruire , nous parcourons le cycle le long des points verticaux, et nous construirons des points horizontaux en parallèle dans la tâche. Pour mieux imaginer le processus de construction, regardez l'animation ci-dessous.

Maillage
image

Le nombre de points verticaux indiquera le nombre de tâches, à son tour, chaque tâche ne construira des points que horizontalement, après avoir terminé toutes les tâches, les points sont additionnés dans une liste. C'est pourquoi j'ai besoin d'utiliser une tâche comme IJobParallelFor pour passer horizontalement l'index du point sur la grille dans la méthode Execute .

Et donc nous avons la structure de points, maintenant vous pouvez créer la structure de la tâche Job et l'hériter de l'interface IJobParallelFor , tout est simple ici.

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


Nous revenons à la méthode de reconstruction de la classe Map , où nous effectuerons les calculs nécessaires pour la mesure de la grille.

La carte
 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(); } /*... ...*/ } 


Dans la méthode Rebuild , nous calculons la taille exacte de la carte mapSize , en tenant compte de l'indentation, puis dans les verticales nous écrivons le nombre de points verticalement, et en horizontal le nombre de points horizontalement. Si le nombre de points verticaux est égal à 0, nous arrêtons de construire la carte et appelons la méthode OnRebuildFinish pour terminer le processus. La variable d' origine indiquera l'endroit à partir duquel nous commencerons à construire la grille - dans l'exemple, il s'agit du point inférieur gauche sur la carte.

Vous pouvez maintenant accéder aux tâches elles-mêmes et les remplir de données.
Lors de la construction de la grille, la tâche aura besoin d'un tableau NativeArray où nous placerons les points, également puisque nous avons des obstacles sur la carte, nous devrons également les passer à la tâche, pour cela nous utiliserons un autre tableau NativeArray , puis nous aurons besoin de la taille des points dans le problème , la position initiale à partir de laquelle nous allons construire les points, ainsi que les coordonnées initiales de la série.

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) {} } 


J'ai marqué le tableau de points avec l'attribut WriteOnly, car dans la tâche, il ne sera nécessaire que d '« écrire » les points reçus dans le tableau, au contraire, le tableau des limites d' obstacles est marqué avec l'attribut ReadOnly car dans la tâche, nous ne « lirons » que les données de ce tableau.

Eh bien, pour l'instant, procédons au calcul des points eux-mêmes plus tard.

Revenons maintenant à la classe Map , où nous désignons toutes les variables impliquées dans les tâches.
Ici, tout d'abord, nous avons besoin d'une poignée de tâche globale, d'un tableau d'obstacles sous la forme d'un tableau natif , d'une liste de tâches qui contiendra tous les points reçus sur la grille et du dictionnaire avec toutes les coordonnées et les points sur la carte, de sorte qu'il serait plus pratique de les rechercher plus tard.

La carte
 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>(); /*... ...*/ } 


Maintenant, nous revenons à la méthode Rebuild et continuons à construire la grille.
Tout d'abord, initialisez le tableau des limites des obstacles pour le passer à la tâche.

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


Ici, nous créons une instance de NativeArray via un nouveau constructeur avec trois paramètres. J'ai examiné les deux premiers paramètres dans un article précédent, mais le troisième paramètre nous aidera à gagner un peu de temps en créant un tableau. Le fait est que nous allons écrire des données dans le tableau immédiatement après sa création, ce qui signifie que nous n'avons pas besoin de nous assurer qu'elles sont effacées. Ce paramètre est utile pour NativeArray qui ne sera utilisé qu'en mode lecture dans la tâche.

Et donc, nous remplissons le tableau de limites avec des données.

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


Nous pouvons maintenant passer à la création de tâches, pour cela nous allons parcourir un cycle à travers toutes les lignes verticales de la grille.

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


Pour commencer, dans xPos et yPos, nous obtenons la position horizontale initiale de la série.

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


Ensuite, nous créons un tableau NativeArray simple où les points de la tâche seront placés, ici pour le tableau de tableau, vous devez spécifier le nombre de points qui seront créés horizontalement et le type d'allocation Persistant , car la tâche peut prendre plus d'une image.
Après cela, créez l'instance de tâche Job elle-même, placez les coordonnées initiales de la série startCoords , la position initiale de la série startPos , la taille des points nodeSize , le tableau de limites des obstacles et le tableau de points lui-même à la fin.
Il ne reste plus qu'à mettre la tâche en poignée et la liste globale des tâches.

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


C'est fait. Nous avons une liste de tâches et leur handle commun, maintenant nous pouvons exécuter ce handle en appelant sa méthode Complete dans la méthode OnRebuildStart .

Onrebuildstart
 private void OnRebuildStart() { this.rebuilding = true; this.handle.Complete(); } 


Étant donné que la variable de reconstruction indiquera que le processus de maillage est en cours, la méthode UpdateChanges elle-même doit également spécifier la condition à laquelle ce processus se terminera à l'aide de handle et de sa propriété IsCompleted .

Mises à jour
 private void UpdateChanges() { if (this.rebuilding) { print(“  ...”); if (this.handle.IsCompleted) OnRebuildFinish(); } else { if (this.requireRebuild) { print(“  ,   !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } } 


Après avoir terminé les tâches, la méthode OnRebuildFinish sera appelée où nous rassemblerons déjà les points reçus dans une liste de dictionnaires générale, et surtout, pour effacer les ressources occupées.

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; } 


Tout d'abord, nous effaçons le dictionnaire de nœuds des points précédents, puis utilisons la boucle foreach pour trier tous les points que nous avons reçus des tâches et les mettons dans le dictionnaire de nœuds , où la clé est les coordonnées ( PAS la position !) Du point, et la valeur est le point lui-même. Avec l'aide de ce dictionnaire, il nous sera plus facile de rechercher des points voisins sur la carte. Après le remplissage, nous effaçons le tableau tableau à l'aide de la méthode Dispose et à la fin, nous effaçons la liste des tâches de travaux elle-même .

Vous devrez également effacer le tableau des limites des obstacles s'il a été créé précédemment.

Après toutes ces actions, nous obtenons une liste de tous les points sur la carte et maintenant vous pouvez les dessiner sur la scène.

Quelque chose comme ça
image

Pour ce faire, dans la classe Map , créez la méthode OnDrawGizmos où nous allons dessiner les points.

La carte
 public sealed class Map : Obstacle { /*... …*/ #if UNITY_EDITOR private void OnDrawGizmos() {} #endif } 


Maintenant, dans la boucle, nous dessinons chaque point.

La carte
 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 } 


Après toutes ces actions, notre carte semble en quelque sorte ennuyeuse, afin d'avoir vraiment une grille, vous avez besoin que les points soient connectés les uns aux autres.

Mesh
image

Pour rechercher des points voisins, nous avons juste besoin de trouver le point souhaité par ses coordonnées dans 8 directions, donc dans la classe Map , nous allons créer un simple tableau statique de directions Directions et une méthode de recherche de cellules par ses coordonnées GetNode .

La carte
 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 } 


La méthode GetNode renverra un point par coordonnées de la liste des nœuds , mais vous devez le faire avec précaution, car si les coordonnées Vector2Int sont incorrectes, une erreur se produira, nous utilisons donc ici le bloc try bypass exception , qui aidera à contourner l'exception et à ne pas " bloquer " l'application entière avec une erreur.

Ensuite, nous allons parcourir le cycle dans toutes les directions et essayer de trouver des points voisins dans la méthode OnDrawGizmos , et surtout, n'oubliez pas de considérer la perméabilité du point.

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 


Vous pouvez maintenant démarrer le jeu en toute sécurité et voir ce qui s'est passé.

Carte dynamique
image

Dans cet exemple, nous avons construit uniquement le graphique lui-même à l'aide de tâches, mais c'est ce qui s'est produit après avoir vissé sur le système l'algorithme A * lui - même , qui utilise également le système Job pour trouver le chemin, la source à la fin de l'article .

Recherche de carte et de chemin
image

Vous pouvez donc utiliser le nouveau système de tâches pour vos objectifs et créer des systèmes intéressants sans trop d'effort.

Comme dans l'article précédent, le système de tâches est utilisé sans ECS , mais si vous utilisez ce système en conjonction avec ECS , vous pouvez obtenir des résultats tout simplement incroyables en termes de gains de performances. Bonne chance !

Source du projet Path Finder

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


All Articles