14 junio 2025

¿Qué archivos debemos incluir en Git para un proyecto de Unity?

Imagen de portada
Si aún no tienes tu proyecto de Unity a buen recaudo, en un repositorio de Git como GitHub, deberías hacerlo cuanto antes. 

Las principales razones para añadir un repositorio de versiones a tu rutina de trabajo son:

  • Tener tu proyecto en un repositorio remoto (como GitHub, GitLab o Bitbucket) asegura que no pierdas tu trabajo si algo le pasa a tu ordenador.
  • Git te permite rastrear cambios en tu proyecto, revertir a versiones anteriores si algo sale mal y entender qué modificaciones se hicieron y cuándo.
  • Si trabajas en equipo, Git facilita que múltiples personas trabajen en el mismo proyecto sin conflictos, usando ramas y merges.
  • Puedes usar ramas para experimentar con nuevas funciones o correcciones sin afectar la versión principal, manteniendo el proyecto ordenado.
  • Muchas plataformas de CI/CD y herramientas de desarrollo se integran con Git, lo que agiliza flujos de trabajo como pruebas automáticas o despliegues.
  • Usar Git es un estándar en la industria, y dominarlo te prepara para trabajar en entornos profesionales.

Sin embargo, no debes subir todos tus ficheros a un repositorio Git. Por ejemplo, no tiene sentido subir los compilados porque cualquiera los puede generar si cuentan con el código fuente. Subir los compilados sólo incrementaría innecesariamente el tamaño del repositorio, haciéndolo más caro de mantener e incrementando los tiempos de sincronización del resto del equipo de desarrolladores.

Por eso, para proyectos de Unity, asegúrate de incluir un archivo .gitignore adecuado para evitar subir archivos innecesarios. Git espera encontrar este fichero en la raíz del proyecto (al mismo nivel que la carpeta Assets). Su contenido enumera el listado de nombres de fichero y carpetas que Git debe evitar incluir en el repositorio para mantenerlo limpio. En internet, puedes encontrar abundantes ejemplos de ficheros .gitignore adecuados para Unity. Si usas Rider, JetBrains tiene un plugin oficial (denominado .ignore) que te ofrece un wizard para generar un .gitignore adecuado al proyecto que te traigas entre manos (incluido Unity). Otra fuente puede ser GitHub, que tienen un repositorio oficial con archivos .gitignore para los frameworks de desarrollo más populares, y tampoco falta el de Unity

Qué debes dejar fuera del repositorio

Si optas por construirte un fichero .gitignore a mano, debes excluir lo siguiente:

  • Carpetas de Unity: Excluye Library, Temp, Obj, Build, Logs y UserSettings, ya que son generadas automáticamente por Unity y no deben versionarse.
  • Archivos de compilación: Archivos como .csproj, .sln y otros generados por Visual Studio o Rider no son necesarios en el repositorio.
  • Caché y assets: Excluye caché de Addressables, StreamingAssets y paquetes para reducir el tamaño del repositorio.
  • Archivos del sistema operativo: Ignora .DS_Store (macOS) y Thumbs.db (Windows).
  • Editores y herramientas: Excluye configuraciones específicas de editores como VS Code o Rider, pero permite incluir configuraciones compartibles (como .vscode/settings.json).
  • Archivos grandes: Si no usas Git LFS, excluye archivos comprimidos o pesados como .unitypackage.

Qué debes incluir en el repositorio

Eliminado lo anterior, tu repositorio debería contener sólo las siguientes carpetas:

  • Assets
  • Packages
  • Project Settings

La más crítica de las carpetas anteriores es la de Assets ya que contiene todo el código del proyecto, así como los modelos, texturas, música, sonidos, y todos los elementos con los que está construido nuestro proyecto. 

La pregunta de siempre: ¿Qué hago con los ficheros .meta?

Si usas el explorador de ficheros para examinar la carpeta Assets, verás que cada uno de los elementos anteriores lleva asociado un fichero con extensión .meta. Dado que Unity se encarga de generar automáticamente esos ficheros .meta, mucha gente se pregunta si deberían incluirse en el repositorio. Para responder a esa pregunta, primero hay que entender para qué sirven.

Unity asocia un fichero .meta a cada uno de los ficheros assets que se incluyan en el proyecto. En los ficheros .meta se guardan los parámetros para importar cada asset en el proyecto. Esto es especialmente importante con assets como las texturas o los sonidos ya que, si no incluimos los ficheros .meta en el versionado, otros desarrolladores del equipo pueden importar los mismos recursos con diferentes parámetros, lo que puede darnos problemas.

Dado que Unity tiende a generarlos automáticamente si no los encuentra, es muy importante insistirle a todo el equipo en que, cuando creen un nuevo asset, lo incluyan en el repositorio junto con su respectivo archivo .meta. Si alguno se le pasase incluir en el repositorio el fichero .meta del recién creado asset, los próximos desarrolladores que se lo bajasen generarían un fichero .meta para él (lo haría el editor de Unity de manera automática al echarlo en falta) por lo que tendríamos un potencial conflicto al hacer los merges, dado que tendríamos diferentes ficheros .meta (uno por desarrollador) para un mismo asset. Es una situación fea que da para recurrentes preguntas en reddit

Moraleja: bajo ningún concepto te olvides de incluir los ficheros .meta en el repositorio de versiones.

Conclusión

Con lo anterior, ya tienes lo básico para saber si te falta algo importante en tu repositorio. Luego, operar con ese repositorio es un capítulo aparte que da para muchísimo. Usar un repositorio de Git es fácil, pero hacerlo de manera efectiva, y sin liarla, tiene mucha más miga.

Si quieres profundizar en este tema, le dedico el capítulo 15 de mi libro "Reconstrucción de un juego mítico con Unity".

12 junio 2025

Game Engines basados en Blender

Portada de Yo Frankie!
Blender se ha erigido, por derecho propio, como una de las grandes herramientas de modelado, animación y rendering del mercado. Su calidad, unida a su carácter open-source y gratuito, ha disparado su popularidad aupándola a una posición en la que puede mirar de tú a tú a grandes "monstruos" comerciales como Maya o 3D Studio.

Lo que no sabe tanta gente es que Blender también fue un engine para construir juegos. Se llamaba Blender Game Engine (BGE), y estuvo integrado con Blender desde comienzo de la década de los 2000. Permitía la creación de aplicaciones interactivas directamente desde Blender, sin necesidad de herramientas externas. Para la lógica, permitía no sólo programar en Python sino también emplear un lenguaje visual, muy similar a los actuales Blueprints de Unreal Engine.

El engine permitía crear juegos tanto 3D como 2D, con física en tiempo real (usando la librería Bullet Physics). También permitía crear shaders básicos y animaciones. Como estaba integrado en Blender, ya no hacía falta exportar los modelos a otros motores.

