20 December 2024

Particle systems in Godot introduction (Part II) - The rain

In one of my previous articles, I explained how we could use Godot’s particle systems to simulate the falling of cherry blossom petals. It was a very subtle effect but encapsulates the basic principles of what particle systems offer. Another much more spectacular and common effect is rain. If we think about it, adding rain to our game using a particle system is very simple. In this article, we’ll see how.

As with the other articles, I’ve shared the code and resources for this one on GitHub. I recommend downloading it and opening it in the Godot editor to comfortably follow along with what I explain here. The code is in the SakuraDemo repository, although I recommend downloading this specific commit to ensure you see exactly the version I’ll explain.

Once you’ve done that, you can open the project in Godot. The new feature, compared to previous SakuraDemo examples, is that I’ve introduced three new nodes: two particle emitters (Rain and RainSplash) and a LightOccluder2D (WaterRainCollider). However, I’ve disabled the latter, making it invisible in the editor (by closing the eye icon in the hierarchy) and setting its Process → Mode to Disabled in the inspector (so the node doesn’t execute any logic on each frame).

Scene Hierarchy
Scene Hierarchy

In this article, we’ll explain the two particle emitters. The first one, Rain, generates the raindrops, while the second, RainSplash, activates at the point where a drop ceases to exist to simulate the small splashes that would occur when rain falls on the lake’s water. In particle system terms, this is called a sub-emitter. They’re named this way because they originate at the endpoints (either due to lifespan expiration or collision) of particles in a parent particle system.

We mentioned that particles can cease to exist either because their lifespan expires or because they collide. The thing is, particle systems are managed by the GPU, which doesn’t have knowledge of the CollisionShapes that populate our scene, as these are managed by the CPU. What particles can detect are polygons defined on the screen with a LightOccluder2D. In theory, when a particle touches one of the edges of a LightOccluder2D, it will cease to exist. In practice, it’s a bit more complex. If the particles are too small and fast, they may pass through the edges of a LightOccluder2D without being detected. To solve this, you need to increase the physical (but not visible) size of the particles by adjusting the Collision → Base Size parameter of the GPUParticles2D node in the particle system. If you test increasing the particle size within the physics simulation, you’ll see they penetrate less into the LightOccluder2D polygon.

Another difficulty with LightOccluder2D nodes is that, in my opinion, they’re only useful for simulating surfaces that rain can hit if your game is purely side-scrolling. However, in a perspective-based scene like this one, we don’t want our raindrops to collide with the edge of a polygon but rather on the visual area covered by the water sprite. While reducing the physical size of the particles can allow them to reach the water area, I haven’t observed much difference compared to the effect achieved by randomizing the particles’ lifetimes, as we’ll see below. That’s why, in the end, I ended up disabling the LightOccluder2D. Still, I recommend activating it and experimenting with the particles' physical size to observe its effect.

LightOccluder2D Shape
LightOccluder2D Shape

Let’s focus on the primary rain effect. If you access the Rain node, you’ll see I’ve made the following configurations:

  • Amount: Set to 1000 to ensure a sufficient number of raindrops.
  • Sub Emitter: Drag the particle system node you want to activate when your raindrops die here. In this case, it’s RainSplash, which we’ll discuss later in this article.
  • Time → Lifetime: I set it to 0.7 because this is the minimum time a raindrop needs to travel from its generation point above the top edge of the screen to the area of the screen where the water is located.
  • Time → Preprocess: It would look odd if the drops appeared nearly stationary at the top edge of the screen and then accelerated from there. Intuitively, you’d expect the drop to already be moving at high speed, having originated from a great height above the screen. This parameter allows you to simulate that prior acceleration, making particles appear with physical characteristics as if they started falling 0.5 seconds earlier. Through trial and error, I found 0.5 seconds sufficient to achieve a believable effect.
  • Drawing → Visibility Rect: This is the rectangle within which the particles will be visible. In my case, I wanted the rain to cover the entire screen, so I configured a rectangle spanning the entire screen.
  • CanvasItem → LightMask and Visibility Layer: I placed the particle system in its own layer. For this specific case, I think it has no effect, but I like to keep things tidy.
  • Ordering → Z Index: This parameter is important. Raindrops should not be obscured by anything, so their Z-Index must be higher than the other sprites. For this reason, I set it to 1,200.

