Simple Day/Night Cycle in Godot: A Script-Driven Approach
The best games immerse us. A convincing day/night cycle can dramatically elevate that immersion. While many approaches exist, from complex shaders to pre-baked lighting, I argue that a simple, script-driven cycle, expertly executed, provides the optimal balance between visual impact and performance, especially for lower-end hardware or mobile platforms. Let’s craft that system in Godot.
Setting Up the Godot Scene
The most-downloaded assets on Wayline this month — sorted by what real developers actually use.
Start with a new Godot 4 project. Create a 3D scene with a WorldEnvironment node for ambient lighting control, a DirectionalLight3D to represent the sun, and a Camera3D for viewing the scene. Add a MeshInstance3D with a PlaneMesh to act as the ground.
Name the DirectionalLight3D node “Sun.” We’ll be manipulating its rotation and intensity via script. The WorldEnvironment node lets you control the ambient light – the general light level in the scene.
Pitfall alert: Forgetting the WorldEnvironment is a common mistake. Without it, your scene will be pitch black when the sun “sets,” regardless of ambient light settings in the editor.
The DayNightCycle Script
Create a new GDScript called DayNightCycle.gd. Attach this script to the root node of your scene (typically the Node3D you named “World”). This script will be the heart of our day/night system.
extends Node3D
@export var day_length_minutes: float = 5 # Length of a full day in minutes
@onready var sun: DirectionalLight3D = $Sun
@onready var world_environment: WorldEnvironment = $WorldEnvironment
var time_in_seconds: float = 0.0
var day_length_seconds: float
var sun_initial_rotation: Quaternion
func _ready():
day_length_seconds = day_length_minutes * 60.0
sun_initial_rotation = sun.rotation
func _process(delta: float):
time_in_seconds += delta
var time_of_day = fmod(time_in_seconds, day_length_seconds) / day_length_seconds
update_sun_rotation(time_of_day)
update_lighting(time_of_day)
func update_sun_rotation(time: float):
# Rotate the sun around the X axis.
var rotation_degrees = lerp(-90, 270, time)
sun.rotation = sun_initial_rotation.rotated(Vector3(1, 0, 0), deg2rad(rotation_degrees))
func update_lighting(time: float):
# Adjust ambient light and sun intensity.
var sun_intensity = calculate_sun_intensity(time)
sun.light_energy = sun_intensity
var ambient_light_color = calculate_ambient_light_color(time)
world_environment.environment.ambient_light_color = ambient_light_color
func calculate_sun_intensity(time: float) -> float:
# Intensity peaks at midday, dims to zero at night.
var intensity = sin(time * PI)
return maxf(intensity, 0.0) * 2.0 # Scale intensity.
func calculate_ambient_light_color(time: float) -> Color:
# Adjust the ambient light color.
var intensity = sin(time * PI)
var base_color = Color(0.1, 0.1, 0.2) # Night color
var day_color = Color(0.7, 0.7, 0.8) # Day color
return base_color.lerp(day_color, maxf(intensity, 0.0))
This script exports day_length_minutes, allowing you to adjust the cycle speed directly from the Godot editor. It calculates the sun’s rotation based on the elapsed time, creating a smooth day/night transition.
Fine-Tuning the Lighting
The key to a compelling day/night cycle lies in the lighting. The script adjusts both the sun’s intensity and the ambient light color. The calculate_sun_intensity function uses a sine wave to smoothly transition the sun’s brightness from zero at night to full intensity at midday. The calculate_ambient_light_color function similarly blends between a dark blue night color and a brighter, neutral day color.
Challenge: Notice how the sun intensity and the ambient light are linked. This creates a more believable effect. Experiment with different color combinations and intensity curves to achieve the desired mood. For example, you might add a subtle orange tint during sunrise and sunset.
Optimizing Performance
Even a simple script can impact performance if not optimized. The current script is efficient, but consider these optimizations for larger scenes or mobile platforms:
- Avoid
_process: If you only need to update the lighting once per second (or less), use aTimernode instead of_process. - Simplify calculations: The sine wave is relatively cheap, but complex math can add up. If possible, use lookup tables or simpler approximations.
Real-World Applications
This system is ideal for open-world games, simulations, or any application where a dynamic time-of-day is required. Imagine a farming simulator where crop growth is tied to the day/night cycle, or a survival game where darkness brings increased danger.
Concrete example: In a game where plants grow, the update_lighting function could also influence plant growth rates. Plants could grow faster during the day and slower at night, adding a layer of realism and strategic depth.
Common Mistakes and How to Avoid Them
- Sudden transitions: Avoid abrupt changes in light intensity. Smooth transitions are crucial for visual appeal. The sine wave in
calculate_sun_intensityis one way to achieve this. - Unrealistic colors: The ambient light color should complement the sun’s color. Avoid jarring color combinations.
- Ignoring shadows: Adjust shadow strength and color to match the time of day. Softer shadows at sunrise and sunset look more natural.
Implementing a convincing day/night cycle doesn’t require complex techniques. A simple, well-crafted script, combined with careful lighting adjustments, can create a surprisingly immersive experience. Don’t underestimate the power of simplicity.