Para demostrar las capacidades del engine, la fundación Blender desarrolló el juego "Yo Frankie!" en 2008. El juego era vistoso, pero pronto quedó claro que que se quedaba atrás respecto a lo que podían ofrecer otros engines como Unity o Unreal. Frente a estos, el BGE no podía ofrecer más que un rendimiento limitado en proyectos complejos, adolecía de falta de soporte para plataformas móviles modernas y su interfaz de desarrollo estaba menos pulido.

Al final, la fundación Blender tuvo que tomar una decisión. Para mantenerse a la par que otros engines, el BGE requería en exclusiva de un equipo de desarrolladores que la fundación no podía dedicar. Además, la comunidad de desarrolladores de BGE, ajenos a la fundación, era muy pequeña y eso hacía que el ritmo de actualizaciones fuera muy lento. BGE se iba quedando atrás a ojos vista e iba lastrando al resto del proyecto de Blender. Al final se optó por abandonar el BGE, para poder concentrar los recursos de desarrollo en aquello que mejor se le daba a Blender: las herramientas de modelado, animación y renderizado (como Cycles).

La eliminación formal del BGE tuvo lugar en 2019, con la versión 2.80 de Blender. A partir de ahí, la fundación recomendó usar Blender como herramienta de modelado y exportar desde allí los assets a motores más avanzados.

Afortunadamente, como suele pasar en el mundo open-source, el cierre de un proyecto no suele ser tal, sino más bien su traspaso. Otros desarrolladores, interesados en continuar con el proyecto, recogieron el código fuente de BGE y lo hicieron evolucionar a partir de ahí. Gracias a eso, dónde antes teníamos un engine (BGE) ahora tenemos dos descendientes de aquel y uno fuertemente inspirado en él. Vamos a analizarlos:

UPBGE (Uchronia Project Blender Game Engine)

Logo de UPBGE
Este engine fue creado, junto con otros colaboradores, por Porteries Tristan, uno de los desarrolladores del BGE original 

Inicialmente, UPBGE pretendía ser una bifurcación que mejorase el código de BGE, limpiando su base y experimentando con nuevas características, pero con vistas a acabar incorporándose en el código principal de BGE. Sin embargo, la eliminación de BGE en la versión 2.80 de Blender llevó a UPBGE a adquirir identidad propia e independiente. A partir de entonces, UPBGE ha continuado su desarrollo asegurándose de sincronizar su código con el de Blender, para mantener la compatibilidad.

UPBGE está completamente integrado con Blender, permitiendo modelado, animación y desarrollo de juegos en un solo entorno sin necesidad de exportar assets.

Utiliza el motor EEVEE de Blender para renderizado en tiempo real, con soporte para sombreado PBR, reflejos SSR, oclusión ambiental GTAO, bloom, sombras suaves y volumétricos.

Usa la biblioteca Bullet Physics para simulaciones en tiempo real, incluyendo cuerpos rígidos, simulación de obstáculos y búsqueda de caminos.

Además, incluye un motor de audio basado en OpenAL y Audaspace, con soporte para sonido 3D

Para la lógica de los juegos, mantiene el sistema de "logic bricks" del BGE original y añade Logic Nodes, facilitando la creación de comportamientos interactivos sin programar. También soporta scripting en Python para los que prefieran programar a golpe de teclado.

A la hora de importar y exportar elementos, permite los mismos formatos que Blender, principalmente FBX, Collada, glTF, OBJ y STL.

Se trata de un proyecto muy activo, con un equipo pequeño pero muy implicado de desarrolladores, que sacan actualizaciones regulares cada pocos meses y mantienen el engine sincronizado con las evoluciones de Blender.

A nivel de funcionalidades, los desarrolladores planean añadir características modernas como SSAO, profundidad de campo (DoF), herramientas para modo en línea o luces de área.

Por tanto, se trata de un proyecto muy interesante y prometedor, al que sólo se puede poner un pero y es que el rendimiento que se puede esperar en escenas complejas, con muchos polígonos o riqueza de efectos, puede ser más limitado que el que se puede conseguir con engines como Unity o Unreal.

También hay que tener en cuenta que hereda la licencia GPL de BGE. Los juegos desarrollados con UPBGE no están obligados a ser de código abierto si solo usan los datos del proyecto (modelos, texturas, scripts, etc.) y no distribuyen el ejecutable de UPBGE (blenderplayer). Sin embargo, si un juego incluye el blenderplayer o partes del código de UPBGE, debe cumplir con la GPL y ser distribuido con el código fuente.

Range Engine

Logo de Range Engine
Este engine no es un fork del BGE original, sino de UPBGE. Se originó en 2022, como consecuencia de determinadas decisiones de diseño que se tomaron para UPBGE 3.0. Algunos desarrolladores entendieron que dichas decisiones alejaban a UPBGE del espíritu del BGE original y por eso decidieron escindirse a través de un fork.

Por ese motivo, Range Engine conserva un interfaz mucho más cercano al que había en el BGE original. Frente a UPBGE, Range Engine prima la sencillez de uso.

Range Engine prioriza la compatibilidad con proyectos antiguos de BGE y la sencillez en el flujo de trabajo de los desarrolladores.

Su juventud, y su relativamente menor base de desarrolladores, explican que sus funcionalidades y ritmo de actualizaciones sean menores que los de UPBGE. 

Aun así, parece que han conseguido optimizar el apartado de animaciones frente a UPBGE. Por otro lado, también se han observado limitaciones de rendimiento cuando se utiliza este engine con grandes escenarios abiertos. 

Al igual que UPBGE, está licenciado con GPL. Sin embargo, incluye Range Armor, un formato proxy que permite empaquetar datos de juego separadamente del ejecutable blenderplayer, posibilitando la creación de juegos bajo licencias más flexibles como MIT, lo que facilita la distribución comercial sin restricciones estrictas.

Armory 3D

Logo de Armory 3D
A diferencia de los dos anteriores, no se trata de un fork directo de BGE, sino de engine de juegos independiente que usa a Blender como complemento para el modelado, texturizado, animación y composición de escenas. Además, enriquece lo anterior con herramientas relacionadas como ArmorPaint (para pintura de texturas PBR) y ArmorLab (para creación de texturas con IA), ambas integradas con el motor Armory.

Basa su editor de lógica en scripts de lenguaje Haxe. Algunos dicen que este lenguajes es más flexible que el Python utilizado en BGE. También ofrece un editor visual basado en nodos, a los que recientemente se han añadido uevos nodos para interpolación (tweening) de valores, vectores y rotaciones, nodos de dibujo de cámara, nodos de pulso para controlar tasas de disparo, y mejoras en la jerarquía de nodos lógicos.

A nivel multimedia, este engine se apoya en el framework Kha. Utiliza un renderizador físicamente basado, compatible con pipelines HDR, sombreado PBR y efectos de posprocesamiento, todo ello de forma muy personalizable.

