No me voy a complicar la vida, así que voy a replicar la estructura de aquel artículo, pero adaptando las instrucciones a Unity.
Recuerda que la premisa de partida es que queremos tener personajes NPC que sean capaces de orientarse por el escenario para poder ir de un punto a otro de este. Vamos a empezar por ver cómo crear un escenario que pueda ser procesado por el sistema de navegación.
Creación de un escenario navegable
Como escenario, nos podría vale cualquier conjunto de sprites, siempre que tuvieran colliders asociados para definir cuáles suponen un obstáculo al NPC. Como eso no tendría mucho misterio, vamos a optar por un caso algo más complejo, pero también bastante habitual en un juego 2D. Vamos a suponer que nuestro escenario está construido a base de tilemaps. El uso de tilemaps, bien merece su propio artículo y los encontrarás a raudales en Internet, así que aquí voy a suponer que ya sabes utilizarlos.
En nuestro ejemplo vamos a suponer un escenario construido con tres niveles de tilemaps: uno para los muros perimetrales del escenario (Tilemap Wall), otro para los obstáculos internos del escenario (Tilemap Obstacles) y otro para el Tilemap del suelo (TilemapGround).
Jerarquía de tilemaps del ejemplo |
Courtyard es el nodo padre de los tilemaps y el que contiene el componente Grid. Con la cuadrícula del tilemap.
Tilemap Ground sólo contiene los componentes visuales Tilemap y TilemapRenderer para poder mostrar los tiles del suelo. Los tilemaps Wall y Obstacles también tienen esos componentes, pero además incorporan dos componentes adicionales: un Tilemap Collider 2D y un Composite Collider 2D. El componente Tilemap Collider 2D sirve para tener en cuenta el collider de cada uno de los tiles. Dicho collider se define en el sprite editor, para cada uno de los sprites utilizados como tiles. El problema de Tilemap Collider 2D es que cuenta el collider de cada tile de manera individual, lo cual es muy poco eficiente por la cantidad de tiles que llega a acumular cualquier escenario basado en tilemaps. Por esa razón, es muy habitual acompañar al componente Tilemap Collider 2D de otro componente llamado Composite Collider 2D. Este último componente se encarga de fundir todos los collider de los tiles, generando un único collider conjunto que es mucho más ligero de manipular por el engine.
Cuando uses estos componentes en tus tilemaps te aconsejo dos cosas:
- Fija el atributo "Composite Operation" del Tilemap Collider 2D al valor Merge. Eso le dirá al componente Composite Collider 2D que la operación que tiene que hacer es juntar todos los colliders individuales en uno solo.
- En el Composite Collider 2D, yo fijaría el atributo "Geometry Type" al valor Polygons. Si lo dejas en el valor por defecto, Outlines, el collider generado estará hueco, es decir sólo tendrá bordes, por lo que algunas operaciones de detección de colliders podrían fallar, tal y como expliqué en otro artículo anterior.
Creación de un NavMesh 2D
Un NavMesh es un componente que analiza el escenario y genera un grafo basado en él. Este grafo es el que usará el algoritmo de búsqueda de caminos para guiar al NPC. Crear un NavMesh en Unity 3D es muy sencillo.
Opción para añadir un repositorio de git al package manager de Unity. |
Una vez que hayas instalado NavMeshPlus, tienes que seguir los siguientes pasos:
- Crear un GameObject vacío en la escena. No deberá depender de ningún otro GameObject.
- Añadir un componente Navigation Surface al GameObject anterior. Asegúrate de usar el componente de NavMeshPlus y no el nativo de Unity. Mi consejo es que te fijes en el icono identificativos de los componentes que muestro en las capturas, y que te asegures de que los componentes que uses tengan los mismo iconos.
- Hay que añadir también el componente Navigation CollectSources2d. En ese mismo componente hay que pulsar el botón "Rotate Surface to XY"; por eso es importante que estos componentes se instalen en un GameObject vacío que no depende de ningún otro. Si lo has hecho bien, parecerá que no pasa nada. En mi caso, me equivoqué y añadí los componentes al GameObject Courtyard que mencionaba antes, y al pulsar el botón se me giró todo el escenario. Así que mucho ojo.
- Luego hay que añadir un componente Navigation Modifier a cada uno de los elementos del escenario. En mi caso se lo añadí a cada uno de los GameObjects de los tilemaps que veíamos en la captura con la jerarquía de tilemaps del ejemplo. Estos componentes nos servirán para discriminar qué tilemaps definen áreas que se puede recorrer y qué tilemaps definen obstáculos.
- Por último, en el componente Navigation Surface ya podremos pulsar el botón "Bake" para generar el el NavMesh.
Vamos a examinar cada uno de los pasos anteriores con algo más de detalle.
El GameObject donde he colocado los dos componentes anteriores cuelga directamente de la raíz de la jerarquía. No le he dado muchas vueltas y lo he llamado NavMesh2D. En la captura siguiente puedes ver los componentes que incluye y su configuración.
Configuración de los componentes principales de NavMeshPlus |
Como puedes ver en la figura anterior, la principal utilidad del componente NavigationSurface es definir qué capas vamos a tener en cuenta para construir nuestro NavMesh ("Include Layers"). Supongo que, si tienes capas muy cargadas, te puede interesar limitar el parámetro "Include Layers" sólo a las capas donde haya elementos del escenario. En mi caso, el escenario era tan sencillo que aun incluyendo todas las capas no he notado ralentización a la hora de crear el NavMesh.
Otra personalización que he hecho es fijar el parámetro "Use Geometry" a "Physics Colliders". Este valor presenta mejor rendimiento cuando usas tilemaps ya que se usan unas formas geométricas más sencillas para representar el escenario. La opción "Render Meshes" permite crear un NavMesh mucho más detallado, pero menos optimizado, sobre todo al usar tilemaps.
Si te estás preguntando cómo se modelan las dimensiones físicas del agente de navegación (su radio y altura, por ejemplo), aunque se muestran en la parte superior del componente "Navigation Surface", no se configuran ahí sino en la pestaña Navigation, que también es visible en la captura anterior. Si no la vieses en tu editor, la puedes abrir en Window --> AI --> Navigation.
Pestaña Navigation |
Por último, los componentes Navigation Modifier nos permiten distinguir los tilemaps que contienen obstáculos de los tilemaps que contienen zonas deambulables. Para ello, tenemos que marcar el check de "Override Area" y luego definir el tipo de zona que contiene este tilemap. Por ejemplo, los GameObject de los tilemaps Wall y Obstacles cuentan con el componente Navigation Modifier de la siguiente captura:
![]() |
Navigation Modifier aplicado a los tilemaps con obstáculos |
Una vez creado el NavMesh 2D, nuestros NPC deben ser capaces de leerlo.
Consulta al NavMesh para obtener una ruta entre dos puntos |
En la captura anterior he puesto un ejemplo de cómo realizar estas consultas.
- Punto de partida: Generalmente es la posición actual del NPC, por eso yo le he pasado directamente transform.posicion.
- Punto de destino: En este caso, le he pasado una variable global del NPC donde se encuentra l ubicación de su objetivo.
- Un filtro de áreas del NavMesh: En casos complejos, puedes tener tu NavMesh dividido en áreas. Este bitmask te permite definir a qué áreas quieres restringir la consulta. En un caso sencillo como este, lo normal es pasarle un NavMesh.AllAreas para que las tenga en cuenta todas.
- Una variable de salida del tipo AI.NavMeshPath: es la variable en la que se depositará la ruta resultante hasta el punto de destino. Yo le he pasado una variable privada global del NPC.
Una variable de tipo AI.NavMeshPath cuenta con el campo "corners" donde guarda un array con las localizas de las diferentes escalas de la ruta. Esas localizaciones son tridimensionales (Vector3), mientras que en mi proyecto trabajo con puntos bidimensionales (Vector2), esa es la razón por la que en el método UpdatePathToTarget() recorro todos los puntos del campo "corners" (línea 111) y los voy convirtiendo a elementos de un array de Vector2 (línea 113). Ese array es el que luego utilizo para dirigir mis componentes de movimiento a cada una de las escalas de la ruta.
Conclusión
Listo, con esto ya tienes todo lo que necesitas para hacer que tus NPC se muevan de manera inteligente por el escenario, orientándose hasta alcanzar el objetivo. A alto nivel es cierto que los conceptos son muy similares entre Godot y Unity, pero el demonio está en los detalles. A la hora de bajar al nivel de la implementación te encontrarás los matices y diferencias que hemos analizado en este artículo, pero con las indicaciones que te he dado el resultado que obtengas en Unity y en Godot debería ser similar.