13 diciembre 2024

Introducción a los sistemas de partículas en Godot (2ª parte) - La lluvia

En uno de mis artículos anteriores conté cómo podíamos utilizar los sistemas de partículas de Godot para simular la caída de las flores de un cerezo. Se trataba de un efecto muy sutil pero que encierra los principios básicos de lo que ofrecen los sistemas de partículas. Otro efecto mucho más espectacular, y habitual, es el de la lluvia. Si nos paramos a pensarlo, incluir lluvia en nuestro juego, usando un sistema de partículas, es muy sencillo. En este artículo veremos cómo.

Como en los otros artículos, he dejado el código y recursos de este en GitHub. Te recomiendo que te lo bajes y lo abras en el editor de Godot, para poder seguir cómodamente lo que te cuente aquí. El código está en el repositorio de SakuraDemo, aunque te recomiendo que te bajes este commit concreto para estar seguros de que veas exactamente la versión que voy a explicar.

Hecho lo anterior, ya puedes abrir el proyecto en Godot. La novedad, respecto a los ejemplos anteriores de SakuraDemo es que he introducido tres nodos nuevos: dos emisores de partículas (Rain y RainSplash) y un LightOccluder2D (WaterRainCollider), aunque  he deshabilitado este último haciéndolo invisible en el editor (cerrando el icono del ojo en la jerarquía) y fijando su Process--> Mode a Disabled en el inspector (para que el nodo no ejecute lógica alguna en cada frame).

Jerarquía de la escena
Jerarquía de la escena

En este artículo explicaremos los dos emisores de partículas. El primero, Rain, es el que genera las gotas de lluvia, mientras que el segundo, RainSplash, se activa en el punto en el que una gota deja de existir, para simular las pequeñas salpicaduras que se producirían cuando lloviese sobre el agua del lago. En términos de sistemas de partículas, se denomina sub-emisor. Se denominan así porque se originan en los puntos de finalización (bien por fin de vida o por colisión) de las partículas de un sistema de partículas padre.

Hemos dicho que las partículas pueden dejar de existir porque expire su tiempo de vida o bien porque colisionen. Lo que pasa es que los sistemas de partículas se gestionan en la GPU, la cual no tiene conocimiento de los CollisionShape que pueblan nuestro escenarios, ya que estos últimos se gestionan en la CPU. Lo que sí pueden detectar esas partículas son los polígonos que definamos en pantalla con un LightOccluder2D. En teoría, cuando una partícula toque alguno de los lados de un LightOccluder 2D dejará de existir. La práctica resulta ser algo más compleja. Si las partículas son demasiado pequeñas y rápidas, pueden atravesar los lados de un LightOccluder2D sin detectarlos. Para resolverlo hay que incrementar el tamaño físico (que no visible) de las partículas aumentando el parámetro Collision--> Base Size, del nodo GPUParticles2D del sistema de partículas. Si pruebas a aumentar el tamaño de la partícula dentro de la simulación de físicas verás que las partículas penetran menos en el polígono del LightOccluder2D. Otra dificultad de los nodos LightOccluder2D es que, en mi opinión, sirven para simular superficies, sobre las que puede chocar tu lluvia, sólo si tu juego es puramente lateral. Sin embargo, en una escena con perspectiva como esta no queremos que nuestras gotas choquen contra el borde de un polígono, sino sobre la zona visual que cubre el sprite del agua. Es cierto, que reduciendo el tamaño físico de las partículas puedes hacer que estas entren en la zona del agua, pero no he observado mucha diferencia respecto al efecto conseguido al hacer aleatorio el tiempo de vida de las partículas, como vamos a ver a continuación. Esa es la razón por la que, al final, he acabado desactivando el LightOccluder2D. Aun así te recomiendo que lo actives y juegues con el tamaño físico de las partículas para que puedas comprobar su efecto. 

Forma del LightOccluder2D
Forma del LightOccluder2D