Al igual que BGE, utiliza Bullet Physics, como motor de físicas, y soporta cuerpos rígidos, colisiones y físicas blandas. También permite configurar el motor de físicas Oimo como alternativa. Además, ofrece soporte para sistemas de partículas (emisor y cabello) con instanciación por GPU para mejorar el rendimiento

Armory 3D tiene mayor compatibilidad para exportar tu juego a diferentes plataformas y ofrecer un rendimiento optimizado de las animaciones. Actualmente soporta Windows, macOS, Linux, Web, Android, iOS y consolas (con configuraciones específicas).

A diferencia de los anteriores, Armory 3D está impulsado principalmente por un único desarrollador (Lubos Lenco) y eso se nota en que las actualizaciones llegan con cuenta gotas. Aun así, poco a poco se van incrementando las colaboraciones y aportaciones externas conformando un primigenio núcleo de desarrolladores.

En cuanto a la licencia, una ventaja respecto a los herederos directos de BGE es que Armory 3D usa la licencia Zlib, más permisiva que la GPL de Blender, permitiendo mayor flexibilidad para proyectos comerciales.

Conclusiones

Como puedes ver, el desarrollo de juegos con Blender está muy vivo, y no sólo usándolo como herramienta complementaria de modelado y animación, sino como un engine en si mismo.

A día de hoy no ofrece un rendimiento equiparable al que se puede obtener con engines dedicados como Unity, Unreal o Godot Engine, pero la posibilidad de poder concentrar en una misma herramienta tanto la programación como el modelado, texturizado y rigging de assets, puede simplificarle mucho la vida a los que quieran iniciarse en el mundo de los videojuegos. Sin contar con que lenguajes como Python o Haxe son realmente sencillos para aprender a implementar los algoritmos típicos de un juego.

Mi consejo, si quieres iniciarte en este tipo de engines, es que empieces con UPBGE y luego pruebes los otros dos, a ver qué valor diferencial te pueden ofrecer.

06 junio 2025

Cómo implementar un cono de visión en Unity

Un cono de visión
Un cono de visión en videojuegos es un mecanismo utilizado principalmente en juegos de sigilo o estrategia para simular el campo de visión de un personaje no jugable (PNJ), como un enemigo o un guardia. Se representa como un área cónica que se origina en los ojos del PNJ y se extiende hacia adelante en un ángulo determinado, definiendo el rango y la dirección en la que el PNJ puede detectar al jugador u otros objetos. Si no hay obstáculos por medio, todos los objetos que se encuentren dentro del cono de visión del jugador son visibles para este.

Puedes encontrar algunos ejemplos célebres de uso de este concepto en juegos como Commandos o Metal Gear Solid. En Commandos el cono de visión de los enemigos es visible en la ventana principal  para mostrar el área de vigilancia de los soldados enemigos.

Conos de visión en Commandos
Conos de visión en Commandos

En Metal Gear Solid los conos de visión no se muestran en la ventana principal sino en el minimapa de la esquina superior derecha, para permitir que el jugador pueda planear sus movimientos para desplazarse por el escenario sin ser detectado.


Conos de visión en Metal GearSolid

En general, el cono de visión es clave para crear mecánicas de sigilo, ya que obliga al jugador a planificar movimientos, usar el entorno (como coberturas o distracciones) y gestionar el tiempo para evitar ser detectado. También añade realismo, ya que simula cómo los humanos o criaturas tienen un campo de visión limitado, no omnidireccional.

En este artículo vamos a ver cómo podemos implementar un cono de visión en Unity. La idea es crear un sensor que simule este modo de detección, de tal manera que podamos añadirlo a nuestros PNJ.

Características principales de los conos de visión


  • Ángulo: El cono suele tener forma de triángulo o sector circular en 2D, o un cono tridimensional en juegos 3D. El ángulo de visión (por ejemplo, 60° o 90°) determina la amplitud de campo que el PNJ puede "ver".
  • Distancia: El cono tiene un alcance máximo, más allá del cual el PNJ no detectará al jugador, incluso si está dentro del ángulo de visión.
Puedes añadirles más adornos, pero un cono de visión se define sólo por esos dos factores.

En muchos juegos, el cono de visión se muestra gráficamente al jugador (especialmente en vista cenital o en interfaces específicas) para indicar dónde debe evitar ser visto. Puede cambiar de color (por ejemplo, verde para "sin alerta" y rojo para "alerta"). En este artículo no cubriré la parte visual porque tampoco aporta mucho. Quiero enfocarme a implementar lo que el PNJ puede ver y lo que no, no a la representación del cono.

En Unity, lo normal es que el componente que implemente el cono de visión exponga esas dos características en el inspector, tal y como se puede ver en la captura siguiente:

Campos básicos para implementar un cono de visión
Campos básicos para implementar un cono de visión

En mi caso, detectionRange (línea 15) implementa la distancia, mientras que detectionSemiconeAngle (línea 18) implementa el ángulo.

En el caso del ángulo, mi código parte de unas premisas que hay que tener en cuenta. La primera es que he usado un atributo [Range] (línea 17) para poder configurar el valor de este campo con un control deslizante y para limitar el rango de valores pasibles al intervalo entre 0 y 90 grados. Si bien es cierto que el ángulo de visión de una persona es superior a los 90º grados laterales, en los juegos sería demasiado difícil esquivar a un personaje con ese cono de visión así que lo normal es no superar los 90º, siendo los 45º lo más habitual. La segunda premisa es que trato al ángulo como un semiángulo. Es decir, lo mido desde la dirección que considere frontal (Forward, en mi caso) en un sentido, y luego se replica especularmente en el otro sentido para generar un cono simétrico. 

Los dos parámetros que definen un cono de visión
Los dos parámetros que definen un cono de visión


En mi ejemplo, estoy trabajando en 2D, así que he definido que Forward sea el eje +Y local, tal y como se puede ver en la siguiente captura. 
 
Definición del vector frontal (Forward)
Definición del vector frontal (Forward)


En la línea 20 de la captura de código he incluido un campo más, layersToDetect, que vamos a usar de filtro, como veremos un poco más adelante.


Cómo detectar si una posición está dentro del cono de visión


Definida la distancia y el ángulo, tenemos que valorar si la posición a comprobar está a menos de esa distancia y si el ángulo entre el vector de posición relativa y el vector Forward del cono es inferior al ángulo de este. En Unity, es muy fácil calcular ambas cosas.

 
Método para determinar si una posición está dentro del cono de visión
Método para determinar si una posición está dentro del cono de visión

La manera más sencilla de calcular la distancia es usar el método Vector2.Distance(), tal y como hago en la línea 126 de la captura, pasándole como parámetros la posición del cono de visión (coincidente con su vértice) y la posición a chequear.

