Skip to content

Add time of day to Fray's End#1991

Merged
manuq merged 13 commits intomainfrom
time-of-day
Feb 27, 2026
Merged

Add time of day to Fray's End#1991
manuq merged 13 commits intomainfrom
time-of-day

Conversation

@manuq
Copy link
Collaborator

@manuq manuq commented Feb 26, 2026

GameState: Add lights on/off state

A (non persisted) state used to communicate when lights
should be turned on or off between components.


Add TimeAndWeather node

A drop-in node that adds a day-night cycle animation. Using
post-processing and overlay visual effects.

It's possible to continue with the same time-of-day in the next scene.

Nothing is persisted. Instead, the in-game time is derived from the
system time.

If two scenes have TimeAndWeather nodes with "use system time" enabled
and same setting for time scale, they'll be in sync. See the weather
museum scene for an example.

Duck typing is used to call methods in the visual effects:
randomize, fade_in, fade_out.

For now every day is the same cloudy and every night is the same foggy.
Later more weather conditions and randomization can be added.

This is crying for sound effects too!


Add ArtificialLightBehavior

A behavior node that can manage lights and turn them
on/off according to the game state.


Player: Use artificial light

No delay because we want the area around the player to immediately
be illuminated.


Houses and loom: Add artificial light

For houses, add some random delay so there is variation in
stacks of houses.


Add ModulateAsSkyBehavior

To be used in CanvasItem nodes with reflective surfaces.


Add time and weather museum

Two scenes to show how the next scene continues at the same time.


Fray's End: Use TimeAndWeather

Add a TimeAndWeather node with the default settings. And
remove the clouds shadow overlay effect, because the new
node provides it now.

Add a ModulateAsSkyBehavior to the water tilemap layer.


Initial prototype: #1913

A (non persisted) state used to communicate when lights
should be turned on or off between components.
A drop-in node that adds a day-night cycle animation. Using
post-processing and overlay visual effects.

It's possible to continue with the same time-of-day in the next scene.

Nothing is persisted. Instead, the in-game time is derived from the
system time.

If two scenes have TimeAndWeather nodes with "use system time" enabled
and same setting for time scale, they'll be in sync. See the weather
museum scene for an example.

Duck typing is used to call methods in the visual effects:
randomize, fade_in, fade_out.

For now every day is the same cloudy and every night is the same foggy.
Later more weather conditions and randomization can be added.

This is crying for sound effects too!
A behavior node that can manage lights and turn them
on/off according to the game state.
No delay because we want the area around the player to immediately
be illuminated.
For houses, add some random delay so there is variation in
stacks of houses.
To be used in CanvasItem nodes with reflective surfaces.
Two scenes to show how the next scene continues at the same time.
Add a TimeAndWeather node with the default settings. And
remove the clouds shadow overlay effect, because the new
node provides it now.

Add a ModulateAsSkyBehavior to the water tilemap layer.
@manuq manuq requested a review from a team as a code owner February 26, 2026 13:40
@github-actions
Copy link

Play this branch at https://play.threadbare.game/branches/endlessm/time-of-day.

(This launches the game from the start, not directly at the change(s) in this pull request.)

@manuq
Copy link
Collaborator Author

manuq commented Feb 26, 2026

Things that were in the prototype but not in this pull request:

  • Random weather. for now every day is the same cloudy and every night is the same foggy. I'll open a follow-up task for this.
  • Rain effect. In separate PR Add rain overlay effect #1989

New or changed things since the prototype:

  • Calling all nodes in a group was replaced by signal emission/handling. Going through the game state. Using behavior nodes.
  • Added a museum scene as interactive documentation.

Other followups:

  • Add documentation to the wiki page! Besides the museum, we need to explain how to apply these effects individually in your own levels. And also the decision of how time passes in Threadbare (in main areas, locked in levels).
  • Add placeholder AudioStreamRandomizer for sound effects, so learners can add their own. As suggested by @wjt .

Copy link
Member

@wjt wjt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it.

Comment on lines +54 to +55
## Current state of artificial lights.
var lights_on: bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting that this (correctly) adds another non-persisted field to the state. intro_dialogue_shown is already not persisted.

To me this suggests that we should (later) split the state up into:

  • This singleton
  • A resource holding the persisted state
    • and some functions to load and save it
  • Another resource holding transient state

Don't change it in this branch!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a plan. It has become a bag of cats. The separation between persisted and transient is a great idea.

Comment on lines +165 to +185
[sub_resource type="GDScript" id="GDScript_oruto"]
script/source = "# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
extends Label

@onready var animation_player: AnimationPlayer = $\"../../AnimationPlayer\"


func _process(_delta: float) -> void:
var time := animation_player.current_animation_position
var period: String
if time > 5 and time <= 12:
period = \"morning\"
elif time > 12 and time <= 18:
period = \"afternoon\"
elif time > 18 and time <= 22:
period = \"evening\"
else:
period = \"night\"
text = \"%.1f %s\" % [time, period]
"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to leave this here? It's just a debug node so probably OK to have it be embedded in the scene.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm maybe I move this script to components/.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +42 to +44
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I learnt recently is that it is better to use set_process(false) in _ready rather than have this guard here, because calling a method 60 times per second in the editor has a cost even if it just returns immediately.

(This could be fixed later.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'll fix it before merging!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I remember what happened here. Yesterday I had:

func _ready() -> void:
	if Engine.is_editor_hint():
		process_mode = Node.PROCESS_MODE_DISABLED
		return

But this effectively disabled the node in the editor, and the scene was saved with the node disabled. So I removed it. Using set_process(false) is the correct thing to do.

Comment on lines +36 to +42
## How fast should time pass in the game.[br]
## - 1: One day in game matches one day in reality. Don't do this![br]
## - 24: One hour in game matches one day in reality.[br]
## - 144: 10 minutes in game matches one day in reality (default).[br]
## - 1440: One minute in game matches one day in reality.[br]
## - 3600: 24 seconds in game matches one day in reality.[br]
@export_range(1.0, 3600.0, 1.0) var time_scale: float = 144.0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find these numbers a bit hard to think about.

One option is to describe this number in a different way: this number is the number of game-days in a real-world day! I only realised this while writing this comment.

The alternative would be to store the reciprocal: the length of an in-game day, in seconds. So the new default would become 600.0 seconds.

Or store the fractional number of real-world minutes in an in-game day, so the new default would be 10.0 minutes.

We don't have to change this - I'm mainly thinking aloud.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the feedback I most welcome, because it's the part I had more doubts. Let me think about it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can have both... Have one real property that is adjustable and stored; and another which is not stored, readonly, shown in the editor, that converts to the other number. So you adjust e.g. this time_scale factor, and below it you would see "In-Game Day Length: 10.0 minutes".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's almost certainly overengineering though!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option is to describe this number in a different way: this number is the number of game-days in a real-world day! I only realised this while writing this comment.

I've been scratching my head and haven't thought about this. Well wording!

@wjt
Copy link
Member

wjt commented Feb 26, 2026

I pushed a fixup to make it possible to toggle the current-time debug label while the game is running.

@manuq
Copy link
Collaborator Author

manuq commented Feb 26, 2026

@wjt thanks for the detailed review!

@manuq manuq merged commit e67b1fe into main Feb 27, 2026
6 checks passed
@manuq manuq deleted the time-of-day branch February 27, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants