Devlog #2: Real time day-night cycle

Adding a clock synced to real time can be a creative way to introduce elements from real life into a game. Inspired by real time games like Animal Crossing, the clock in Usagi Shima is synced to the player’s system time to provide a sense of dynamism and “realness” to the player. It is meant to help bring the miniature island to life as opposed to presenting a static, unchanging environment.

The day-night cycle implementation in Usagi Shima is very straightforward. Three components make up the in-game day-night cycle:

  • The overall color palette that gradually changes with the time of day
  • The clock UI
  • All the lights/lanterns in the game

A changing color palette

Palette transitions: Sunrise → Day → Sunset → Night

Godot provides a nifty node called CanvasModulate that enables us to easily change the overall tint of the canvas. Used in conjunction with OS.get_datetime() and linear interpolation with lerp(), we can gradually ease the transition from one Color value to the target Color over real time. I established base colors for daytime, nighttime, sunrise, and sunset, as well as the range of hours each time of day covers. With the colors and phases of the day concretely defined, it’s simple to then linearly interpolate among these values depending on the time of day using lerp().

To perform the gradual color transitions, I attached a script to the CanvasModulate node in my main scene tree and modified CanvasModulate.color over every minute, using lerp() to determine the correct transition color for each minute during the transition period.

Take sunset for example. I’ve defined sunset to start at 5 PM and to last for 2 hours, ending with the night color at 7 PM. I’ve also defined the starting (sunset) and ending (nighttime) colors as follows:

var sunset_color = Color(0.99, 0.85, 0.75, 1) # fcd9bf
var night_color = Color(0.39, 0.51, 0.64, 1) # 6483a4

For the curious, that corresponds to the two colors below to produce the resulting tints in the palette comparison image shown earlier in this section.

Sunset → Night

Using lerp(), we can gradually ease from the sunset color to the night color over a period of 2 hours, using the minute divided by the duration in minutes (minute/duration) as the weight. For example, if it’s 6:30 PM, sunset started at 5 PM, and the sunset duration is 120 minutes total, that means we’ve progressed to minute 90 out of 120 — that is, the resulting weight to lerp() would be 90/120, which is 75% of the way between our sunset color and night color. Once we have reached 7 PM, we will have reached step 120/120, that is 100% of the way and thus fully reaching the final nighttime color.

Additionally, I implemented a separate Time singleton that stores our time of day definitions (i.e., when sunset starts, when nighttime starts, etc.) and regularly checks OS.get_datetime() in its _process() function to keep track of which phase of the day the game is currently in. The time phase of the day (i.e., sunrise, sunset, nighttime, etc) is represented as an enum and can be queried from the Time singleton via Time.get_time_of_day().

# Number of steps. We advance once per minute for 2 hours (120 steps)
# Declare as float since lerp() needs weight represented as a float.
var duration:float = 120.0
# Sunset begins at 5 PM
var sunset_start = 17

func _process(delta):
    match(Time.get_time_of_day()):
        Time.SUNSET: # 17:00 - 19:00
            var time = OS.get_datetime()
            var hr = time['hour'] # Hour represented in 24h time
            var minute = time['minute']
            var step = (hr % sunset_start * 60) + minute
            color = lerp(sunset_color, night_color, step/duration)
        # ...and repeat for other time of day cases.

Clock UI

The in-game clock is implemented as a separate scene that only has one function: to continuously update and display the system time. Implementing the clock UI is trivial thanks to OS.get_datetime(). This method returns a Dictionary that represents the player’s system time and it contains all the time information we need to display in the UI.

The clock is continuously updated via the _process() function. Since this is a relatively simple clock, we only need the ‘hour’, ‘minute’, and ‘weekday’ key-value pairs from the Dictionary returned by OS.get_datetime(). The returned time is in 24 hour format, so the AM/PM string is determined by checking whether the hour is >= 12. If the time and weekday strings in the clock scene are represented by Label nodes, then put together this would look like:

# OS.get_datetime() returns a weekday value from 0 to 6
var weekdays = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]

func update_clock():
    var time =  OS.get_datetime()
    var hour = time['hour']
    var minute = "%02d" % time['minute'] # pad with zeroes for a minimum width of 2 digits
    var weekday = weekdays[time['weekdays']]
    var ampm = "PM" if time['hour'] >= 12 else "AM"
    $ClockLabel.text = str(hr, ":", minute, " ", ampm)
    $WeekdayLabel.text = weekday

func _process(delta):
    update_clock()

Lastly, the icon to the left of the clock – representing daytime or nighttime – is set according the time of day recorded in the Time singleton mentioned earlier. The texture is swapped whenever time transitions between day and night, and vice versa.

Lighting up Lanterns

Each lantern or element that emits light (such as windows) in the game is associated with a Light2D node that automatically adjusts its brightness level according to the time of day. Each Light2D shares the same script that, like our CanvasModulate node, uses lerp() to continuously brighten or dim the Light2D’s energy, i.e. its brightness.

To create the individual lights in each sprite, a separate layer representing all the lights in a sprite is drawn on top of the original sprite and then exported to be used as the Light2D’s texture. Since the lights are based on the same image but drawn on a separate layer, the lights match up with the sprite in-game. Finally, to make the lights actually glow, the blend mode of the Light2D texture is set to Add.

The shop sprite’s Light2D texture.

To gradually turn the lights on/off, the lights in the game are synchronized with the time phases defined in our Time singleton, and thus are also in-sync with the color modulation. Similar to the CanvasModulate script, the Light2D logic also continuously checks the current time phase of the day by querying the Time singleton and adjusts the energy attribute accordingly, utilizing the same method of lerp()‘ing using the minute divided by the duration in minutes (minute/duration) as the weight.

Let’s use sunset again as the example. As established earlier, sunset begins at 5 PM and the transition between sunset and nighttime has a duration of 2 hours. If we define our light energy to be 0.5 at sunset, and 1.0 at nighttime, we can lerp() towards our target value using minute/duration as the weight, just as we did in the CanvasModulate script.

var sunset_energy = 0.5
var night_energy = 1.0
var day_energy = 0.0 # Turned off

# Sunset begins at 5 PM
var sunset_start = 17
# Total number of lerp() steps
var duration:float = 120.0
	
func _process(delta):
    match(Time.get_time_of_day()):
        Time.SUNSET: # 17:00 - 19:00
            var time = OS.get_datetime()
            var hr = time['hour'] # Hour represented in 24h time
            var minute = time['minute']
            var step = (hr % sunset_start * 60) + minute
            energy = lerp(sunset_energy, night_energy, step/duration)
        # ...and repeat for other time of day cases.

With the lighting, color palette, and the clock working in conjunction to subtly change the game environment according to real time, my hope is that it’ll create a more visually engaging and vivid gameplay experience for the player ✨

Thanks for reading!