Para el ángulo, podemos usar el método Vector2.Angle(), tal y como se ve en la línea 127. Este método devuelve el ángulo absoluto entre dos vectores, por eso le paso por un lado Forward (línea 128) y por otro el vector de la posición a chequear, relativa al origen del cono (línea 129).

Si tanto la distancia, como el ángulo, están por debajo de los umbrales definidos en el cono, entonces la posición chequeada está dentro de este.


Filtrado de objetos


Podríamos dejar aquí el artículo y tendrías un cono de visión funcional. Te bastaría con recopilar todos los objetos potencialmente visibles del escenario, e ir pasando sus posiciones (uno a uno) al método PositionIsInConeRange() definido antes. Habría que hacer esta comprobación periódicamente, quizás en el método Update() o en el FixedUpdate().

Sin embargo, esto no sería muy eficiente ya que el escenario podría ser enorme y y contener multitud de objetos. Sería mucho mejor si pudiéremos hacer un filtrado previo, de manera que sólo le pasásemos a PositionIsInConeRange() los objetos mínimos e imprescindibles.

Filtrado por capas


El primer filtrado que podríamos aplicar es por capa. Podemos distribuir los objetos del escenario en distintas capas y configurar el cono de visión para que sólo tuviese en cuenta los objetos de una capa determinada. Ese era el propósito del campo layersToDetect mencionado antes. Extendido, este campo tiene un aspecto como el de la captura.

Campo layersToDetect de tipo LayerMask
Campo layersToDetect de tipo LayerMask

Este tipo de campos permite selección múltiple, por lo que puedes definir que tu cono analice varias capas simultáneamente.

Una vez que sepas qué capas quieres analizar, discriminar si un objeto está o no en alguna de esas capas es aparentemente sencillo, como puedes ver en la siguiente captura.

Cómo saber si un objeto está en alguna de las capas de un LayerMask
Cómo saber si un objeto está en alguna de las capas de un LayerMask

Digo "aparentemente" sencillo porque, aunque puedes limitarte a copia-pegar este código en el tuyo, entenderlo del todo tiene su miga.

Para empezar, un LayerMask tiene un campo value que es un entero de 32 bits en el que cada uno de ellos representa las 32 capas posibles en una escena de Unity. Puedes imaginarte una sucesión de 32 unos y ceros. Si en el campo layerMask incluyes dos capas, en el campo value aparecerán 2 bits con valor uno y el resto serán ceros. El valor entero final del campo dependerá de la posición de esos unos aunque, en realidad, dicho valor es indiferente porque lo importante es qué posiciones tienen un uno.

Por otro lado, todos los objetos de Unity tienen un campo llamado layer que contiene un entero con valores que van desde el 0 al 31. Este entero lo que señala es el índice de la capa a la que pertenece el objeto, dentro del LayerMask de todas las capas posibles de la escena. Por ejemplo, si el campo layer de un objeto tiene un valor 3, y esa capa está incluida en un LayerMask, entonces dicho LayerMask tendrá un uno en su bit de índice 3.

Para saber si la capa de un objeto está dentro de las marcadas en un LayerMask tenemos que hacer una comparación, usando para ello la capa del objeto como máscara. El truco es generar un entero cuyo valor binario esté lleno de ceros y meter un uno en la posición que corresponde a la capa a comprobar. Ese entero es lo que llamamos la máscara. Compararemos dicha máscara con el LayerMask, haciendo un AND binario, y veremos si el valor resultante es o no distinto de cero. Si fuese cero significaría que el LayerMask no incluía la capa que queríamos comprobar.

De ve mejor representando el ejemplo de antes. Mira la siguiente captura. 

Operación para comprobar si una capa está contenida dentro de un LayerMask
Operación para comprobar si una capa está contenida dentro de un LayerMask

En ella he representado un LayerMask que con dos capas, la de índice 1 y la de índice 3 (son las posiciones que tiene a uno). Supón ahora que queremos comprobar si el LayerMask contiene la capa 3. 

Lo que hemos hecho ha sido genera una máscara con todo ceros, salvo el uno de la posición 3, y hemos hecho AND con el LayerMask. El hacer AND con una máscara hace depender el resultado final del valor que tuvieran los dígitos del LayerMask en las posiciones marcadas por la máscara. En este caso, la máscara señala la posición 3 por lo que el resultado final será cero o diferente a cero dependiendo de si la posición 3 del LayerMask es cero o diferente de cero. En este caso será diferente de cero.

Filtrado por cercanía


Con el filtrado por capas evitaremos llamar a PositionIsInConeRange() por objetos que estén en capas que no nos interesen. Eso supondrá una mejora en el rendimiento, pero podemos mejorarlo más.

Otro filtrado previo que podemos hacer es descartar aquellos objetos que estén demasiado lejos del cono como para que exista la posibilidad de estar en él.

Como puedes ver en la captura, todo cono de visión puede encajarse en un caja que lo envuelve.

Caja envolvente de un cono de visión
Caja envolvente de un cono de visión


Si esa caja fuese un sensor volumétrico (en términos de Unity: un collider en modo trigger), podríamos pasarle a PositionIsInConeRange() sólo los objetos que entrasen en el sensor volumétrico y fuesen de las capas que nos interesasen.

Método para procesar los objetos que entrasen en la caja


En el código de la captura, OnObjectEnteredCone() sería un event handler que se aplicaría en caso de que un objeto entrase en la caja. En mi caso, el collider en modo trigger lleva asociado un script que emite un UnityEvent cuando el trigger hace saltar su OnTriggerEnter2D. Lo que he hecho es asociar OnObjectEnteredCone() a ese UnityEvent.

Partiendo de ahí, el código de la captura es sencillo. En la línea 159 comprobamos si el objeto está en una de las capas que nos interesa, usando el método ObjectIsInLayerMask() que analizamos antes. En caso afirmativo, en la línea 161 se comprueba si el objeto se encuentra en el área cubierta por el cono de visión, usando el método PositionIsInConeRange() que veíamos al principio. Y, finalmente, si ambas comprobaciones resultan positivas, se añade al objeto a la lista de los objetos detectados por el cono de visión (línea 164) y se emite un evento para que los scripts que usen el cono de visión sepan que este ha hecho una nueva detección.

Como te puedes imaginar, hace falta implementar un método recíproco para procesar los objetos que salgan de la caja de detección; sí como otro método para procesar los que pudieran mantenerse dentro de la caja de detección pero se hubiesen salido del área cubierta para el cono. Bastará con enlazar eventHandler a los métodos OnTriggerExit2D() y OnTriggerStay2D() del script del trigger collider de la caja de detección. Ninguna de esas casuísticas tiene especial complejidad, una vez entendido el código de OnObjectEnteredCone(), pero de todos modos te muestro mi implementación de la comprobación de un objeto que permanezca en el área de detección.

Comprobación de un objeto que permanezca en el área de detección
Comprobación de un objeto que permanezca en el área de detección


