22 enero 2026

Asignación de costes a la navegación 2D por tilemaps en Unity

Portada del artículo

Hace poco vimos cómo se podía hacer en Godot para asignarle costes a los tiles de un tilemap, de manera que se pudiera usar esos costes en los algoritmos de navegación. Unity tiene su manera particular de hacerlo, pero, como en el caso de Godot, dependerá de si queremos utilizar esos costes con un algoritmo personalizado de navegación o con la navegación con navmesh del engine. Vamos a ver cómo se hace en ambos casos.

En este artículo asumiré que has leído mi artículo anterior sobre cómo asignar datos a los tiles en Unity. En este aprovecharemos algunas cosas lo que conté allí.

Costes para algoritmos personalizados de navegación

Cuando usemos algoritmos personalizados, como nuestras propias implementaciones de Dijkstra o A*, querremos acceder al coste de tránsito de una celda determinada. Podríamos optar por asignarle un game object al tile, tal y como vimos en el artículo mencionado antes, y que ese game object tuviese un script con el dato del coste. Pero eso sería excesivamente complejo, ya que tendríamos que asignarle un collider a ese game object para poder detectarlo con un sensor de volumen, al aplicar el sensor a la posición de la celda. No, definitivamente es mucho mejor en este caso la otra aproximación que veíamos en ese artículo: la de crear una clase que herede de Tile, hacer que incluya un campo de coste y utilizarla para dibujar las celdas que tengan un coste de tránsito más elevado. Sería una clase como la siguiente:

Implementación de nuestro tile personalizado
Implementación de nuestro tile personalizado

La clase se limita a tener un campo para poder meter el coste del tile a través del inspector (línea 13).

Para poder consultar desde nuestro código el coste del tile, en la línea 18 hay una propiedad de sólo lectura.

En el fondo, un Tile es una especie de Scriptable Object, por lo que se extiende de una manera muy parecida a como se hacía con estos. Como con los Scriptable Object, podemos decorarlos con el atributo [CreateAssetMenu] (línea 6) para que el editor muestre una entrada de menú desde las que crear instancias de este tile. En este caso lo he creado para que me lo muestre en la ruta "Scriptable Objects > CoutyardTile".

La creación de un tile de este tipo sería similar a la de un Scriptable Object. Pulsaríamos con el botón derecho en la carpeta donde quisiéramos guardar el tile y elegiríamos la opción configurada en el párrafo anterior.

Creación de una instancia del tile personalizado
Creación de una instancia del tile personalizado

Creada la instancia del tile, podremos darle la apariencia que queramos y asignarle un valor al campo del coste. Mi ejemplo es visualmente muy sencillo. A un tile de agua le he dado un coste de 80.

Configuración de un tile de agua
Configuración de un tile de agua

En mi ejemplo he creado cuatro tipos de tiles transitables:

  • Ground: Gris. Es el que no tiene dificultades de tránsito, por lo que su coste es 1.
  • Grass: Verde. Tiene un coste de 2.
  • Sand: Naranja. Coste 4.
  • Water: Azul. Coste 80.

Acuérdate también de asignarle un sprite. De otra manera, por mucho que le asignes un valor al color, el tile no se verá. Yo me he limitado a usar un sprite de color blanco, para que se aplique sobre él el color.

Creados los tiles personalizados, te bastará con poner en modo edición la pestaña del Tile Palette y arrastrar sobre ella los tiles, desde su carpeta.

Mi tile palette
Mi tile palette

Con eso, ya podemos dibujar las zonas "lentas" del escenario. El mío está bordeado de muros infranqueables y tiene muros internos de color negro.

El escenario de mi ejemplo
El escenario de mi ejemplo


Una vez que los costes están asociados a sus tiles, ¿cómo los recuperaríamos? Nos bastaría con un método como el de la siguiente captura.

Método para recuperar el coste asociado a cada tile
Método para recuperar el coste asociado a cada tile