Vamos a centrarnos en el efecto principal de lluvia. Si accedes al nodo Rain, verás que he hecho las siguientes configuraciones:

  • Amount: Fijado a 1000 para que haya una cantidad abundante de gotas.
  • Sub Emitter: Aquí debe arrastrarse el nodo con el sistema de partículas que queremos que se active cuando mueran nuestras gotas de lluvia. En este caso será RainSplash, que veremos más adelante en este artículo.
  • Time --> Lifetime: He puesto 0,7 porque es el tiempo mínimo que necesita una gota de agua para llegar desde el punto de generación, encima del borde superior de la pantalla, hasta la zona de la pantalla donde está el agua. 
  • Time --> Preprocess: Quedaría raro si las gotas aparecieran casi paradas en el borde superior de la pantalla y acelerasen desde allí. Intuitivamente, lo esperable es que la gota ya venga a mucha velocidad, al haberse originado a gran altura sobre la pantalla. Esa aceleración previa de las gotas se consigue con este parámetro, que viene a decir "haz aparecer las partículas con las características físicas como si se hubieran empezado a caer 0,5 segundos antes". En mi caso, a base de prueba y error, han bastado 0,5 segundos para conseguir un efecto creíble.
  • Drawing --> Visibility Rect: Es el rectángulo en el que serán visibles las partículas del sistema. En mi caso, quería que la lluvia cubriese la pantalla así que he configurado un rectángulo que abarca toda la pantalla.
  • CanvasItem --> LightMask y Visibility Layer: He situado el sistema de partículas en su propia capa. En este caso concreto, creo que tiene no afecta en nada, pero me gusta tener las cosas ordenaditas.
  • Ordering --> Z Index: Este parámetro es importante. Se supone que nada debe tapar a las gotas de agua, así que su Z-Index debe estar por encima de la del resto de sprites de la imagen. Por esa razón lo he fijado a 1.200. 

Como en el caso del sistema de partículas de las flores del cerezo, el ParticleProcessMaterial que hayamos creado para el parámetro Process Material tiene sus propios parámetros:

  • Lifetime Randomness: Al ampliarlo aumentará el rango de valores posibles para el lifetime. El valor que pongamos aquí debe restarse de 1.0 para calcular el extremo inferior del rango. En mi caso, he fijado 0.22, lo que significa que las gotas se iniciaran con valores de lifetime aleatorios entre 0.78 * lifetime y 1.0 * lifetime. Con este parámetro podemos conseguir que las gotas  cubran todo el sprite del lago. El valor inferior del rango son las gotas que alcanzarán la orilla del lago y el valor superior las gotas que vivirán lo suficiente para alcanzar las cercanías del borde inferior de la pantalla. Todos los valores intermedios se distribuirán por el la superficie de pantalla que ocupa el lago.
  • Disable Z: Estamos en una escena 2D. No tiene sentido que las partículas se desplacen en el eje Z.
  • Spawn --> Position --> Emission Shape: He fijado Box porque quiero que las partículas se generen, más o menos al mismo tiempo, a lo largo de todo el borde superior de la pantalla. Eso encaja muy bien con el volumen de una caja alargada, longitudinal a la pantalla.
  • Spawn --> Position --> Emission Box Extents: Permite definir la forma de la caja desde la que se generarán las partículas. En mi caso es estrechita (Y=1) pero lo suficientemente alargada para cubrir el borde superior de la pantalla (X=700).
  • Accelerations --> Gravity: Se supone que las gotas de lluvia se generan en el cielo, por lo que, para cuando llegan al suelo, bajan a gran velocidad. Podemos simular esa velocidad tan elevada disparando el valor de la gravedad a 3.000 en el eje vertical (Y).
  • Display --> Scale: Sería muy aburrido si todas las gotas fueran del mismo tamaño. En vez de eso, les he dado variedad fijando un tamaño mínimo de 0.2 y un máximo de 1.
  • Display --> Scale Over Velocity curve: La aceleración de las gotas resulta más visual si hacemos que las gotas se alarguen conforme se van acelerando. Eso se consigue con este parámetro, si creas un recurso de curva y haces que esta se eleve, en la dimensión Y, conforme se acerque a la velocidad máxima. En mi caso he hecho que las gotas tengan un tamaño inicial de 15 al iniciar su caída y de 30 cuando lleguen al 25% de su velocidad máxima.