Llegados a este punto, probablemente te estés preguntando cómo dimensionar la caja para que se ajuste al cono de visión. 

Si te fijas en la captura que ponía antes, con la caja envolviendo en cono de visión, verás que la altura de la caja coincide con el parámetro que he denominado detectionRange. 

Lo que tiene algo más de miga es la anchura de la caja, ya que tenemos que recurrir a trigonometría básica. Fíjate en la captura:

Algo de trigonometría para calcular el ancho de la caja
Algo de trigonometría para calcular el ancho de la caja


Partiendo de la captura, para encontrar la anchura de la caja de detección necesitamos calcular la longitud de B, que se corresponderá con la mitad de dicha anchura. 

B es uno de los lados del rectángulo creado usando detectionRange como diagonal. Todo rectángulo está compuesto a su vez de dos triángulos rectángulo cuya hipotenusa será precisamente detectionRange. Si nos fijamos en el triángulo rectángulo superior (el de la zona roja), y repasamos la trigonometría que dimos en el colegio, estaremos de acuerdo de que el seno de detectionSemiConeAngle es igual a B dividido entre detectionRange. Por lo tanto, podemos calcular B como el producto entre detectionRange y el seno de detectionSemiConeAngle; siendo la anchura total de la caja de detección el doble de B.

Traducido a código, las dimensiones de la caja de detección se calcularían de la siguiente forma:

Cálculo de las dimensiones de la caja de detección
Cálculo de las dimensiones de la caja de detección


Puedes hacer este cálculo cada vez que cambies los parámetros del cono de visión, y modificar manualmente las dimensiones del trigger collider con el resultado del cálculo; pero yo he preferido hacerlo automático enlazando el collider con un componente BoxRangeManager que he implementado yo, y que modifica dinámicamente el tamaño del collider según vayas cambiando los campos Range y Width de dicho BoxRangeManager. La implementación de ese componente se basa en lo que expliqué en mi artículo sobre "Sensores volumétricos con dimensiones dinámicas en Unity", así que no voy a repetirlo aquí.


Conclusión


Con esto ya tienes todo lo que necesitas para crear un cono de visión, sencillo, eficiente. Mi consejo es que crees un componente genérico que reutilices en tus diferentes proyectos. Se trata de un elemento tan habitual que no tiene sentido implementarlo desde cero cada vez. Este debería ser uno de los elementos de tu librería  personal de componentes reutilizables.

Espero que este artículo te haya resultado interesante y te sirva para crear mecánicas de juego emocionantes.

28 mayo 2025

Cómo ejecutar métodos desde el inspector de Godot

Botón en el inspector
Botón en el inspector
Hace unos días publiqué un artículo explicando cómo hacer en Unity para activar la ejecución de métodos desde el inspector. Vimos la posibilidad de utilizar un atributo que generaba una entrada en un menú del editor; y también cómo crear un editor personalizado para que el inspector del componente mostrase un botón con el que activar el método.

¿En Godot se puede hacer lo mismo? Bueno, hasta hace realmente poco no. No había una manera fácil que permitiese añadir un botón al inspector sin meterse a desarrollar un plugin y sin que el resultado fuera discutible. En lo que se refiere a personalización del GUI, a Godot aún le queda un largo camino por recorrer para estar al nivel de Unity. 

El atributo [ExportToolButton]

Sin embargo, Godot añadió recientemente un nuevo atributo, @export_tool_button, y su equivalente en C# [ExportToolButton]. Este atributo permite exportar al inspector un campo de tipo Callable y mostrarlo como un botón. Al pulsar el botón se activa el método al que apunte el Callable.

Vamos a ver un ejemplo en Godot C#. Supongamos que tenemos un método ResetBoxManager en nuestro script:

El método que queremos activar al pulsar el botón
El método que queremos activar al pulsar el botón

Lo que hace el método da igual. Se trata sólo de un ejemplo. Muestro una captura de su contenido para que veas que la declaración e implementación del método no tiene nada de particular.

Y ahora el botón. Para declararlo, sólo tienes que decorar un campo de tipo Callable con una etiqueta [ExportToolButton].

Declaración del botón con [ExportToolButton]
Declaración del botón con [ExportToolButton]

Entre los paréntesis del atributo pondremos el texto que queremos que muestre el botón. 

Por otro lado, en la captura puedes ver cómo inicializar el Callable. He llamado al campo ResetButton (línea 107) y lo he inicializado con una nueva instancia de Callable que apunta, por sus parámetros, al método ResetBoxManager de esa misma clase (por eso el "this"), tal y como se puede ver en la línea 108.

Con eso, tu inspector mostrará el botón en el lugar que le habría correspondido al campo, y cuando lo pulses se activará el método enlazado. Tienes una captura de cómo queda en la imagen que abre este artículo.

Conclusión

Como puedes ver, el atributo [ExportToolButton] hace realmente fácil añadir botones a tus inspectores. Combina la sencillez del atributo [ContextMenu] que vimos en Unity, con la apariencia visual de sus editores personalizados. Con esto podrás dar un paso adelante a la hora de dotar a tus inspectores de funcionalidad que agilice el desarrollo y te facilite la depuración de tus proyectos.

24 mayo 2025

Cómo ejecutar métodos desde el inspector de Unity

Imagen de portada del artículo
Unity es una de las herramientas más populares para el desarrollo de videojuegos, gracias a su flexibilidad y su entorno visual intuitivo. Una de las características más útiles de Unity es su inspector. El inspector es una ventana en Unity que muestra las propiedades de los componentes de un GameObject seleccionado y nos permite ajustarlas. Por ejemplo, si un GameObject tiene un script asociado, el Inspector mostrará las variables públicas definidas en ese script, permitiéndote modificarlas directamente en el editor. 

Sin embargo, lo que muchos desarrolladores novatos desconocen es que también puedes configurar el inspector para ejecutar métodos específicos de tus scripts, ya sea para probar funcionalidades o para configurar flujos de trabajo más eficientes. En este artículo, te explicaré cómo ejecutar métodos desde el inspector de Unity de manera sencilla, una técnica que puede ahorrarte tiempo y facilitar la configuración y la depuración de tu proyecto.

El atributo [ContextMenu]

Una forma directa de ejecutar métodos desde el Inspector es utilizando el atributo [ContextMenu]. Este atributo añade una entrada al final del menú contextual del script en el inspector, lo que te permite invocar métodos con un solo clic. El menú contextual es el que sale al pulsar con el botón derecho en la barra del nombre del componente o a los tres puntos que hay en la esquina superior derecha del script en el inspector.

Menú contextual de un script en el inspector
Menú contextual de un script en el inspector

Si decoras el método que quieres activar con este atributo:

Ejemplo de uso del atributo
Ejemplo de uso del atributo

La entrada aparecerá al final del menú contextual, con el texto que hayas incluido entre paréntesis en el atributo.

