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.