As with the cherry blossom particle system, the ParticleProcessMaterial created for the Process Material parameter has its own settings:

  • Lifetime Randomness: Increasing this value expands the range of possible lifetimes. The value you set here is subtracted from 1.0 to calculate the lower bound of the range. In my case, I set it to 0.22, meaning the raindrops will have random lifetimes between 0.78 times the lifetime and 1.0 times the lifetime. With this parameter, we can make the raindrops cover the entire lake sprite. The lower range values represent drops that will reach the lake’s shore, while the upper range values represent drops that will live long enough to reach the bottom edge of the screen. All intermediate values will be distributed across the screen area occupied by the lake.
  • Disable Z: Since this is a 2D scene, particles moving along the Z-axis doesn’t make sense.
  • Spawn → Position → Emission Shape: I chose Box because I want the particles to generate more or less simultaneously along the entire top edge of the screen. This aligns well with the volume of a long box parallel to the screen.
  • Spawn → Position → Emission Box Extents: This defines the shape of the box where particles will generate. In my case, it’s narrow (Y=1) but long enough to cover the top edge of the screen (X=700).
  • Accelerations → Gravity: Raindrops are generated in the sky, so they fall at high speed by the time they reach the ground. We can simulate this high velocity by setting gravity to 3,000 on the vertical axis (Y).
  • Display → Scale: It would be boring if all raindrops were the same size. Instead, I added variety by setting a minimum size of 0.2 and a maximum of 1.
  • Display → Scale Over Velocity Curve: The acceleration of the drops is more visually engaging if they stretch as they speed up. This can be achieved using this parameter by creating a curve resource and making it rise along the Y-axis as it approaches the maximum velocity. In my case, I set the drops to have an initial size of 15 when they start falling and 30 when they reach 25% of their maximum velocity.

Curve to scale particle size along the Y-axis as velocity increases.
Curve to scale particle size along the Y-axis as velocity increases.

  • Display → Color Curves → Color Initial Ramp: Another way to add variety to the drops is to give them slightly different colors. With this parameter, we can set a gradient so each drop adopts a color from it.
  • Display → Color Curves → Alpha Curve: Another curve, this time to vary opacity throughout the particle’s lifespan. I configured a bell curve so the particle is transparent at the beginning and end of its life but visible in the middle.
  • Collision → Mode: Defines what happens when a particle collides. If you’re using a LightOccluder2D to simulate impact zones, you’d likely want the drop to disappear on impact, so you’d set this to Hide On Contact. However, if you’re simulating other types of particles and want them to bounce, you’d use Rigid.
  • Sub Emitter → Mode: Defines when to activate the secondary particle system, the sub-emitter. In our case, this is RainSplash. I set this parameter to At End so the secondary particle system activates at the position where a particle extinguishes due to its lifespan ending. If we had simulated collisions, we’d set it to At Collision here.

If we only wanted to simulate the drops, we could stop here. However, as I’ve mentioned throughout the article, we want to simulate the splashes produced when raindrops hit the lake water. To do this, we need to configure the RainSplash node, the particle system that activates whenever a drop extinguishes.

Just like with the Rain node, there are two levels of configuration: general node settings and specific Process Material settings.

At the general node level, I used the following parameters:

  • Emitting: I set this to false because the Rain node will handle its activation whenever one of its drops extinguishes.
  • Amount: I set this to 200. However, the effect is so subtle and quick that there aren’t significant differences between values for this parameter.
  • Time → Lifetime: The splash effect is so brief that I left this time at 0.2.
  • CanvasItem → LightMask and Visibility Layer: I kept these splashes in the same layer as the rain.
  • Ordering → Z Index: I placed it above the raindrops with a value of 1,300.
At the Process Material level, the settings I used were:

  • Lifetime Randomness: I set this to 1 for maximum randomness in the splashes' lifetimes.
  • Spawn → Velocity → Direction: To make the splashes rise before falling, I set an initial velocity vector of (0, -1, 0).
  • Spawn → Velocity → Spread: In my case, I noticed little difference when modifying this parameter, but in theory, it should randomize the initial velocity vector within an angular range.
  • Accelerations → Gravity: The splashes should fall quickly but not as fast as the raindrops. So, I set it to 500.
  • Display → Color Curves → Alpha Curve: I used a curve similar to the one for the Rain node.

And that’s it. With this configuration, I achieved a sufficiently convincing rain and splash effect. However, this is neither the only nor the best way to do it. All the parameters we’ve adjusted can be set to different values, and there are many others we haven’t even touched. I encourage you to experiment with other parameters and values to see their effects. I have no doubt that, with a bit of time, you’ll achieve effects better than mine.

Final Rain Effect
Final Rain Effect