Menú contextual resultante
Menú contextual resultante


Creación de un editor personalizado

La opción anterior es la más rápida de implementar, pero puede no ser la más cómoda ya que implica dos clicks para ejecutar el método en cuestión. 

Otra alternativa, más visual, pero que más laboriosa, es incluir un botón en el inspector para activar el método. Para ello, tienes que crear un editor propio que muestre una vista personalizada del componente en el editor.

Vamos a ver analizar el ejemplo más sencillo posible. Supongamos que tenemos un script MonoBehavior llamado BoxRangeManager, con un método ResetBoxManager() (el de la captura anterior) que es el que queremos activar. Para crear un editor que muestre una vista personalizada de BoxRangeManager tienes que crear una carpeta Editor dentro de la principal Assets. En la carpeta Editor es donde debes meter todos los scripts que dediques a personalizar el editor de Unity. Es muy importante que esos scripts no acaben en la carpeta Scripts habitual o si no puedes tener graves problemas de compilación cuando quieras construir el ejecutable final del juego. Recuerda: los scripts del juego en la carpeta Scripts, y los scripts de personalización del editor en la carpeta Editor.

Continuando con nuestro ejemplo, una vez creada la carpeta Editor, tienes que crear un script como el de la siguiente captura:

El código de nuestro editor personalizado
El código de nuestro editor personalizado

Lo primero reseñable del código anterior es que importa el espacio de nombres UnityEditor (línea 2). Esa es la primera señal de que tu script debería estar en la carpeta Editor o bien tendrás problemas al construir tu paquete ejecutable.

Incluir el código en un namespace propio (línea 5) es una buena práctica, pero no imprescindible.

Entramos en harina: para que una clase sirva para implementar un editor personalizado, debe heredar de UnityEditor.Editor (línea 8); y para que Unity sepa con qué MonoBehaviour usar el editor personalizado, hay que identificar a este en una etiqueta [CustomEditor] que decore la clase (línea 7).

A partir de ahí, pera personalizar cómo se muestra BoxRangeManager en el inspector, hay que reimplementar el método OnInspectorGUI() de UnityEditor.Editor (línea 10).

Nuestro ejemplo es el más sencillo posible. Sólo queremos mostrar el inspector por defecto y añadir al final de él un botón que active el método ResetBoxManager() al ser pulsado. Así que lo primero que hacemos es dibujar el inspector como lo habría hecho el inspector por defecto (línea 13). Luego añadimos un espacio para no pegar demasiado el botón a todo lo anterior (línea 16). Finalmente añadimos nuestro botón en la línea 22, pasando como parámetros el texto que queremos que aparezca en el botón y la altura que queremos que tenga.

El botón devuelve verdadero cuando es pulsado. Gracias a eso, en las líneas 24 y 25 podemos definir la reacción cuando se pulse. En ese caso, lo primero es ejecutar el método ResetBoxManager() que queríamos (línea 24) y luego llamar al método EditorUtility.SetDirty() (línea 25) para avisar al editor de que hemos hecho cambios en el inspector del componente y forzar que lo vuelva a redibujar.

Fíjate en que el editor personalizado contiene una referencia al MonoBehavior cuyo inspector muestra en el campo target. Sólo tienes que hacer una cast a la clase que sabes que estás mostrando (línea 19) para tener acceso a sus campos y métodos públicos.

Y ya está. No tienes que hacer nada más. Una vez que Unity recargue el dominio, se mostrará el editor personalizado con nuestro nuevo y flamante botón.

Aspecto de nuestro editor personalizado
Aspecto de nuestro editor personalizado

Conclusión

Ejecutar métodos desde el Inspector de Unity es una técnica poderosa para agilizar el desarrollo y la depuración de tus proyectos. Ya sea usando el atributo [ContextMenu] para pruebas rápidas,o con editores personalizados, estas herramientas te permiten interactuar con tu código de manera más dinámica y visual. 

Experimenta con estas opciones y descubre cuál se adapta mejor a tu flujo de trabajo. ¡El Inspector de Unity es mucho más que un simple editor de propiedades!

17 mayo 2025

Sensores volumétricos con dimensiones dinámicas en Unity

Crear un sensor volumétrico en Unity es sencillo: añades un componente Collider a un Transform, lo configuras como trigger, le das forma al Collider para definir el alcance del sensor, y añades un script al Transform que implemente los métodos OnTriggerEnter y OnTriggerExit. El Collider se encargará de activar esos métodos cuando otro collider entre o salga de su alcance, respectivamente.

Eso es todo, y suele bastar en la mayor parte de las ocasiones, pero hay otras en las que tenemos que definir la forma del sensor volumétrico en tiempo real, durante la ejecución del juego. Imagina, por ejemplo, que quieres dotar de un sensor volumétrico a un agente móvil para que detecte obstáculos que se puedan interponer en su rumbo. Lo normal será que pongas un sensor volumétrico con forma de caja delante del agente y que dicha caja se alargue hacia delante del agente a lo largo de una distancia proporcional a la velocidad del agente. Si la caja es demasiado corta, es probable que se detecten los obstáculos demasiado tarde como para que el agente pueda evitar la colisión. Si la caja es demasiado larga, el agente puede reaccionar a obstáculos demasiado lejanos, lo que sería poco realista.

Un agente móvil con un sensor volumétrico para detectar obstáculos
Un agente móvil con un sensor volumétrico para detectar obstáculos

Si el agente mantiene siempre una velocidad constante, nos bastará fijar manualmente el tamaño del sensor al diseñar el agente. Pero si el agente varía su velocidad, dependiendo de su comportamiento, es probable que no nos valga un único tipo de sensor para todas las posibles velocidades. En ese casos podríamos dotar al agente de varios sensores, con diferentes tamaños, y activar unos u otros en función de la franja de velocidad en la que se encuentre el agente. Sin embargo, esa solución será muy compleja y muy difícil de escalar si acabamos cambiando el abanico de velocidades del agente. Es mucho mejor si modificamos el tamaño del sensor durante la ejecución y lo adaptamos a la velocidad del agente en cada momento.

Te voy a explicar cómo lo hago yo con un BoxCollider2D. No digo que sea la mejor manera de hacerlo, pero sí que es la mejor manera con la que he dado hasta el momento. Te resultará fácil adaptar mis ejemplos a otras formas de collider y usarlo como punto de partida para encontrar el mecanismo que mejor se adapte a tu caso.

Un BoxCollider2D permite cambiar su tamaño a través de su propiedad size. Esta consiste en un Vector2 cuya primera componente define el ancho y la segunda la altura de la caja. Podemos cambiar el valor de esta propiedad en cualquier momento y el collider adaptará su forma a ella. El problema es que el BoxCollider se mantendrá centrado y crecerá en ambas direcciones de la componente que hayamos modificado. Si tenemos un caso como el de la captura de antes, lo que querremos es que el collider crezca hacia adelante, cuando aumentemos la velocidad, y no que el collider crezca también hacia la parte trasera del agente.

