Sistema de trabajo y ruta de búsqueda

Mapa


En un artículo anterior, analicé cuál es el nuevo sistema Job , cómo funciona, cómo crear tareas, llenarlas con datos y realizar cálculos de subprocesos múltiples, y solo le expliqué brevemente dónde puede usar este sistema. En este artículo, intentaré analizar un ejemplo específico de dónde puede usar este sistema para obtener más rendimiento.

Dado que el sistema se desarrolló originalmente con el objetivo de trabajar con datos, es ideal para resolver tareas de búsqueda de rutas.

Unity ya tiene un buen navegador NavMesh , pero no funciona en proyectos 2D, aunque hay muchas soluciones listas para usar en el mismo activo . Bueno, e intentaremos crear no solo un sistema que buscará formas en el mapa creado, sino que hará que este mapa sea dinámico, de modo que cada vez que algo cambie en él, el sistema creará un nuevo mapa, y todo esto, por supuesto, lo calcularemos utilizando Un nuevo sistema de tareas, para no cargar el hilo principal.

Ejemplo de operación del sistema
imagen

En el ejemplo, se construye una cuadrícula en el mapa, hay un bot y un obstáculo. La cuadrícula se reconstruye cada vez que cambiamos cualquier propiedad del mapa, ya sea su tamaño o posición.

Para los aviones, utilicé un SpriteRenderer simple, este componente tiene una propiedad de límites excelente con la que puede averiguar fácilmente el tamaño del mapa.

Básicamente, eso es todo para empezar, pero no nos detendremos e inmediatamente nos pondremos manos a la obra.

Comencemos con los guiones. Y el primero es el script de obstrucción de obstáculos .

Obstáculo
public class Obstacle : MonoBehaviour { } 


Dentro de la clase Obstáculo , detectaremos todos los cambios en los obstáculos en el mapa, por ejemplo, cambiando la posición o el tamaño de un objeto.
A continuación, puede crear la clase de mapa Map , en la que se construirá la cuadrícula, y heredarla de la clase Obstacle .

Mapa
 public sealed class Map : Obstacle { } 


La clase Map también rastreará todos los cambios en el mapa para reconstruir la cuadrícula si es necesario.

Para hacer esto, complete la clase base Obstáculo con todas las variables y métodos necesarios para realizar un seguimiento de los cambios en los objetos.

Obstáculo
 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;} } } 


Aquí, la variable de representación tendrá una referencia al componente SpriteRenderer , y las variables tempSize y tempPos se usarán para rastrear los cambios en el tamaño y la posición del objeto.

El método virtual Awake se usará para inicializar las variables, y el método virtual CheckChanges hará un seguimiento de los cambios actuales en el tamaño y la posición del objeto y devolverá un resultado booleano .

Por ahora, dejemos la secuencia de comandos Obstacle y pasemos a la secuencia de comandos Map map en sí, donde también la completaremos con los parámetros necesarios para el trabajo.

Mapa
 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 indicará el tamaño de las celdas en el mapa, aquí he limitado su tamaño de 0.1 a 1 para que las celdas en la cuadrícula no sean demasiado pequeñas, sino también demasiado grandes. La variable de desplazamiento se usará para sangrar el mapa al construir la cuadrícula para que la cuadrícula no se construya a lo largo de los bordes del mapa.

Como ahora hay dos nuevas variables en el mapa, resulta que sus cambios también deberán ser rastreados. Para hacer esto, agregue un par de variables y sobrecargue el método CheckChanges en la clase Map .

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


Listo Ahora puede crear un sprite de mapa en el escenario y lanzar un script de Mapa sobre él.

imagen

Haremos lo mismo con un obstáculo: crear un sprite simple en el escenario y lanzar el guión de Obstáculo sobre él.

imagen

Ahora tenemos objetos de mapa y obstáculos en el escenario.

La secuencia de comandos del Mapa será responsable de rastrear todos los cambios en el mapa, donde en el método de Actualización revisaremos cada cuadro para ver los cambios.

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


Por lo tanto, en el método UpdateChanges, el mapa solo rastreará sus cambios hasta ahora. Incluso puedes comenzar el juego ahora e intentar cambiar el tamaño del mapa o compensar el desplazamiento para asegurarte de que todos los cambios sean rastreados.

Ahora necesita rastrear de alguna manera los cambios de los obstáculos en el mapa. Para hacer esto, colocaremos cada obstáculo en una lista en el mapa, que a su vez actualizará cada cuadro en el método Actualizar .

