Skip to content

Articles

How to make a day-night cycle in Godot 4

Drive a sun and moon lighting rig from a Calendar Time GameClock: author times of day, set the timescale, and keep save and load safe.

This recipe builds a full day-night cycle: a clock advances game time, the calendar reports which time of day is active, and a lighting rig turns that into sun color, energy, and ambient tint. Pausing the clock pauses the sky. Saving the clock saves the sky with it.

You need the Calendar Time addon installed and a working clock first. If you have not set one up yet, follow Getting Started and come back.

1. Author your times of day

The calendar owns the cycle schema. Open your GameCalendar resource and define one TimeOfDay entry per stage you want players to see: dawn, day, dusk, and night at minimum. Each entry carries the hours it covers plus a tint_color the lighting reads later.

Keep the list short. Four to six stages is enough for a readable cycle, and every stage you add is one more lighting preset to tune. Name stages for what the player sees ("dawn", "night"), not for internal indices.

2. Drive the clock with a TimeHost

Add a TimeHost to your main scene and assign your GameClock to its clocks list. The host is the only thing that advances the clock, so there is exactly one place where time comes from:

# Pause this clock while menus stay responsive.
clock.speed_multiplier = 0.0

# Resume the same clock.
clock.speed_multiplier = 1.0

# Run every clock on this host at double speed.
time_host.time_scale.delta_multiplier = 2.0

Use clock.speed_multiplier to pause or resume one timeline. Use time_host.time_scale.delta_multiplier to change the pace of every clock the host drives. Never use Engine.time_scale for day and night: it changes physics and animation pacing for the whole engine session, and it gives you a second clock to keep in sync.

Pick a timescale that fits your game. A common starting point is one game hour every 30 real seconds: days last 12 real minutes, and lighting transitions stay slow enough to read as atmosphere rather than flicker.

3. Add the sun light

Sun and moon lighting ships in the demo download (demo/lighting_rigs/), not the plugin ZIP. Copy the rig files into your project. For 2D, build this scene tree:

World (Node2D)
├── TimeOfDayAmbience (CanvasModulate)
├── TimeOfDayDirectionalLight2D (DirectionalLight2D)
└── LightingRig2D (Node, script: time_of_day_lighting_rig_2d.gd)

Assign the same GameClock the host drives, then point the rig at the CanvasModulate and the directional light and give it a TimeOfDayLightingSettingsGroup2D. For 3D, the equivalent tree binds a DirectionalLight3D and a WorldEnvironment through the 3D rig script. Each time of day gets one settings entry, and the rig blends between the previous and current entries across the transition.

One layering rule for 2D: CanvasModulate multiplies every pixel while DirectionalLight2D adds light on top. Let the ambient color carry the tint and keep the light color near white, or the two layers compound into an oversaturated frame. Night energy around 0.08 and day energy around 0.45 is a sane starting pair.

Full field maps for both rigs live in Connect Custom Lighting to the Clock, and the shipped presets are catalogued in Time-of-day lighting rigs.

4. React to transitions without owning them

Your game code should read the cycle, never compute it. The TimeHost creates one DayNightCycleService per clock, and the clock signal bus announces every change:

  • time_of_day_changed(tod, last_tod) fires with both TimeOfDay resources, so handlers read authored data instead of deriving the stage.
  • day_night_transition_progress_changed(progress) hands you a GameTimeProgress whose updated(ratio) signal runs from 0 to 1 across the blend.
  • date_time_changed(new, old) fires every tick for per-frame bindings like a 3D sun angle recomputed from clock.date_time().

Sync from the current clock instant in _ready() before connecting, because the change signals only fire on change. Disconnect everything in _exit_tree(). The script example in Connect Custom Lighting to the Clock shows this lifecycle in full and is safe to copy.

5. Keep save and load safe

Save the clock through the host authority, not by storing hours and minutes in your own variables:

var save_data: Dictionary = time_host.get_group_serializer().to_dict()
time_host.load_state(save_data)

Restore order matters. Restore the Calendar Time clock first through TimeHost.load_state(), then restore game-domain state, then re-evaluate anything derived (shop hours, NPC schedules) from clock.date_time() or the next clock_state_loaded callback. Anything that subscribes to clock_state_loaded picks up the restored instant at once instead of waiting for another tick.

Never persist a second copy of the current time, an elapsed-seconds counter, or a derived flag like "is night". Those are views of the clock. They go stale the moment the clock changes without them.

Common mistakes

  • Driving the sky from _process(delta) accumulation instead of the clock. Two time sources drift apart; the clock alone decides.
  • Using Engine.time_scale as the day-night pause button. Pause the clock.
  • Writing per-stage if hour > 18 chains in game code. Author stages on the calendar and read TimeOfDay resources from the signals.
  • Tinting hard in both CanvasModulate and the directional light. Pick one layer to carry color.
  • Running an automatic TimeHost and a manual clock driver at the same time. Choose one driver per clock.

Try the demo

The Calendar Time demo runs this exact recipe: a clock driving day and night lighting with adjustable timescale, pausable and save-safe. Try it in your browser on the Calendar Time itch.io page before wiring your own scene.