La manera de resolverlo es modificar la dimensión que nos interese, en el caso del agente de la captura será la altura de la caja, y al mismo tiempo desplazar la caja, manipulando su offset, para que parezca que sólo crece por uno de sus lados. En el ejemplo que nos ocupa, para hacer crecer el sensor, aumentaríamos la altura de la caja y al mismo tiempo desplazaríamos su offset hacia arriba para mantener el lado inferior de la caja en el mismo punto y que pareciese que sólo crece por el lado superior. 

Queremos que el collider crezca sólo por uno de sus lados
Queremos que el collider crezca sólo por uno de sus lados

Partiendo de lo anterior, voy a mostrarte el código que uso para generalizar los diferentes casos posibles para una caja. Para un BoxCollider, las modificaciones de tamaño posible son las del enum de la siguiente captura:

Posibles direcciones de crecimiento de una caja
Posibles direcciones de crecimiento de una caja

Las opciones se explican por si mismas: "Up" implica que queremos que la caja crezca solamente por su lado superior, "Down" por el inferior, "Left" por el lado de la izquierda y "Right" por el de la derecha. "Symmetric" viene a ser el comportamiento por defecto según el cual, al cambiar la altura la caja crecería tanto por su lado superior como por el inferior.

La cuestión es que, cuando aumentas el tamaño de una caja en una de sus dimensiones, dicho aumento se reparte equitativamente en un crecimiento de los dos lados de esa dimensión. Por ejemplo, si aumentas el tamaño de la altura de una caja en 2 unidades, puedes esperar que el lado superior ascienda una unidad y el inferior baje otra. Por tanto, si quisieras que la caja aparentase crecer sólo por el lado superior, deberías mantener el inferior en el mismo punto haciendo que la caja ascendiese una unidad.

La manera de generalizarlo es que la caja debe moverse la mitad del aumento de tamaño en la dirección de crecimiento que le hayamos marcado. 

Cómo calcular el vector de movimiento de la caja
Cómo calcular el vector de movimiento de la caja

El vector de movimiento de la caja se puede obtener de un método como el anterior (GetGrowOffsetVector). En el caso de nuestro ejemplo, en el que queremos que la caja aparente crecer por el lado superior, y que el inferior permanezca en su posición, el growDirection sería "Up" por lo que el método devolvería un Vector2 con los valores (0, 0.5). Fíjate que he definido OffsetBias como una constante de valor 0.5. Ese vector se multiplicará luego por el vector de crecimiento de la caja, lo que nos dará el desplazamiento de la caja.

El vector de crecimiento es el vector resultante de restarle al nuevo tamaño de la caja el tamaño que tenía inicialmente.

Cálculo del vector de crecimiento
Cálculo del vector de crecimiento

Por tanto, cada vez que queramos cambiarle el tamaño a la caja tendremos que calcular el vector de crecimiento y multiplicarlo por el vector de movimiento de la caja para obtener el nuevo ofsset de la caja para que aparente que sólo se ha movido por uno de sus lados.

 

Método para cambiar el tamaño de la caja
Método para cambiar el tamaño de la caja

El método SetBoxSize(), en el que implemento lo anterior, no puede ser más sencillo. En sus líneas 153 y 154 reseteo la caja a su tamaño inicial (1,1) y su offset a (0,0). Como el nuevo tamaño se fija acto seguido, el reseteo es instantáneo y no llega a percibirse.

Luego, en la línea 155 ejecuto el método GetGrowOffsetVector(), para obtener el vector de movimiento de la caja. Y en la línea 156, obtengo el vector de crecimiento al llamar al método GetGrowVector(). Ambos vectores se multiplican en la línea 158, para dar lugar al nuevo offset de la caja. Observa en esa misma línea, la 158, que utilizo el campo initialOffset (de tipo Vector2) para definir un offset por defecto de la caja. Ese será el offset que tendrá la caja cuando no se le haya aplicado movimiento alguno.

Fíjate también que utilizo el campo boxCollider para manipular las propiedades del collider. Este campo cuenta con una referencia a dicho collider. Puedes obtener esa referencia, bien exponiendo el campo en el inspector y arrastrando el collider sobre ese campo, o bien usando una llamada a GetComponent() en el Ready() del script.

Si incluyes los métodos anteriores en un script, y haces que este exponga métodos públicos, que acaben llamando a SetBoxSize() podrás manipular el tamaño del collider en tiempo real desde otros scripts del juego.

Y con esto ya lo tienes todo. Es bastante sencillo cuando ya lo tienes claro, pero si partes desde cero te puede llevar un rato hasta averiguar cómo hacerlo. Espero que este artículo te haya ahorrado ese rato y que te haya parecido interesante.

15 mayo 2025

Curso "Beat'em up Godot tutorial"

Hace algún tiempo que completé el tutorial "Beat'em up Godot tutorial", pero no me había dado tiempo a escribir sobre ello hasta ahora. 

Se trata de un video tutorial de 20 episodios, disponible en YouTube completamente gratis. Se hace en aproximadamente unas 10 horas. En él, el autor te guía en el proceso de crear un juego beat'em up, similar a los clásicos "Streets of Rage" o "Double Dragon". Según tengo entendido, el juego se desarrolló primero para una Game Jam y quedó tan redondo que resultaba candidato ideal para un curso.

El contenido empieza por el principio, así que es perfectamente válido para aquellos que quieran iniciarse con Godot, pero también se mete en conceptos más avanzados, pasada la curva inicial de aprendizaje, por lo que también resultará interesante a los alumnos con un nivel intermedio o, sencillamente, a aquellos que quiera un refresco en Godot.

Parte de los conceptos más básicos, como darle forma a los personajes, a sus animaciones y movimientos. Luego trata el sistema de daño, a base de cajas de daños, lo que me ha resultado bastante interesante ya que es un sistema mucho más sencillo y elegante que el que había usado hasta ahora. También implementa máquinas de estado para los personajes, desde cero y a golpe de código. Esto último puede resultar interesante, pero me ha parecido que pecaba de reinventar la rueda. Habría preferido que hubiera utilizado el sistema de máquinas de estado que ya viene incluido con Godot, a través de sus Animation Tree. Hecho eso pasa a la implementación de armas, tanto cuerpo a cuerpo como arrojadizas, así como de otros objetos recogibles como comida y botiquines. También me ha parecido muy interesante y original cómo arma los escenarios y los diferentes spawn points de él para generar los enemigos. Es todo muy modular y reutilizable. En cuanto al apartado de GUI, así como de música y sonido, cuenta lo básico pero tampoco es que un juego de este tipo, con estética retro, pida mucho más.

