Daily free asset available! Did you claim yours today?

Dynamic Weather Systems in Godot: Rain, Clouds, and More

July 17, 2025

The biting wind howls. Not just outside your window, but also inside your game world, if you dare to embrace the challenge. Far too many games treat weather as a static backdrop, a mere cosmetic effect. But weather is a system, a dynamic force that should impact gameplay, atmosphere, and player immersion. This article will provide you with the knowledge to implement such a system in Godot.

Setting the Stage: Project Setup and First Steps

Let’s cut through the chase. Start with a new Godot project. I strongly advise using Godot 4 or later, as it offers significant improvements in particle systems and rendering that will be beneficial.

Create a new 2D scene. Add a CanvasLayer node as the root. This ensures our weather effects render above everything else, regardless of camera settings. Rename this CanvasLayer to "Weather". Attach a new GDScript to this node called weather.gd. This will be our central control script.

extends CanvasLayer

@export var rain_particle_scene : PackedScene
@export var cloud_scene : PackedScene

var current_weather = "Clear"
var target_weather = "Clear"
var transition_progress = 0.0
@onready var viewport_size = get_viewport_rect().size

This script introduces a few key concepts. First, the @export variables. These let you drag and drop your rain particle scene and cloud scene directly into the Weather node in the Godot editor. More on these later. The current_weather, target_weather, and transition_progress variables will be used for smooth weather transitions. We also cache the viewport_size to spawn the particles correctly.

Making it Rain (and Snow, and Everything in Between): Particle Systems

Let’s dive into the most visually striking aspect: the particle effects. This is where Godot 4 really shines.

Create a new scene. Add a GPUParticles2D node as the root. Rename it to RainParticles. This will be our rain effect.

The key is in the Process Material. Create a new ParticleProcessMaterial resource and assign it to the process_material property of the GPUParticles2D node. Now, let’s configure it:

  • Emission Shape: Set this to Box.
  • Emission Box Extents: Set the X value to be slightly wider than your game’s viewport width and the Y value to a small value to concentrate the rain initially.
  • Gravity: A strong negative Y value (-500) will make the rain fall quickly.
  • Speed Initial: This controls the initial speed of the particles. Experiment with values around 100-200.
  • Scale Initial: Determines the size of the rain drops. Small values (0.1-0.2) are usually best.
  • Color: Select a light blue or grey color for the rain.

Now, the critical part. Without a texture, it won’t be visible. The easiest approach is to create a simple CanvasItemMaterial and apply it to the GPUParticles2D node under Drawing/Material. Then create a GradientTexture2D, and set the gradient to fade from white to transparent white from top to bottom. Apply the gradient texture to the CanvasItemMaterial.

Save this scene as rain_particles.tscn. Remember to assign this to the rain_particle_scene variable in your weather.gd script.

The Pitfall: One common mistake is setting the Lifetime to a value that’s too short. The rain particles will disappear before they reach the bottom of the screen. A Lifetime of 2-3 seconds is generally a good starting point. Another issue is the emission rate. If too high, you’ll see a dense sheet of rain. Adjust the Amount property in the GPUParticles2D node for a more natural look.

Sky is the Limit: Dynamic Sky Colors

The skybox is as important to weather as the rain itself. We can dynamically adjust the background color to reflect the current weather conditions.

Inside your weather.gd script, add the following dictionary:

var weather_colors = {
    "Clear": Color(0.6, 0.8, 1.0), # Light blue
    "Rainy": Color(0.4, 0.5, 0.6), # Dark grey
    "Cloudy": Color(0.7, 0.7, 0.7), # Light grey
    "Stormy": Color(0.2, 0.2, 0.3)  # Dark grey/black
}

This dictionary defines the background colors for different weather states.

Now, add a function to handle the sky color transition:

func _process(delta):
    if current_weather != target_weather:
        transition_progress += delta
        var t = min(transition_progress / 2.0, 1.0) # Transition over 2 seconds
        var current_color = weather_colors[current_weather]
        var target_color = weather_colors[target_weather]
        var blended_color = current_color.lerp(target_color, t)
        RenderingServer.set_default_clear_color(blended_color)
        if t == 1.0:
            current_weather = target_weather
            transition_progress = 0.0

This function smoothly transitions the background color from the current weather state to the target weather state using linear interpolation (lerp). The RenderingServer.set_default_clear_color function sets the background color of the Godot viewport.

Orchestrating the Storm: Randomized Weather Patterns

The heart of our dynamic weather system lies in the ability to randomly change the weather over time.

Add a function to the weather.gd script to randomly select a new weather state:

func set_random_weather():
    var weather_options = weather_colors.keys()
    var new_weather = weather_options[randi_range(0, weather_options.size() - 1)]
    set_weather(new_weather)

func set_weather(new_weather):
    target_weather = new_weather

This function selects a random weather state from the weather_colors dictionary and sets the target_weather.

To trigger this randomly, add a Timer node as a child of the Weather node. Set its wait_time to a value like 15 (seconds) and its autostart property to true. Connect the timeout signal of the Timer to the set_random_weather function in your weather.gd script.

The most common mistake is forgetting to randomize the random number generator. Call randomize() at the beginning of the _ready() function to ensure different random weather patterns each time you run the game.

func _ready():
    randomize()
    set_random_weather()
    viewport_size = get_viewport_rect().size

Bringing it All Together: Instantiating and Managing Effects

Now, let’s instantiate the rain particles and manage their visibility based on the current weather.

func _process(delta):
    if current_weather != target_weather:
        transition_progress += delta
        var t = min(transition_progress / 2.0, 1.0) # Transition over 2 seconds
        var current_color = weather_colors[current_weather]
        var target_color = weather_colors[target_weather]
        var blended_color = current_color.lerp(target_color, t)
        RenderingServer.set_default_clear_color(blended_color)
        if t == 1.0:
            current_weather = target_weather
            transition_progress = 0.0

    # Rain instantiation
    if current_weather == "Rainy" or current_weather == "Stormy":
        if get_node_or_null("RainParticles") == null:
            var rain = rain_particle_scene.instantiate()
            rain.name = "RainParticles"
            add_child(rain)

    else:
        if get_node_or_null("RainParticles") != null:
            get_node("RainParticles").queue_free()

Going Beyond: Adding Clouds and More

This setup provides a solid foundation. You can enhance the system by adding cloud layers (using Sprite2D nodes and parallax scrolling), lightning flashes (using Timer nodes and screen shake), sound effects, and more. Remember that the key is to tie these elements to the underlying current_weather variable, creating a cohesive and immersive weather experience. This framework is designed to be scalable and to provide a flexible and rich experience.