En la clase Mapa , cree una lista de todos los obstáculos posibles en el mapa y un par de métodos estáticos para registrarlos.

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


En el método estático RegisterObstacle , registraremos un nuevo obstáculo Obstáculo en el mapa y lo agregaremos a la lista, pero primero es importante tener en cuenta que el mapa en sí también se hereda de la clase Obstáculo y, por lo tanto, debemos verificar si estamos tratando de registrar la tarjeta como un obstáculo.

El método estático UnregisterObstacle , por el contrario, elimina el obstáculo del mapa y lo elimina de la lista cuando permitimos que sea destruido.

Al mismo tiempo, cada vez que agregamos o eliminamos un obstáculo del mapa, es necesario recrear el mapa en sí, por lo que después de ejecutar estos métodos estáticos, establezca la variable requireRebuild en true .

Además, para tener fácil acceso al script del Mapa desde cualquier script, creé una propiedad de Instancia estática que me devolverá esta misma instancia del Mapa .

Ahora, volvamos a la secuencia de comandos Obstáculo donde registraremos un obstáculo en el mapa. Para hacer esto, agregue un par de métodos OnEnable y OnDisable .

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


Cada vez que creamos un nuevo obstáculo mientras jugamos en el mapa, se registrará automáticamente en el método OnEnable , donde se tendrá en cuenta al construir una nueva cuadrícula y nos eliminaremos del mapa en el método OnDisable cuando se destruya o desactive.

Solo queda realizar un seguimiento de los cambios de los obstáculos en el script Map en el método CheckChanges sobrecargado.

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


Ahora tenemos un mapa, obstáculos: en general, todo lo que necesita para construir una cuadrícula y ahora puede pasar a lo más importante.

Malla


La cuadrícula, en su forma más simple, es una matriz bidimensional de puntos. Para construirlo, necesita saber el tamaño del mapa y el tamaño de los puntos en él, después de algunos cálculos obtenemos el número de puntos horizontal y verticalmente, esta es nuestra cuadrícula.

Hay muchas formas de encontrar una ruta en una cuadrícula. Sin embargo, en este artículo, lo principal es entender cómo usar correctamente las capacidades del sistema de tareas, por lo que aquí no consideraré diferentes opciones para encontrar la ruta, sus ventajas y desventajas, pero tomaré la opción de búsqueda más simple A * .

En este caso, todos los puntos de la cuadrícula deben tener, además de la posición, las coordenadas y la propiedad de permeabilidad.

Con la permeabilidad, creo que todo está claro por qué es necesario, pero las coordenadas indicarán el orden del punto en la cuadrícula, estas coordenadas no están vinculadas específicamente a la posición del punto en el espacio. La imagen a continuación muestra una cuadrícula simple que muestra las diferencias de coordenadas desde una posición.

imagen
¿Por qué las coordenadas?
El hecho es que, en la unidad, para indicar la posición de un objeto en el espacio, se utiliza un flotador simple que es muy inexacto y puede ser un número fraccionario o negativo, por lo que será difícil usarlo para implementar una búsqueda de ruta en el mapa. Las coordenadas se hacen en forma de un int claro que siempre será positivo y con el que es mucho más fácil trabajar cuando se buscan puntos vecinos.

Primero, definamos un objeto de punto, esta será una estructura de Nodo simple.

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


Esta estructura contendrá la posición de posición en forma de Vector2 , donde con esta variable dibujaremos un punto en el espacio. La variable de coordenadas de los coords en forma de Vector2Int indicará las coordenadas de un punto en el mapa, y la variable id su número de cuenta numérico usándolo, compararemos diferentes puntos en la cuadrícula y verificaremos la existencia de un punto.

La permeabilidad del punto se indicará en forma de su propiedad booleana , pero como no podemos usar los tipos de datos convertibles en el sistema de tareas, indicaremos su permeabilidad en forma de un número int , para esto utilicé una enumeración simple NodeType , donde: 0 no es un punto transitable, y 1 es pasable.

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


Además, para la conveniencia de trabajar con un punto, sobrecargaré el método Equals para facilitar la comparación de puntos y también complementaré el método de verificación para la existencia de un punto.

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


Como el número de identificación del punto en la cuadrícula comenzará con 1 unidad, comprobaré la existencia del punto como condición de que su identificación sea mayor que 0.

Vaya a la clase Mapa donde prepararemos todo para crear un mapa.
Ya tenemos una verificación para cambiar los parámetros del mapa, ahora necesitamos determinar cómo se llevará a cabo el proceso de construcción de la cuadrícula. Para hacer esto, cree una nueva variable y varios métodos.

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