Curva para escalar el tamaño en el eje Y de la partícula conforme se incremente su velocidad.
Curva para escalar el tamaño en el eje Y de la partícula conforme se incremente su velocidad.

  • Display --> Color Curves --> Color Initial Ramp: Otra manera de darle variedad a las gotas es que tengan colores ligeramente diferentes. con este parámetro podemos fijar un gradiente, para que cada gota adopte un color de él.
  • Display --> Color Curves --> Alpha Curve: Otra curva. En este caso para variar la opacidad a lo largo del tiempo de vida de la partícula. Yo he configurado una campana de manera que la partícula es transparente al comienzo y final de su vida, pero visible en la franja intermedia.
  • Collision --> Mode: Define qué hacer con la partícula al colisionar. Si usases un LightOccluder2D para simular zonas de impacto, lo normal sería que quisieras que la gota desapareciese al impactar, por lo que fijarías "Hide On Contact". Sin embargo, si estuvieses simulando otro tipo de partículas, y quisieses que estas rebotasen, deberías utilizar "Rigid".
  • Sub Emitter --> Mode: Define cuándo activar el sistema de partículas secundario. Es decir, el subemisor. En nuestro caso recuerda que se trata de RainSplash. He fijado el parámetro en "At End" para que el sistema de partículas secundario se active en la posición en la que se extinga una partícula por finalización de su tiempo de vida. Si hubiéramos simulado colisiones, aquí deberíamos haber puesto "At Collision".
Si sólo quisiéramos simular las gotas, lo podríamos dejar aquí; pero como he ido comentando a lo largo del artículo, queremos simular las salpicaduras que producirían las gotas del agua al chocar con el agua del lago. Por eso, tenemos que configurar el nodo RainSplash, el sistema de partículas que se activará cada vez que se extinga una gota.

Al igual que con el nodo Rain, hay dos niveles de configuración, primero a nivel general del nodo y luego a nivel particular del Process Material.

A nivel general, los parámetros que he usado son:
  • Emitting: Lo he fijado a falso, porque ya se encargará de la activación el nodo Rain, cada vez que se extinga una de sus gotas.
  • Amount: He fijado 200, aunque es un efecto tan sutil y rápido que no hay grandes diferencias entre un valor de este parámetro u otro.
  • Time --> Lifetime: El efecto de salpicadura es tan rápido, que he dejado este tiempo en 0.2
  • CanvasItem --> LightMask y Visibility Layer: He djado estas salpicaduras en la misma capa de la lluvia.
  • Ordering --> > Index: Lo he situado por encima de las gotas de lluvia, en un valor de 1.300.

En cuanto a nivel de Process Material, bastaría con:

  • Lifetime Randomness: Lo he fijado a 1 para que el nivel de aleatoriedad de del tiempo de vida de las salpicaduras sea máximo.
  • Spawn --> Velocity --> Direction: Para que las salpicaduras asciendan antes de caer, he fijado un vector inicial de velocidad de (0, -1, 0)
  • Spawn --> Velocity --> spread: En mi caso, he notado poca diferencia al modificar este parámetro, pero en teoría te debería permitir aleatorizar el vector inicial de velocidad en un rango angular.
  • Accelerations --> Gravity: Las salpicaduras deben caer rápido, pero no tan rápido como las gotas. Así que yo lo he dejado en 500.
  • Display --> Color Curves --> Alpha Curve: He dejado una curva similar a la del nodo Rain.

Y ya está. Con esta configuración he conseguido un efecto de lluvias y de salpicaduras suficientemente convincente. Sin embargo, no dio que esta no sea ni la única, ni la mejor manera de hacerlo. Todos los parámetros que hemos tocado se pueden fijar a valores diferentes a los míos, y hay muchos otros que ni siquiera hemos tocado. Te animo a que experimentes con otros parámetros y valores, y que veas el efecto que tienen. No me cabe duda alguna de que, a poco que le dediques algo de tiempo, conseguirás efectos mejores que el mío.

Efecto de lluvia resultante
Efecto de lluvia resultante