How to Make a 2D Rain Effect in the Unity Engine
Rain in video games can enhance the atmosphere significantly, even in 2D games. However, It’s not clear how to make rain in a video game and using the wrong technique can hurt performance. Fortunately, rain is very straightforward to implement using the particle system, and we will show you how to do this efficiently in the Unity engine step-by-step.
Project Setup
First we create the project, open the Unity Hub and create a project using the 2D (Built-In Render Pipeline) template.
You should now have a new project with a Main Camera object.
Scene Setup
Import the Textures
Let’s setup the scene. Download all the images by right-clicking each one of the images and clicking Save Image As.. and add them to your Assets folder inside your Unity project. You can also click each image to open them in a new tab and save them. We will use these textures to create the scene but also the rain as well.
Your Project tab now should look like this in:
Create the Background and Ground Sprites
Right-click the Hierarchy and add a Square sprite by selecting 2D Object -> Sprites -> Square.
Select the object and do the following:
- Rename it to Background.
- Reset the transform of Background by going to the Transform section in the Inspector, clicking the 3 dots and selecting Reset.
- Set the z position value to 1 so that it’s always behind the rain and ground.
Let’s create a material for the background:
- Right-click inside the Assets folder and select Create -> Material.
- Rename it to BackgroundMaterial.
- Select the material and change its shader in the Inspector by selecting Unlit -> Texture.
Select the Background object then:
- Drag the BackgroundMaterial in your Assets and drop it in the Material slot.
- Drag the Sky texture and drop it on the Sprite slot in the inspector.
Let’s create another square sprite representing the ground:
- Right-click the Hierarchy and add a Square sprite by selecting 2D Object -> Sprites -> Square.
- Rename it to Ground.
- Reset its transform as you did before.
- Set its scale to 10 on the x axis so that it stretches along the window.
- Move it all the way to the bottom, at around -4.5 in the y-axis, such that the lower edge lines up with the bottom of the view.
Let’s create a material for the Ground object:
- Create a new material as you did above and call it GroundMaterial.
- Set its Shader to Unlit/Texture.
Select the Ground texture and:
- Set the Wrap Mode section to Repeat. This will allow us to tile the texture when we set it to the ground.
- Click Apply.
Click the Ground object in the Hierarchy and:
- Drag and drop the GroundMaterial to the Material section.
- Drag and drop the Ground texture to the Sprite spot.
- The texture will look stretched because it’s a square texture but the ground sprite is rectangular. To fix this, increase the x value of the Tiling under the Material section until it looks good. Setting it to 5 should work ok.
Create the Rain Particle System
Now it’s time to create the rain:
- Right-click the Hierarchy and create a Particle System by selecting Effects -> Particle System.
- Rename it to RainParticles.
- Reset its transform.
Set the following:
- Set the Start Speed to 0.
- In the Start Size section, click the arrow and choose Random Between Two Constants and set the left value to 0.5 and right value to 1.
Set the following:
- Go to the Velocity over Lifetime section, click the checkbox to enable it.
- Click the arrow next to Linear and choose Random Between Two Constants. Set the top value to (0, -10, 0) and the bottom value to (0, -15, 0). The negative values on the y-axis will make the particles fall down. We’re setting them to a random value between -10 and -15 such that not all the particles fall at the same speed, resulting in a more realistic and interesting effect.
The particles are being spawned from the center, so we need to move up the particle system such that it appears like the rain is coming from above. Set the y value of the position in the Transform section to around 5.5, or whatever works for your scene such that the particle system is above the background sprite.
Also notice how the particles are spawned in a single line, we need to spread them apart on the x-axis:
- Go to the Shape section.
- Set the Shape to Box.
- Set the Scale to 10 on the x-axis.
Notice the rain is falling too low way below the ground, to fix this set the Start Lifetime to 0.85.
Ok it’s starting to vaguely look like rain:
Let’s improve the shape of particles:
- Create a new material.
- Rename the material RainParticleMaterial.
- Set the Shader to Particles -> Standard Unlit.
- Set the Rendering Mode to Fade.
- Drag and drop the RainDrop texture to the Albedo section (see picture below).
- Click the color box to the right of Albedo and set the alpha value to 128.
Select the RainParticles object in the Hierarchy and do the following:
- Go to the Renderer section.
- Assign the RainParticleMaterial to the particle system by dragging and dropping it in the Material sections.
- Set the Order in Layer to 1 so that the rain is always drawn on top of everything else.
Looks better but the particle size is wrong, let’s fix this:
- Go to the Size over Lifetime section.
- Click the checkbox next to Size of Lifetime to enable it.
- Click the Separate Axes checkbox.
- Click the arrow icon and select Random Between Two Constants.
- Set the top value to (0.05, 0.1, 1).
- Set the bottom value to (0.1, 0.4, 1).
Looks better, but very subdued. Go to the Emission section and set the Rate over Time to 200.
Finally, we will add some fading towards the end of the lifetime of particles to make it look like it’s dissipating. Let’s add the final touch:
- Go to the Color over Lifetime section.
- Click the checkbox next to Color over Lifetime to enable it.
- Click the color box to the right of the Color spot.
- Now you will get a Gradient Editor window popup. Click the top right handle (see picture below on the right).
- Set the alpha value to 50.
You should now have something like this:
Now it looks like rain. Of course you can do much more to it using the particle system properties, however, this will give you a good template to build on. Next we will add a splash effect to the rain particles.
Adding Rain Drop Splash Effect
You can enhance the visuals of the rain by adding particle collision. This will give the effect of rain drop splashes when they hit the ground or another object.
First let’s turn on the collision detection:
- Go to the Collision section and click the checkbox next to it to enable it.
- Set the Type to World.
- Set the Mode to 2D.
- Set the Bounce to 0. We don’t want the particles to bounce off the colliders.
- Set the Lifetime Loss to 1. This will make the particle disappear instantly when it touches a collider.
- Set the Radius Scale to 0.1. Lower values make the collision hit points more accurate.
- Check the Send Collision Messages checkbox. This will allow us to receive collision callbacks in the C# script that we will create later on.
To make this work we need to add a collider to the ground object:
- Select the Ground object from the Hierarchy.
- In the Inspector, click on the Add Component button.
- Select Box Collider 2D.
You should notice that now the particles do not go below the ground because they are intersecting with the collider.
Now we create the splash effect when the raindrops hit the ground, or any collider:
- Right-click the Hierarchy and select Create Empty.
- Rename it to Splash.
- Reset its transform.
We will now animate a splash:
- Select the Splash object you just created.
- Click on the Animation tab.
- Click on Create.
- When the dialog appears, name the animation to SplashAnimation and click Save to save it to your assets folder.
Now we will drag and drop our splash sprites to the animation timeline. Before we can do that, we need to create a new tab in order to have the Animation tab and the Project tab visible at the same time, otherwise we can’t drag and drop. Click on the 3 dots at the top right section of the Inspector tab and select Add Tab -> Project.
This should open a new Project tab on the right of the screen:
Now we can create our animation:
- Drag the Splash1 sprite and place it in the animation timeline at time 0:00.
- Drag the Splash2 sprite and place it in the animation timeline at time 0:02.
- Drag the Splash3 sprite and place it in the animation timeline at time 0:04.
- Drag the Splash4 sprite and place it in the animation timeline at time 0:06.
- Now we reverse the order, drag the Splash3 sprite and place it in the animation timeline at time 0:08.
- Drag the Splash2 sprite and place it in the animation timeline at time 0:10.
- Drag the Splash1 sprite and place it in the animation timeline at time 0:12.
If you play the animation it will look something like this:
You might think that it looks bad, but when we add it later in the scene with the correct scale it will look much better. Set the scale of the Splash object to 0.1 on the x and y axis.
Create a C# script for the splash by right-clicking the Assets folder and selecting Create -> C# Script and rename it to SplashScript.
Double-click the script to open it in Visual Studio or any other text editor and paste the code below and save it:
using UnityEngine;
public class SplashScript : MonoBehaviour
{
public void OnSplashAnimationFinished()
{
Destroy(gameObject);
}
}
This code defines a method called OnSplashAnimationFinished. When the splash animation is done, we don’t need it anymore, so we call Destroy on its gameObject here to remove it from the scene.
Go back to the editor and attach the SplashScript by dragging and dropping it to the Splash object.
Now we connect our animation to the script:
- Select the Splash object.
- Go to the Animation tab.
- Right-click above the last key and click on Add Animation Event. Make sure the animation event is on the 0:12 time.
In the inspector, set the Function setting to SplashScript -> Methods -> OnSplashAnimationFinished(). Now when this last key is reached, our method will be called and will destroy the splash object.
Click on SplashAnimation in the Assets folder and un-check the Loop Time setting because we don’t want this animation to loop, once it splashes its animation is done.
Now drag and drop the Splash object from the Hierarchy to the Assets folder to create a Prefab. A prefab is an object that you can later re-use without having to re-create it and its properties all over again.
Now delete the Splash object from the Hierarchy since we don’t need it anymore.
What we want is to instantiate the Splash prefab you just created everytime the rain particle collides with the ground of any collider. Create a script by right-clicking the Assets folder and selecting Create -> C# Script and rename it to RainParticleScript.
Double-click the script to open it in Visual Studio and paste the code below and save it:
using System.Collections.Generic;
using UnityEngine;
public class RainParticleScript : MonoBehaviour
{
[SerializeField]
private GameObject splashPrefab;
private ParticleSystem particleSystem;
void Awake()
{
particleSystem = GetComponent<ParticleSystem>();
}
void OnParticleCollision(GameObject other)
{
if (splashPrefab && other.CompareTag("CollidesWithRain"))
{
List<ParticleCollisionEvent> collisionEvents = new List<ParticleCollisionEvent>();
int numCollisionEvents = particleSystem.GetCollisionEvents(other, collisionEvents);
for (int i = 0; i < numCollisionEvents; i++)
{
Vector3 collisionPosition = collisionEvents[i].intersection;
Instantiate(splashPrefab, collisionPosition, Quaternion.identity);
}
}
}
}
Let’s see what this code is doing: in the Awake method, it gets the ParticleSystem component attached to the RainParticles object.
The OnParticleCollision method is a built-in method called every time a particle (rain drop) collides with any collider in the scene. Inside the method, it checks if the object it collided with has the tag CollidesWithRain which we will create shortly. If the tag is correct, then it instantiates a splash otherwise nothing happens. Checking the tag is useful for many reasons, such as:
- Performance. We don’t want to instantiate a new object for every collider that the rain particles collide with as it might deteriorate performance if there are too many objects with colliders in the scene.
- Game logic. Maybe you have some objects in the scene that should not collide with rain. For example, if you have a ghost character in your game then it wouldn’t really make sense to make rain collide with it.
- Special behaviour. Maybe you want to do something different when the particle collides with a wall than when the particle collides with the ground.
Now if the object the particle collides with has the tag CollidesWithRain, the code retrieves all the particle collisions related to this object using the method GetCollisionEvents. then it simply loops over all the collision events and instantiates a splash at the position of the intersections. When the splash is instantiated, it automatically plays the splash animation. Remember that we’ve already added the code that destroys the splash object once the animation is done in SplashScript.cs, so we don’t need to keep track of the splash objects in this script.
Go back to the Unity editor and do this:
- Drag the RainParticleScript.cs and drop it on the RainParticles object.
- Select the RainParticles object and in the inspector scroll down to the RainParticleScript component. Then drag the Splash prefab and drop it on the Splash Prefab property.
We need to assign the CollidesWithRain tag to our ground object. Select the Ground object and at the top of the Inspector, click on the Tag dropdown and select Add Tag…
Create the tag as follows:
- Press the + button.
- Type CollidesWithRain in the New Tag Name text field.
- Click Save.
Select the Ground object again and click on the Tag dropdown and select the CollidesWithRain tag.
Now play the game, you should see the splash animations when the rain particles hit the ground:
You can play around with it by adding any sprite with a collider in the scene. Don’t forget to set the Tag properly.
So there it is, now you know how to implement rain using a particle system and a simple couple of scripts. You also know how to add collisions to your particles. The last thing we’re going to discuss is code optimization.
Code Optimization
The code we have instantiates then destroys splash objects repeatedly using the Instantiate and Destroy methods. While this works, it is inefficient and can hurt the performance of your game, especially if you have lots of collisions in your scene. This is because frequent memory allocations and deallocations can cause frequent garbage collection cycles and also memory fragmentation. Both of these make it hard for the system to manage memory efficiently.
To improve efficiency, we will implement a technique called object pooling. Basically we pre-instantiate a list of splash objects before the game begins and then we pick from this list any splash object that is not currently being used and activate it. This way, we avoid calling Instantiate and Destroy repeatedly which is a huge improvement over the previous implementation.
Let’s start with the RainParticleScript.cs script, this is the new updated code with object pooling. Replace the old code by copying and pasting it inside your RainParticleScript.cs, then save it:
using System.Collections.Generic;
using UnityEngine;
public class RainParticleScript : MonoBehaviour
{
[SerializeField]
private GameObject splashPrefab;
private ParticleSystem particleSystem;
const int MAX_NUM_SPLASHES = 10;
public static Queue<GameObject> splashQueue = new Queue<GameObject>();
public static void EnqueueSplash(GameObject gameObject)
{
splashQueue.Enqueue(gameObject);
}
private GameObject GetSplash()
{
if (splashQueue.Count == 0)
{
return null;
}
return splashQueue.Dequeue();
}
void Awake()
{
for (int i = 0; i < MAX_NUM_SPLASHES; i++)
{
GameObject splash = Instantiate(splashPrefab);
splash.SetActive(false);
splashQueue.Enqueue(splash);
}
particleSystem = GetComponent<ParticleSystem>();
}
void OnParticleCollision(GameObject other)
{
if (other.CompareTag("CollidesWithRain"))
{
List<ParticleCollisionEvent> collisionEvents = new List<ParticleCollisionEvent>();
int numCollisionEvents = particleSystem.GetCollisionEvents(other, collisionEvents);
for (int i = 0; i < numCollisionEvents; i++)
{
Vector3 collisionPosition = collisionEvents[i].intersection;
GameObject splash = GetSplash();
if (splash)
{
splash.transform.position = collisionPosition;
splash.SetActive(true);
}
}
}
}
}
We now have a Queue object called splashQueue. This queue will hold all the splash objects that are ready to be used. The MAX_NUM_SPLASHES constant defines the max number of splashes that can happen simultaneously in the scene. You can increase or decrease this number as you wish.
The EnqueueSplash method adds a splash object to the queue to signify that it’s ready to be used. The GetSplash method checks if there are any splash objects not currently being used, if yes, it returns a splash object otherwise it returns null, meaning that all splash objects are being used.
The Awake method fills the splashQueue with a bunch of splash prefab objects, then it sets them to inactive. We deactivate them otherwise they will be visible on the screen and we don’t want that. We only activate them when the rain particle collides with something.
The OnParticleCollision method is pretty similar to the code we had before, with the exception that we don’t Instantiate the splash object anymore. We now get a splash using GetSplash and if it’s not null, we simply place it where we want and activate it so that it plays the animation.
The final piece of the puzzle is how to put the splash object back in the queue once we removed it from the queue. This happens in the updated SplashScript.cs script, replace the old code with the one below and save it:
using UnityEngine;
public class SplashScript : MonoBehaviour
{
public void OnSplashAnimationFinished()
{
gameObject.SetActive(false);
RainParticleScript.EnqueueSplash(gameObject);
}
}
Instead of destroying the object, OnSplashAnimationFinished now de-activates the object and puts it back in the queue, signaling that it can be reused.
Run the game and see how the new code works:
Can you tell the difference? Not really, it looks exactly the same as the inefficient code we had before but with the advantage of better performance. Now if you look closely, you will realize that not all the particles are creating a splash animation when they hit the ground. This happens when all the splash objects are being used, i.e. the splash queue is empty. To mitigate this you can simply increase the MAX_NUM_SPLASHES number to your needs. However, even with a low value of MAX_NUM_SPLASHES, the player probably won’t notice that some particles are not playing a splash animation. In the end, this is just a decorative effect and accuracy is not really the objective here.
Conclusion
Once again the particle system comes to the rescue! Many different effects can be efficiently implemented using particles. We covered another one of them here. We have only scratched the surface in this tutorial, you can make the rain effect more interesting if you play around with the other properties of the particle system. Finally, we covered a simple object pooling implementation to improve performance. Pooling is a technique that you will find yourself using in many different applications and is crucial in performance optimization. If you want to learn more about performance optimization, we also have a blog on that here.
We Need Your Help!
Help us increase our apps’ visibility in the app stores. Check out our apps and if there’s anything you like, download it on your phone and try it out. Don’t forget to leave us a review. We appreciate your support, it truly makes a difference!