El método supone que existe una variable global walkableTilemap con una referencia al tilemap donde se encuentren dibujados todos los tiles transitables del escenario. Con esa referencia, usamos su método WorldToCell() (línea 319) para convertir la posición global del escenario que queremos analizar a la coordinada del tilemap que se corresponde con dicha posición. Luego, usamos el método GetTile() (línea 320) para recuperar el tile asociado a esas coordenadas. Una vez que tengamos el tile, podemos acceder a su propiedad Cost para devolver su valor (línea 321).

Mesh navigation con zonas lentas

El método anterior es útil si queremos que nuestros agentes se muevan por el tilemap usando nuestro propio algoritmos de navegación. Sin embargo, puede ser que por rendimiento, o sencillez, prefiramos utilizar el sistema de mesh navigation que ya viene con el engine. El problema en Unity es que su sistema de mesh navigation está pensado para 3D y no sirve tal cual para escenarios 2D como los del ejemplo. Afortunadamente, hay un módulo libre llamado NavMeshPlus que solventa este problema para el 2D y se usa prácticamente igual que el mesh navigation nativo de Unity para 3D. Expliqué su instalación y uso en un artículo anterior en el que cubrí la navegación 2D en Unity. A partir de lo dicho en ese articulo, en este explicaremos cómo implementar zonas lentas que sean tenidas en cuenta por el algoritmo de búsqueda de caminos.

Por tanto, partiendo de que te has leído ese artículo, y que has seguido sus indicaciones para generar un un NavMesh, ahora hay que crear un tipo de área transitable por cada tipo de baldosa que tengamos. Para ello, ve a Window > AI > Navigation. La ventana que se abre tiene dos pestañas: Agents y Areas. Como te imaginarás, la que nos interesa es Areas. Allí ya están definidas las áreas por defecto del sistema de navegación de Unity:  Walkable, Not Walkable y Jump. Fíjate que en la columna de la derecha se definen el coste de tránsito de cada una de las áreas. Es aquí donde definiremos los tipos de áreas que vimos en la sección anterior, con sus correspondientes costes.

Configuración de las áreas transitables y sus costes


Definidas las áreas, hay que asociar cada tile al tipo de área al que pertenecen. Si seguiste el artículo de navegación 2D en Unity, habrás incluido un componente NavigationModifier en el mismo GameObject del tilemap donde tuvieras los tiles transitables. Desmárcale el campo Override Area. 

Acto seguido añade un componente NavigationModifierTilemap. En él hay una lista llamada Tile Modifiers. Añade a esa lista un elemento por cada tipo de tile transitable que queras definir. Luego arrastra a cada uno de los elementos el recurso de su respectivo tile, desde su carpeta. De esta manera podrás definir a qué area pertenece cada tile. 

En mi caso, la configuración queda de la siguiente manera:

Asignación de tiles a cada una de las áreas
Asignación de tiles a cada una de las áreas

Ahora hay que regenerar el NavMesh para que incluya los costes de tránsito que acabamos de definir para cada una de las áreas. 

Sin embargo, antes de pulsar Bake en el componente NavigationSurface tienes que asegurarte de cambiar el parámetro Render Meshes. Si lo tienes configurado tal y como lo dejamos en el artículo de navegación 2D en Unity, este parámetro tendrá el valor Physics Colliders, por lo que sólo tendrá en cuenta los objetos con colliders físicos asociados para "recortar" la zona transitable del NavMesh. Tenemos que cambiarlo a Render Meshes. De esta manera usará como referencia los tiles que hemos definido como transitables (que son componentes visuales, renderizables) para dar forma al NavMesh. Una vez cambiado podremos pulsar Bake.

Configuración de mi NavMesh
Configuración de mi NavMesh

Si haces que el editor te muestre el NavMesh, verás que este tiene varios colores para señalar las distintas áreas que ha detectado.

NavMesh generado con diferentes áreas

Ya tienes tu NavMesh, pero si ejecutas el escenario en este punto es probable que tu agente se quede quieto. Para que se mueva tienes que asegurarte de que esté configurado para usar las áreas transitables. Eso se hace configurando el parámetro AreaMask del NavMeshAgent, de manera que contenga todas las zonas por las que se podrá andar.

Configuración de NavMeshAgent
Configuración de NavMeshAgent

En la captura anterior, puedes ver que he configurado el AreaMask para que incluya las áreas Ground, Grass, Water y Sand.