La propiedad de reconstrucción indicará si el proceso de mallado está en progreso. El método Reconstruir recopilará datos y tareas para construir la cuadrícula, luego el método OnRebuildStart comenzará el proceso de construcción de la cuadrícula y el método OnRebuildFinish recopilará datos de las tareas.

Ahora cambiemos un poco el método UpdateChanges para que se tenga en cuenta la condición de la cuadrícula.

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


Como puede ver ahora en el método UpdateChanges , existe la condición de que, mientras que la construcción de la malla antigua no comienza a construir una nueva, y también en el método Reconstruir , la primera acción verifica si el proceso de mallado ya está en progreso.

Resolución de problemas


Ahora un poco sobre el proceso de construcción de un mapa.
Como usaremos el sistema de tareas y construiremos la cuadrícula en paralelo para construir el mapa, utilicé el tipo de tarea IJobParallelFor , que se ejecutará un cierto número de veces. Para no cargar el proceso de construcción con ninguna tarea separada, utilizaremos el conjunto de tareas empaquetadas en un JobHandle .

Muy a menudo, para construir una cuadrícula, use dos ciclos anidados entre sí para construir, por ejemplo, horizontal y verticalmente. En este ejemplo, también construiremos la cuadrícula primero horizontalmente y luego verticalmente. Para hacer esto, calculamos el número de puntos horizontales y verticales en el método Reconstruir , luego en el método Reconstruir pasamos por el ciclo a lo largo de los puntos verticales, y construiremos los horizontales en paralelo en la tarea. Para imaginar mejor el proceso de construcción, eche un vistazo a la animación a continuación.

Malla
imagen

El número de puntos verticales indicará el número de tareas, a su vez, cada tarea creará puntos solo horizontalmente, después de completar todas las tareas, los puntos se suman en una lista. Es por eso que necesito usar una tarea como IJobParallelFor para pasar el índice del punto en la cuadrícula horizontalmente al método Execute .

Y así tenemos la estructura de puntos, ahora puede crear la estructura de la tarea Trabajo y heredarla de la interfaz IJobParallelFor , todo es simple aquí.

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


Regresamos al método de Reconstrucción de clase de Mapa , donde haremos los cálculos necesarios para la medición de la cuadrícula.

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


En el método Reconstruir , calculamos el tamaño exacto del mapa mapSize , teniendo en cuenta la sangría, luego en verticales escribimos el número de puntos verticalmente, y en horizontales el número de puntos horizontalmente. Si el número de puntos verticales es 0, entonces dejamos de construir el mapa y llamamos al método OnRebuildFinish para completar el proceso. La variable de origen indicará el lugar desde donde comenzaremos a construir la cuadrícula; en el ejemplo, este es el punto inferior izquierdo del mapa.

Ahora puede ir a las tareas mismas y llenarlas con datos.
Durante la construcción de la cuadrícula, la tarea necesitará una matriz NativeArray donde colocaremos los puntos, también dado que tenemos obstáculos en el mapa, también tendremos que pasarlos a la tarea, para esto usaremos otra matriz NativeArray , luego necesitamos el tamaño de los puntos en el problema , la posición inicial desde donde construiremos los puntos, así como las coordenadas iniciales de la serie.

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


Marqué la matriz de puntos como un atributo con WriteOnly, ya que en la tarea solo será necesario " escribir " los puntos recibidos en la matriz, por el contrario, la matriz de límites de obstáculos está marcada con el atributo ReadOnly ya que en la tarea solo " leeremos " datos de esta matriz.

Bueno, por ahora, procedamos al cálculo de los puntos mismos más adelante.

Ahora volvamos a la clase Map , donde denotamos todas las variables involucradas en las tareas.
Aquí, en primer lugar, necesitamos un manejo global de tareas, una serie de obstáculos en forma de NativeArray , una lista de tareas que contendrá todos los puntos recibidos en la cuadrícula y el Diccionario con todas las coordenadas y puntos en el mapa, por lo que sería más conveniente buscarlos más tarde.

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


Ahora, nuevamente, volvemos al método Reconstruir y continuamos construyendo la cuadrícula.
Primero, inicialice la matriz de límites de obstáculos para pasarla a la tarea.

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


Aquí creamos una instancia de NativeArray a través de un nuevo constructor con tres parámetros. Examiné los dos primeros parámetros en un artículo anterior, pero el tercer parámetro nos ayudará a ahorrar un poco de tiempo creando una matriz. El hecho es que escribiremos datos en la matriz inmediatamente después de su creación, lo que significa que no necesitamos asegurarnos de que se borre. Este parámetro es útil para NativeArray, que solo se usará en modo de lectura en la tarea.

