En otro artículo expliqué cómo usar el nodo NavigationAgent2D de Godot para obtener la ruta para llegar a cualquier destino, atravesando un escenario. En aquel caso, la ruta obtenida permitía evitar el choque con los obstáculos estáticos del escenario. Sin embargo, en aquel artículo no vimos cómo evitar el choque con otros agentes. Dado que los agentes son dinámicos, no cabe la preparación previa de un NavigationPolygon de navegación, como hacíamos con los elementos estáticos. Aunque, el mismo nodo NavigationAgent2D permite gestionar también la prevención de colisiones con otros agentes, lo hace siguiendo mecanismos independientes a los usados para elaborar las rutas para desplazarse por el escenario.
En el caso de los escenarios estáticos, NavigationAgent2D creaba un grafo basado en el NavigationPolygon generado a partir del escenario estático. Sobre ese grafo ejecutaba el algoritmo A* para encontrar la ruta hasta el destino.
Para evitar la colisión con otros agentes, NavigationAgent2D mantiene una simulación en la que ubica a los distintos agentes del juego (aquellos objetos con un nodo NavigationAgent2D) y utiliza sus posiciones, velocidades y radios para prever cuándo hay riesgo de que se produzca una colisión. Esto lo hace utilizando un algoritmo denominado Reciprocal Velocity Obstacle (RVO). Para que el nodo NavigationAgent2D sepa a dónde nos gustaría ir, debemos pasarle el vector de velocidad ideal (derecho a nuestro siguiente objetivo). Con esa información, el nodo hará sus cálculos y nos recomendará el vector de velocidad más cercano al ideal que nos garantiza no chocar con ninguno de los agentes cercanos. Nos corresponderá a nosotros utilizar ese vector como mejor veamos para mover al agente. Al igual que pasaba con la búsqueda de rutas, el nodo NavigationAgent2D es pasivo. Facilita información, pero no mueve al agente, a diferencia del componente equivalente en Unity que sí que mueve por defecto al agente que lo porta.
Al ser pasivo, el nodo puede estar en cualquier punto de la arquitectura de un objeto. Como pasa con el resto de los nodos de Godot, podemos adosarle un script que herede de él y extienda su funcionalidad. A la hora de configurar su uso para prevenir las colisiones con otros agentes, tenemos que centrarnos en la sección Avoidance del nodo.
| Configuración de la prevención de colisiones con otros agentes en el nodo NavigationAgent2D |
Para empezar, la simulación para prevenir las colisiones se activa con el check que hay en el título de la sección. Si no está marcado, el nodo se limitará a trabajar en la resolución de rutas.
Una vez marcado, se mostrarán el resto de las opciones de la captura. La primera, Radius, sirve para definir el tamaño del agente. Como mínimo, deberías fijarlo al radio del collider de tu agente, pero no está de más añadir algo más para evitar que los agentes rocen entre sí. Ten en cuenta que este parámetro sólo sirve para la prevención de colisiones con otros agentes y no para la búsqueda de rutas. En ese último caso, el radio que se utiliza es el que aparece en la sección Agents del NavigationPolygon.
La opción Neighbor Distance define el alcance de nuestra detección de otros agentes. Cualquier agente que esté a más distancia no será tenido en cuenta por el algoritmo.
Como te puedes imaginar, Max Neighbors nos permite fijar la cantidad máxima de agentes que se tendrán en cuenta en los cálculos. Al final, es una manera de fijar la carga máxima del algoritmo en el rendimiento del juego.
La rapidez de reacción del agente se configura con el parámetro Time Horizon Agents. En realidad, se trata del tiempos de colisión mínimo a partir del cual el algoritmo empieza a calcular vectores de evasión. Si el algoritmo calcula que la colisión con otro agente tardará en llegar más de lo que fija este parámetro, descartará al agente como fuente de peligro. Si reducimos mucho este parámetro, el agente esperará mucho más antes de iniciar las maniobras evasivas. De hecho, si nos pasamos reduciendo este parámetro, puede que empiece a esquivar tan tarde que acabe estrellándose. Por contra, si ampliamos demasiado este parámetro, el agente empezará a condicionar su velocidad demasiado pronto y empezará a esquivar exageradamente antes.
Como decíamos, el algoritmo sólo tiene en cuenta en su simulación aquellos objetos móviles dotados del nodo NavigationAgent2D. Pero también tiene en cuenta aquellos dotados del nodo NavigationObstacle2D, a los cuales considera objetos inmóviles. A este último caso es al que se aplica el parámetro Time Horizon Obstacles, con las mismas consecuencias que tenía Time Horizon Agents para los objetos móviles.
El parámetro Max Speed define la velocidad máxima del agente. El vector de evasión nunca tendrá una magnitud superior a este valor.
Avoidance Layers, funciona como una capa de colisiones físicas. En este caso sirve para definir a qué capa de evasión pertenece el agente, mientras que el parámetro Avoidance Mask sirve para configurar las capas de los agentes a los que hay que evitar.
Por último, el parámetro Avoidance Priority sirve para configurar la preferencia de paso de este agente frente a otros. Este agente no intentará evitar a otros agentes con menos prioridad, porque estos forzarán aún más sus maniobras de evasión para dejarle pasar.
Con todo lo anterior, el nodo quedará preparado que realizar sus cálculos. Vamos a ver cómo interactuar con él desde el código para darle los datos que necesita y recuperar sus resultados.
Lo primero que hay que tener en cuenta es que el nodo no calcula el vector de evasión inmediatamente. Una vez que le pasamos el vector de velocidad que nos gustaría adoptar, necesita un tiempo para calcular el vector más cercano a él que nos ahorra un choque con otro agente. En el fragor del juego puede que ese tiempo parezca instantáneo, pero en la lógica de tu código ya te advierto que el resultado no estará disponible en el mismo frame. Por eso, una vez que el nodo acaba el cálculo del vector de evasión, emite la señal VelocityComputed. Tendremos que suscribir un callback a esa señal para recoger el vector de evasión calculado. Lo normal será hacer esa suscripción en el _Ready().
| Suscripción a la señal VelocityComputed |
La captura anterior es en Godot C#, pero para GDScript es muy similar. La suscripción se produce en la línea 192, en la que se asocia el método OnAvoidVectorComputed a la señal VelocityComputed.
En mi caso, el citado callback es muy sencillo y se limita a recoger en una variable globas el resultado incluido en la señal.
| Recogida del vector de evasión calculado |
Ese vector será el que pasemos a nuestro mecanismos de movimiento para avanzar hacia nuestro destino mientras evitamos chocar con otros agentes.
Para que ese vector se mantenga actualizado, tendremos que asegurarnos de facilitarle, en cada frame físico (_PhysicsProcess()) el vector de aproximación ideal a nuestro objetivo.
| Cómo pasarle al nodo la velocidad de aproximación ideal al destino |
Según la documentación de Godot sobre este nodo, eso es todo lo que tenemos que hacer para desplazar a nuestro agente por un escenario poblado de otros agentes. Sin embargo, el algoritmo RVO empleado por este nodo tiene un punto débil: el caso en el que dos agentes van de frente, el uno contra el otro. En ese caso, el vector de evasión es justamente el opuesto al vector de dirección por lo que ambos se anulan. O bien el agente se queda parado o bien la situación degenera en que uno acaba persiguiendo al otro. La solución que le he encontrado a ese problema es detectarlo con un dot-product y aplicar en ese caso un vector de evasión perpendicular al vector de dirección.
| Resolución del problema de los agentes chocando de frente |
Dado que NavigationAgent2D devuelve un vector que aúna la dirección ideal que queremos tomar y el vector de evasión, si sólo queremos la componente del vector del vector de evasión tendremos que restar la de la dirección ideal. Es lo que hago en la línea 333.
Para ver si ese vector es opuesto al de dirección, hacemos un dot-product en la línea 334 con los valores normalizados del vector de evasión y el de la velocidad actual del agente. Si está muy cercano a 1 (en valor absoluto) es que son vectores opuestos. En ese caso, y sólo en ese caso, sustituyo el vector de evasión por una versión perpendicular del vector de velocidad actual. Mantenido durante el suficiente tiempo (de ahí el temporizador, activado en la línea 348) un vector de evasión así debería generar, en el peor de los casos, una situación de persecución de la que el agente delantero acabaría zafándose por un lateral, para acabar dirigiéndose a su objetivo original. No es una maniobra bonita, pero funciona en los contados casos en los que nos veamos con dos agentes dirigiéndose el uno contra el otro.
Con esto ya tienes todo lo que necesitas para evitar que tus agentes colisionen entre sí cuando se muevan por el escenario. En todo caso, recuerda que todos los agentes deben contar con un nodo NavigationAgent2D o no serán incluidos en la simulación para evitar colisiones.