Ahora sí, si ejecutas el juego, tu agente debería ser capaz de moverse por las zonas transitables, eligiendo siempre el camino con menor coste.

Probablemente te des cuenta de que, aunque el agente elija la ruta de menor coste, no disminuirá la velocidad si se ve obligado a atravesar una zona lenta. Para que eso ocurra, tendrás que llamar al método estático en NavMesh.SamplePosition() desde el Update() del agente, para analizar en todo momento la posición donde se encuentre. Ese método devuelve como parámetro de salida un objeto de tipo AI.NavMeshHit que tiene una propiedad mask con el área a la que está asociada ese punto. Lo normal es que un punto sólo esté asociado a una área, pero a pesar de eso la propiedad mask es (como su nombre indica) una máscara de 32 bits. Un bit por cada una de las áreas de navegación que Unity permite que se definan. Te basta averiguar el índice del bit que está marcado a 1 para saber el área de navegación del punto analizado. Con es índice puedes llamar a otro método estático, NavMesh.GetAreaCost() para obtener el coste del área sobre la que estemos y actuar en consecuencia (habitualmente dividiendo la velocidad del agente entre el coste del área).

Conclusión

Con esto, ya hemos revisado las dos opciones que tenemos en Unity para navegar por un escenario construido con un tilemap.

  1. Usar datos de coste asociados a los tiles para hacer una búsqueda de caminos clásica con nuestra implementación de Dijkstra o con la de A* que integra Godot.
  2. Usar mesh navigation con zonas lentas.

Si tus escenarios tienen un tamaño moderado, puedes permitirte la opción 2, si por la razón que fueses prefirieses modelar el escenario con un grafo. 

Sin embargo, en cuanto tu escenario crezca de tamaño preferirás la opción 3. Es la que mejor rendimiento ofrece y la más sencilla de configurar.

14 enero 2026

Asignación de costes a la navegación 2D por tilemaps en Godot

En mi anterior artículo expliqué cómo asignar datos personalizados a los tile de los tilemap con los que podemos construir un escenario 2D. Uno de esos datos puede ser el coste de tránsito por el tile, para ser utilizado por un algoritmo de Dijkstra que implementemos, o por el componente AStar2D ya incluido en Godot, para crear rutas a través del escenario.

Lo anterior sería una manera perfectamente válida de implementar la navegación por nuestro escenario. Pero podría ocurrir que, a pesar de haber utilizados tilemaps para construir el escenario, prefiriésemos recurrir al rendimiento que ofrece el mesh pathfinding de Godot, partiendo de lo que expliqué en mi artículo sobre navegación 2D en Godot. El problema es que en aquel artículo asumimos un escenario que sólo comprendía obstáculos y una zona transitable uniforme, por lo que no explicamos cómo aplicar costes a determinadas áreas de la zona transitable. Vamos a ver cómo hacerlo.

Para empezar, hay que aclarar que el interfaz de Godot mezcla los distintos métodos de navegación, empleando también términos similares, por lo que es fácil confundirse y sentirse perdido. 

Navegación integrada en los TileMaps

En mi opinión, la primera causa de confusión fue que cuando entramos en la configuración de un TileSet, por ejemplo expandiendo el recurso Tile Set del Tilemap que lo emplea, la categoría de Navigation Layers no tiene nada que ver con la navegación con mesh pathfinding. Estos navigation layers se asignan al TileSet aquí, pero se crean en Project > Project Settings... > General > Layer Names > 2D Navigation. 

Creación de navigation layers dentro de los Project Settings
Creación de navigation layers dentro de los Project Settings



Asignación de navigation layers al recurso de un TileSet en la configuración de un TileMapLayer


Una vez asignados al TileSet, a nivel de recurso, se puede definir la presencia de cada Tile en cada navigation layer "dibujando" un polígono en el apartado Navigation de la pestaña TileSet, de manera similar a como se definiría la capa física del Tile.

Asignación de un tile a un navigation layer
Asignación de un tile a un navigation layer

Hecho eso, tendríamos que activar la navegación en el TileMap, marcando su opción Navigation.

Opción Navigation, dentro de la configuración del TileMap