Y así, luego llenamos la matriz de límites con datos.

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


Ahora podemos pasar a crear tareas, para esto pasaremos por un ciclo a través de todas las filas verticales de la cuadrícula.

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


Para empezar, en xPos e yPos obtenemos la posición horizontal inicial de la serie.

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


A continuación, creamos una matriz nativa simple donde se colocarán los puntos en la tarea, aquí para la matriz que necesita especificar cuántos puntos se crearán horizontalmente y el tipo de asignación Persistente , porque la tarea puede tomar más de un fotograma.
Después, creamos la instancia de la tarea Job en sí, colocamos las coordenadas iniciales de la serie startCoords , la posición inicial de la serie startPos , el tamaño de los puntos nodeSize , la matriz de límites de obstáculos y, al final, la matriz de puntos en sí.
Solo queda poner la tarea en control y la lista de tareas global.

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


Listo Tenemos una lista de tareas y su identificador común, ahora podemos ejecutar este identificador llamando a su método Completo en el método OnRebuildStart .

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


Dado que la variable de reconstrucción indicará que el proceso de mallado está en marcha, el método UpdateChanges también debe especificar la condición en la que este proceso terminará usando el identificador y su propiedad IsCompleted .

Cambios de actualización
 private void UpdateChanges() { if (this.rebuilding) { print(“  ...”); if (this.handle.IsCompleted) OnRebuildFinish(); } else { if (this.requireRebuild) { print(“  ,   !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } } 


Después de completar las tareas, se llamará al método OnRebuildFinish donde ya recopilaremos los puntos recibidos en una lista general del Diccionario , y lo más importante, para eliminar los recursos ocupados.

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


Primero, borramos el diccionario de nodos de los puntos anteriores, luego usamos el bucle foreach para clasificar todos los puntos que recibimos de las tareas y ponerlos en el diccionario de nodos , donde la clave son las coordenadas (¡ NO la posición !) Del punto, y el valor es el punto en sí. Con la ayuda de este diccionario, será más fácil para nosotros buscar puntos vecinos en el mapa. Después de llenar, borramos la matriz de matriz usando el método Dispose y al final borramos la lista de tareas de trabajo en.

También deberá despejar los límites de obstáculos si se creó anteriormente.

Después de todas estas acciones, obtenemos una lista de todos los puntos en el mapa y ahora puede dibujarlos en el escenario.

Algo como esto
imagen

Para hacer esto, en la clase Map , cree el método OnDrawGizmos donde dibujaremos los puntos.

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


Ahora a través del ciclo dibujamos cada punto.

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


Después de todas estas acciones, nuestro mapa se ve de alguna manera aburrido, para realmente obtener una cuadrícula, necesita que los puntos estén conectados entre sí.

Malla
imagen

Para buscar puntos vecinos, solo necesitamos encontrar el punto deseado por sus coordenadas en 8 direcciones, por lo que en la clase Mapa crearemos una matriz estática simple de direcciones de Direcciones y un método de búsqueda de celdas por sus coordenadas GetNode .

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


El método GetNode devolverá un punto por coordenadas de la lista de nodos , pero debe hacerlo con cuidado, porque si las coordenadas de Vector2Int son incorrectas, se producirá un error, por lo que aquí usamos el bloque de omisión de la excepción try catch , que ayudará a evitar la excepción y no " colgar " toda la aplicación con un error.

A continuación, veremos el ciclo en todas las direcciones e intentaremos encontrar puntos vecinos en el método OnDrawGizmos y, lo más importante, no olvidemos considerar la permeabilidad del punto.

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 


Ahora puedes iniciar el juego de forma segura y ver qué sucedió.

Mapa dinámico
imagen

En este ejemplo, construimos solo el gráfico en sí mediante tareas, pero esto es lo que sucedió después de que atornillé en el sistema el algoritmo A * , que también utiliza el sistema Job para encontrar la ruta, la fuente al final del artículo .

Búsqueda de mapas y rutas
imagen

Por lo tanto, puede utilizar el nuevo sistema de tareas para sus objetivos y crear sistemas interesantes sin mucho esfuerzo.

Como en el artículo anterior, el sistema de tareas se usa sin ECS , pero si usa este sistema junto con ECS , puede lograr resultados simplemente sorprendentes en ganancias de rendimiento. Buena suerte !

Buscador de ruta Fuente del proyecto

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


All Articles