Ya llevo bastantes cursos, libros y algún desarrollo a mis espaldas, por lo que ya distingo bastante bien las buenas prácticas y lo que es un desarrollo ordenado y limpio, y este lo es. Quizás me dé la sensación de que si hubiera empleado C# el código habría quedado más modular, pero sí que parece que hace buen uso de GDScript hasta donde el lenguaje le permite. Todo el sistema de nodos de Godot ya obliga por si mismo a una altísima modularidad y reutilización, lo que unido a la ligereza del editor hace que el desarrollo del juego sea bastante directo y haya muy pocos momentos de espera o de generar código repetido. Dado que los recursos del juego están públicamente disponibles, quiero implementarlo en Unity a ver cómo queda, comparado con Godot.

Así que se hace muy entretenido y realmente obtienes un alto rendimiento del curso por el tiempo que le dedicas. Resumiendo: un curso muy recomendable.

27 abril 2025

Navegación 2D en Unity

Una persona mirando un mapa ante un laberinto
Hace un par de meses, publiqué un artículo sobre cómo usar el sistema de navegación 2D en Godot. Va siendo hora de que escriba un artículo análogo para el caso de Unity.

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
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. 

El problema es que en 2D no funcionan los componentes que sí sirven en 3D. No hay avisos, ni mensajes de error. Sencillamente, sigues las instrucciones de la documentación y el NavMesh no se genera si estás en 2D. Al final, tras mucho buscar por Internet, he llegado a la conclusión de que la implementación nativa de Unity para los NavMesh 2D está rota. Sólo parece funcionar para 3D. 

Por lo que he visto, al final todo el mundo utiliza un paquete open source, externo a Unity, llamado NavMeshPlus. Este paquete implementa una serie de componentes, muy parecidos a los nativos de Unity, pero que sí que funcionan a la hora de generar un NavMesh en 2D. 

El enlace anterior te lleva a la página en GitHub del paquete, donde se explica cómo instalarlo. Hay varias maneras de hacerlo, la más sencilla quizás es añadir la URL del repositorio usando la opción "Install package from git URL..." del icono "+" del package manager de Unity. Una vez que lo hagas, y se refresque el índice de paquetes, podrás instalarte NavMeshPlus, así como sus posteriores actualizaciones. 

Opción para añadir un repositorio de git al package manager de Unity.
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:

  1. Crear un GameObject vacío en la escena. No deberá depender de ningún otro GameObject.
  2. 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.
  3. 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.
  4. 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.
  5. 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
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
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
Navigation Modifier aplicado a los tilemaps con obstáculos

Al marcar el área como "Not Walkable" estamos diciendo que lo que pinta este tilemap son obstáculos. Si fuera una zona transitable, comoe le tilemap de  Ground, la pondríamos a Walkable.

Una vez configurados todos los Navigation Modifier, ya podemos crear nuestro NavMesh pulsando en el botón "Bake" del componente Navigation Surface. Para poder verlo, hay que pulsar en el icono de la brújula del toolbar inferior (es el segundo por la derecha del toolbar) de la pestaña escena. Eso abrirá un panel emergente a la derecha donde podremos marcar el check "Show NavMesh". Si el NavMesh se ha generado correctamente entonces aparecerá en la pestaña escena, superponiéndose al escenario. Todas las zonas marcadas en azul serán las transitables por nuestro NPC.

Visualización del NavMesh
Visualización del NavMesh

Uso del NavMesh 2D

Una vez creado el NavMesh 2D, nuestros NPC deben ser capaces de leerlo. 

En el caso de Godot, eso significaba incluir un nodo MeshNavigationAgent2D en los NPC. A partir de ahí, se le iba diciendo a ese nodo a dónde se deseaba ir y este calculaba la ruta y devolvía la localización de las distintas escalas de la ruta. El resto de los nodos del gente se encargaban de moverlo hacia esa localización. 

Unity también cuenta con un componente NavMeshAgent, el problema es que no es pasivo como el de Godot; es decir, no se limita a dar las diferentes escalas de la ruta, sino que se encarga también de mover al agente hacia esas escalas. Eso puede ser muy cómodo en muchas ocasiones cuando el movimiento es sencillo, con un único componente suples dos necesidades: orientas el movimiento y lo ejecutas. Sin embargo, pensado fríamente no es una buena arquitectura porque no respeta el principio de Separación de Responsabilidades, en el que se supone que cada componente debe centrarse en realizar una sola cosa. Mi caso es un buen ejemplo de los problemas que puede haber cuando no se respeta ese principio. Mi proyecto configura fuertemente el movimiento; este no es homogéneo sino que va cambiando a lo largo de una ruta en función de múltiples factores. Es un nivel de personalización que excede lo que que permite el NavMeshAgent de Unity. Si Unity hubiera respetado el principio de Separación de Responsabilidades, como si ha hecho Godot en este caso, habría separado la generación de la ruta y el movimiento del agente en dos componentes separados. De esta manera, se podría haber aprovechado el componente generador de la ruta tal cual, mientras que el del movimiento del agente se podría haber envuelto en otros componentes para personalizarlo de manera adecuada.

Afortunadamente,  hay una manera poco publicitada de hacerle consultas al NavMesh 2D, para sacar rutas sin necesidad de un NavMeshAgent, con lo que se puede replicar la manera de funcionar de Godot. Voy a enfocar este artículo por ese lado, porque es lo que he hecho en mi proyecto. Si lo que te interesa es cómo usar el NavMeshAgent entonces te recomiendo que consultes la documentación de Unity que explica con todo lujo de detalles cómo usarlo. 

Consulta al NavMesh para obtener una ruta entre dos puntos
Consulta al NavMesh para obtener una ruta entre dos puntos

En la captura anterior he puesto un ejemplo de cómo realizar estas consultas. 

La clave está en la llamada al método NavMesh.CalculatePath() de la línea 99.  A ese método se le pasan 4 parámetros:
  • 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.
La llamada a CalculatePath() es síncrona, lo que quiere decir que el juego se detendrá un instante hasta que CalculatePath() calcule la ruta. Para rutas pequeñas y actualizaciones puntuales, la interrupción no afectará al rendimiento del juego; pero si te pasas el rato calculando muchas rutas largas, entonces te encontrarás con que el rendimiento empezará a resentirse. En esos casos, lo suyo es que dividas los trayectos en varios tramos cortos más livianos de calcular. En el caso de formaciones, en vez de hacer que cada miembro de la formación calcule su ruta, es más eficiente que sólo el "comandante" calcule la ruta y el resto se limite a seguirle manteniendo la formación.

La variable de salida de tipo AI.NavMeshPath, en la que CalculatePath() vuelca la ruta calculada, aún se le podría pasar a un NavMeshAgent a través de su método SetPath(). Yo, sin embargo, he preferido prescindir del NavMeshAgent, por lo que he procesado la variable de salida, en el método UpdatePathToTarget() de la línea 107, para que me resultase más fácil de usar.

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.