Con eso ya tendríamos todo para que un agente recorriese el escenario, usando una API similar a la que vimos en el mencionado artículo sobre navegación 2D en Godot. La diferencia respecto a aquel artículo sería que no habría que poner un NavigationRegion2D por encima de los TileMapLayer, y que a nivel de agente el nodo a utilizar no sería un MeshNavigationAgent2D, sino un NavigationAgent2D a secas.

Este método es más rápido y directo, pero tiene dos inconvenientes graves:

  1. Tiene un rendimiento muy malo, por lo que sólo es viable para escenarios pequeños.
  2. No permite establecer áreas de coste diferenciados en la zona transitable, por lo que sólo tiene sentido si tu zona transitable es uniforme. 

En realidad la utilidad de asignar navigation layers aquí es para definir los tiles por los que pueden transitar los diferentes agentes, no para asignar costes. Por ejemplo, el agente de un murciélago navegará por los tiles del cielo, mientras que el agente del protagonista se moverá por los tiles del suelo.

Estos inconvenientes son lo que me llevaron a enfocar el artículo anterior sobre navegación 2D en Godot al uso de mesh pathfinding. Como vamos a ver, este método resuelve los inconvenientes anteriores, pero es algo más complicado de configurar.

Mesh navigation con zonas lentas

Para explicar este método, vamos a partir de lo que ya conté en el artículo sobre navegación 2D en Godot que mencionaba más arriba. Así que si no te lo has leído aún, este sería el momento. En ese artículo dimos forma a un escenario que sólo tenía tres tipos de elementos: muros y obstáculos, ambos tiles con colliders configurados en sus correspondientes capas físicas, y baldosas de suelo, las cuales carecían de collider. También definimos un NavigationRegion2D, del cual colgaban los nodos de los TileMapLayers, y que abarcaba todo el recinto intramuros del escenario. Partiendo de ese escenario, en al de este artículo incorporaremos baldosas que serán transitables pero con un coste superior de lo normal. Sólo como ejemplo, esas baldosas "lentas" serían de hierba (verdes), arena (naranjas) y agua (azules).

Fíjate que, cuando pulsamos "Bake AnimationPolygon", lo que hace NavigationRegion2D es superponer un polígono sobre el recinto que le hayamos dicho y recortar las zonas donde detecte un collider. El polígono resultante será lo que le de forma al NavigationRegion2D. Los collider que tendrá en cuenta en el proceso son los incluidos en las capas físicas marcadas en el campo Parsed Collision Mask de la configuración del NavigationPolygon.

Configuración de las capas físicas a tener en cuenta para darle forma a un NavigationRegion2D
Configuración de las capas físicas a tener en cuenta para darle forma a un NavigationRegion2D

Hay que tener cuidado porque la configuración del NavigationRegion2D tiene un campo de Navigation Layers, por lo que es fácil creer que al darle forma al NavigationPolygon sólo se tendrán en cuenta los tiles asociados a determinados Navigation Layer. Sin embargo, no es así, nada más lejos de la realidad. A la hora de darle forma al NavigationPolygon sólo se tendrán en cuenta los collider asociados a los tiles, independientemente del navigation layer al que estén asociados estos. De hecho, si utilizas este método probablemente no tengas que asociar ningún navigation layer a los TileMapLayers. Eso sólo tenía sentido con la navegación integrada en los TileMap.

Configuración del NavigationRegion2D

Entonces, ¿para qué sirve el campo Navigation Layers de la configuración del NavigationRegion2D? En realidad, no es un parámetro de entrada, sino de salida. Sirve para asociar el NavigationPolygon generado a un navigation layer concreto. Como vamos a ver, puedes tener varios NavigationRegion2D asociados a un mismo navigation layer. Cuando un agente transite por un navigation layer, el algoritmo de búsqueda conformará un mapa que será la suma de todos los NavigationRegion2D asociados a ese navigation layer.

Fíjate también que en la configuración de un NavigationRegion2D hay un campo de Travel Cost. Este parámetro es un multiplicador de la distancia recorrida. Para calcular el coste de transitar por esa región, el algoritmo de búsqueda de caminos entenderá que cada unidad recorrida será equivalente al multiplicador introducido en Travel Cost.

Por tanto, aunque el sistema de mesh navigation de Godot no permita asignarle costes a tiles específicos, si puede poner costes por región, y puede montar un mapa uniendo varias regiones. Partiendo de eso, lo que vamos a hacer es crear una región que abarque los tiles con el coste 100 de tránsito (el coste por defecto), y otra región por cada zona de tiles "lentos que tengamos en el mapa". A esas últimas regiones les pondremos un Travel Cost acorde a los tiles recogidos en ellas. Asociaremos todas esa regiones al mismo navigation layer para que conformen un mapa unificado.

La pega, es que eso supone asociar colliders a las baldosas transitables. Sólo para ser detectadas al generar el NavigationPolygon. En el caso de mi ejemplo, eso supone crear una capa física para cada uno de los tipos de navegación. Como con el resto de las capas físicas, se crean en Project > Project Settings... > General > Layer Names > 2D Physics

Las capas 6, 7, 8, 9 son para definir los costes de navegación

Se puede ver en la captura que he creado cuatro capas físicas (NavGround, NavGrass, NavSand y NavWater) para asociarlas a sus respectivas baldosas. No queremos que nuestro agentes choquen esas baldosas. Nos bastará asegurarnos de no marcar esas capas en las máscaras de colisión de nuestros agentes.

Creadas las capas físicas, hay que asociarlas al TileMapLayer donde hayamos colocado las baldosas lentas. Para ello añadiremos cuatro elementos al campo Physics Layers del recurso TileSet del TileMapLayer. Añadiremos un elemento por cada capa física recién creada y nos aseguraremos de que su Collision Layer apunte a los números asignados a las nuevas capas. Los Collision Mask de los nuevos elementos pueden dejarse en blanco.

Con las capas físicas añadidas al TileMapLayer, ya podremos verlas en la pestaña de TileSet para crear polígonos que definan los collider asociados al tile en esa capa física. Por ejemplo, teniendo en cuenta que la capa NavWater ha quedado en el elemento de índice 6 de los Physics Layers asociados al TileMapLayer, la configuración del collider de la baldosa de agua tal y como puede verse en la captura.

Configuración del collider de la baldosa de agua
Configuración del collider de la baldosa de agua

Tendremos que repetir la misma operación en el resto de baldosas, asegurándonos de definir los collider en sus respectivas capas físicas (en mi caso NavGround, NavGrass, NavSand y NavWater).

Ahora que podemos diferenciar cada tipo de baldosa, en función de la capa física de sus respectivos collider, podemos definir una NavigationRegion2D por cada tipo de baldosa transitable.

Una región por cada baldosa transitable
Una región por cada baldosa transitable

En cada una de esas regiones, configuraremos su respectivo NavigationPolygon para que en su Parsed Collision Mask tenga en cuenta todas las capas físicas de los obstáculos y resto de baldosas, salvo la de la baldosa que da nombre a la región. Sólo entonces podremos regenerar el NavigationPolygon. Por ejemplo, la configuración para la región de la hierba (GrassNavigationRegión2D) sería la de la captura.

Configuración para generar el NavigationPolygon de la región de la hierba.

Como los collider de las capas físicas que marquemos en Parsed Collision Mask recortarán el NavigationPolygon resultante, la única capa que no hemos marcado es precisamente la de NavGrass.

Acuérdate también de fijar el radio de tu agente, para evitar que se roce con las paredes al desplazarse. En el caso de este ejemplo, lo he fijado en 50 píxeles, tal y como se puede ver en la captura anterior.

En mi caso, dado que sólo tengo un tipo de agente, todas las NavigationRegion2D tienen el campo de Navigation Layers fijado a su valor por defecto (1). Como ya he comentado, eso hará que todas las regiones se sumen para conformar un único mapa.

Una vez generado el nuevo NavigationPolygon de cada región, será un buen momento de fijar el Travel Cost de cada una al coste que queramos que tenga transitar por ella. Dado que en mi ejemplo mido las distancias en píxeles, las baldosas normales de suelo tienen un coste de 100 (su anchura en píxeles), mientras que a las de agua le he fijado un coste de 8.000. 

Llegados a este punto, la suma de todas las regiones transitables como en la siguiente captura.

Suma de las diferentes regiones transitables
Suma de las diferentes regiones transitables

Sin duda te darás cuenta de un problema evidente. Las regiones "lentas" no se unen con la región transitable con el coste por defecto. Se debe a que en el proceso de formación del NavigationPolygon, no sólo se recortan las formas de los collider de las capas físicas, sino que también se recorta una distancia equivalente al radio del agente. Se hace para evitar que el agente se acerque tanto a una pared que roce con ella. Podríamos reducir el radio del agente a cero, y con eso las regiones transitables se tocarían, pero entonces los agentes rozarían las paredes de una manera muy poco convincente.

Afortunadamente, hay una manera de resolverlo. Vía código, podemos decirle al sistema de navegación que suelde los laterales de regiones de navegación contiguas que se encuentren a una distancia inferior a un determinado umbral. He situado dicho código en el script que hay en el nodo del que cuelgan las diferentes NavigationRegion2D (puedes ver cual es en la captura de la jerarquía que puse hace unos párrafos).

Código para fusionar los laterales de regiones transitables cercanas
Código para fusionar los laterales de regiones transitables cercanas

Si nos fijamos en cómo han quedado las regiones, veremos que la distancia máxima entre dos contiguas es del doble del radio del agente. Dado que en la configuración de los NavigationPolygon metí un radio de 50 pixeles, en este script he fijado 100 como umbral (línea 13).

En la línea 26, saco el identificador del mapa de navegación activo. Es el que contiene las regiones que hemos generado.

Y ahora viene la magia: gracias al método estático MapSetEdgeConnectionMargin() de la línea 26, se podrá pasar entre las regiones transitables cuyas aristas contiguas estén a menos de 100 píxeles de distancia.

Como el script es además un [Tool], el editor de Godot refleja el cambio inmediatamente.

Regiones soldadas gracias a nuestro script
Regiones soldadas gracias a nuestro script

Fíjate en las marcas rosas con las que el editor ha marcado los lados de las regiones en las que ha aplicado soldadoras con otra contigua lo suficientemente cercana.

Al probar un ejemplo similar a este, verás que funciona si tu agente se desplaza evitando las zonas de mayor coste de tránsito, y entrando en ellas sólo si no tiene más remedio o si pones el objetivo explícitamente en el interior de una de ellas.

Otra cosa que observarás es que el agente no aminorará la velocidad cuando atraviese zonas de mayor coste. Eso es lógico porque debes ser tú quien lo implemente en función de cómo quieres que le afecte a tu agente el coste de una una zona. Lo habitual es que el mayor coste de una zona se refleje en una reducción de la velocidad, pero también podría ser que no quisieras que afectase a la velocidad sino al consumo de combustible o a la energía de tu agente. En todo caso, para averiguar el coste del NavigationRegion2D que esté travesando el agente no tienes más que llamar a NavigationServer2D.MapGetClosestPointOwner() para obtener el identificador de la region sobre la que se encuentre el agente en ese momento. Acto seguido, no tendrías más que usar ese identificador para que NavigationServer2D.RegionGetTravelCost() te diese el coste de tránsito de la región. Con ese dato ya es cosa tuya reflejar el mayor o manor impacto de viajar por una zona determinada.

Conclusión

Entre el anterior artículo y este hemos revisado las diferentes opciones que hay para navegar por un escenarios construido con un tilemap 2D:

  1. Usar datos de coste asociados a los tiles para hacer una búsqueda de caminos clásica con nuestra implementación de Dijkstra o con la de A* que integra Godot.
  2. Usar la navegación integrada con los tilemaps.
  3. Usar mesh navigation con zonas lentas.

Si tienes claro que vas a necesitar zonas de tránsito lentas, ya puede tachar la opción 2. En ese caso. la navegación integrada con los tilemaps no te servirá. A partir de ahí, si tus escenarios tienen un tamaño moderado, empezaría intentando la aproximación de la opción 2 y sólo pasaría a la 3 si el rendimiento de la otra fuese insuficiente. Como has podido ver, la opción 3 es la más potente, pero también más engorrosa de configurar y mantener.