Calendar Time 2.0 is a major clean-break release. It replaces the older World Time / 1.x runtime shape with a final 2.0 architecture centered on:
GameClock: the runtime Resource that owns canonical integer microseconds, calendar state, the per-clockTimeSignalBus, per-clock save/load, and optional ageing.TimeHost: the scene Node that advances one or more clocks from Godot process, physics process, or manual drive mode.ClockGroupSerializer: the multi-clock save/load facade returned bytime_host.get_group_serializer()and used bytime_host.load_state(save_data).
Code and scenes need a one-time rename/wiring pass. Save files are designed to auto-migrate on load through the 2.0 save DTO path.
Quick Start
- Back up your project and any
user://save files. - Replace the old plugin folder with the 2.0
addons/calendar_time/folder. - Open and re-save scenes/resources that used World Time / Calendar Time nodes or resources so Godot refreshes Inspector data.
- Update scripts using the rename tables below.
- Add or update a
TimeHostand assign one or moreGameClockresources toTimeHost.clocks. - Route app-level group loads through
time_host.load_state(save_data)so each clock publishesclock_state_loadedafter state restore. - Run a save/load smoke test and confirm clocks reload to the exact saved time.
Core Rename Table
| Old name (World Time / 1.x) | New name (2.0) | Notes |
|---|---|---|
| World Time | Calendar Time | Product/plugin branding. |
addons/world_time/ |
addons/calendar_time/ |
Plugin folder. |
WorldTimePlugin |
CalendarTimePlugin |
Editor plugin class. |
GameTimeSystem |
TimeHost |
Scene Node that drives assigned GameClock resources. |
TimeState |
GameClock / TimeSnapshot |
Runtime state lives on GameClock; save DTO data uses TimeSnapshot. |
WorldTimeSerializer |
ClockGroupSerializer |
Multi-clock facade via time_host.get_group_serializer(). |
WorldAgeSystem |
AgeService |
Optional per-clock ageing service, usually assigned on GameClock. |
AgeingComponent |
AgeComponent |
Node attached to ageable game objects. |
AgeingSettings |
AgeSettings |
Ageing configuration Resource. |
AgeingSceneReplacement |
AgeSceneReplacement |
Age-threshold scene replacement node. |
AddWhenAgeing |
AddWhenAged |
Age-threshold resource addition node. |
WorldTimeClock |
removed | Bind consumers to GameClock and clock.signal_bus directly. |
Common Property Renames
| Old property | New property | Notes |
|---|---|---|
time_state |
clock / clocks |
UI/components bind one GameClock; TimeHost drives clocks[]. |
calendar on old system nodes |
calendar on GameClock |
Calendar schema belongs to the clock Resource. |
ageing_component |
age_component |
Node-path exports. |
ageing_settings |
age_settings |
Ageing resources and components. |
world_age_system |
age_service |
Save/load and ageing setup. |
DateChangeEvent.new |
DateChangeEvent.new_date |
Avoids collision with GDScript constructor naming. |
DateChangeEvent.old |
DateChangeEvent.old_date |
Paired with new_date. |
TimeOfDay.color |
TimeOfDay.tint_color |
Time-of-day resources. |
TimeOfDayLightSettings.color |
TimeOfDayLightSettings.light_color |
Lighting resources. |
EventDay.color |
EventDay.accent_color |
Event day resources. |
TimeOfDayToggle.target |
TimeOfDayToggle.target_node |
Demo example script, no longer core plugin API. |
TimeOfDayToggle.property |
TimeOfDayToggle.target_property |
Demo example script, no longer core plugin API. |
CloseButton.target |
CloseButton.target_node |
Demo example script, no longer core plugin API. |
AddAfterGameSeconds.add_target |
AddAfterGameSeconds.target_node |
Age/timer helper nodes. |
AddAfterGameSeconds.amount |
AddAfterGameSeconds.seconds_amount |
Age/timer helper nodes. |
AddWhenAged.target_resource |
AddWhenAged.target_age_state_resource |
Age trigger resources. |
AddWhenAged.amount |
AddWhenAged.age_amount |
Age trigger resources. |
Scene Wiring Changes
1.x style
Older projects often had a time system node that owned time state directly. Consumers reached into that node or a state object for current time and signals.
2.0 style
Create or duplicate a GameClock Resource, assign its calendar, then wire it explicitly:
@export var clock: GameClock
@export var time_host: TimeHost
func _ready() -> void:
time_host.clocks = [clock]
clock.signal_bus.game_seconds_advanced.connect(_on_game_seconds_advanced)
clock.signal_bus.clock_state_loaded.connect(_on_clock_state_loaded)
For designer-authored scenes, assign TimeHost.clocks in the Inspector. auto_attach_unbound_clocks defaults to false; set it to true only for legacy scenes that intentionally rely on auto-register behavior.
Signals
Signals are consumed through clock.signal_bus.
| Need | 2.0 signal |
|---|---|
| Time advanced by seconds | clock.signal_bus.game_seconds_advanced(amount, total) |
| Date/time changed | clock.signal_bus.date_time_changed(new, old) |
| Date changed | clock.signal_bus.date_changed(event) |
| Day finished | clock.signal_bus.day_finished(finished_date) |
| Time of day changed | clock.signal_bus.time_of_day_changed(current, previous) |
| Day/night transition progress | clock.signal_bus.day_night_transition_progress_changed(progress) |
| Event day started | clock.signal_bus.event_day_started(event_day) |
| Clock speed changed | clock.signal_bus.clock_speed_changed(speed_multiplier) |
| State loaded | clock.signal_bus.clock_state_loaded() |
Example:
func _ready() -> void:
clock.signal_bus.game_seconds_advanced.connect(_on_game_seconds_advanced)
clock.signal_bus.day_night_transition_progress_changed.connect(_on_day_night_progress)
clock.signal_bus.clock_state_loaded.connect(_refresh_from_loaded_clock)
clock_state_loaded is emitted by time_host.load_state(save_data) after the group serializer restores state. Use it to refresh UI, day/night visuals, and any derived state that should update immediately after load.
Saving And Loading
Per-clock save
Use this when you only own one clock and do not need group composition:
var save_data: Dictionary = clock.to_dict()
clock.from_dict(save_data)
clock.from_dict(save_data) publishes clock_state_loaded after restoring the clock.
Multi-clock save
Use the host facade for normal app-level saves:
var save_data: Dictionary = time_host.get_group_serializer().to_dict()
time_host.load_state(save_data)
The current group shape is:
{
"clocks": {
"<clock_id>": {
"clock_id": "<clock_id>",
"time_state": { ... },
"age_states": { ... },
"age_service_state": { ... }
}
}
}
age_states and age_service_state are present only when the clock has an age_service.
Each GameClock has a stable clock_id. Resource-backed clocks derive it from resource_path. Runtime-only clocks get a session-unique runtime id, which is useful inside the current process but not stable across separate game launches. Set clock_id_override for runtime clocks that must load across separate game sessions.
Pre-2.0 save data that used the top-level 1.x WorldTimeSerializer shape is migrated when loaded through time_host.load_state(save_data). That true legacy shape used time_state, age_states, and world_age_system_state at the save root; Calendar Time loads it into the first bound clock positionally. Early grouped pre-release compatibility saves under clocks are still tolerated: entries with clock_id load by id, entries without clock_id fall back to positional matching, and entries with unknown ids are skipped instead of being loaded into an unrelated clock.
Float game_seconds and legacy game_ticks are migrated by the TimeSnapshot DTO path. The save key time_state remains for compatibility even though the current runtime object is GameClock and the DTO is TimeSnapshot.
Time Driving
TimeHost.drive_mode controls how clocks advance:
| Mode | Use |
|---|---|
PROCESS |
Normal frame-driven game time. |
PHYSICS_PROCESS |
Physics-step-driven game time. |
MANUAL |
Tests, replay, fixed-step, ECS, or deterministic-friendly integrations. |
For deterministic-friendly simulation, use integer microseconds:
time_host.drive_mode = TimeHost.DriveMode.MANUAL
time_host.drive_microseconds(16_666)
auto_increment_time is preserved as a backward-compatible alias for drive_mode:
auto_increment_time = trueis equivalent todrive_mode = PROCESS.auto_increment_time = falseis equivalent todrive_mode = MANUAL.drive_mode = PHYSICS_PROCESSreads back asauto_increment_time == truebecause it is still an auto-driven mode.
GameClock.current_microseconds is the canonical value. game_seconds() and date_time() are derived views.
Ageing Migration
Ageing is optional in 2.0 and is per-clock.
- Replace
WorldAgeSystemwithAgeService. - Replace
AgeingComponentwithAgeComponent. - Assign the same
GameClockused by the relevant gameplay system. - Bind each
AgeComponent.clockto thatGameClock; components resolve their registry fromclock.age_registry. - Use
clock.age_registryinstead of the old process-wide singleton registry.
Most projects can migrate object ageing by assigning age_service on the relevant GameClock, updating renamed node/resource classes, binding each AgeComponent.clock, and re-saving affected scenes. Template clock resources show the supported .tres shape: a GameClock can include both calendar = ExtResource(...) and age_service = ExtResource(...).
File And Template Paths
| Older path/name | 2.0 path/name |
|---|---|
addons/world_time/ |
addons/calendar_time/ |
template world_time_systems.tscn |
templates/calendar_time/calendar_time_services.tscn |
ageing/ageing_component.gd |
ageing/age_component.gd |
ageing/ageing_settings.gd |
ageing/age_settings.gd |
ageing/add_when_ageing.gd |
ageing/add_when_aged.gd |
ageing/ageing_scene_replacement.gd |
ageing/age_scene_replacement.gd |
If you use uid:// references dragged through the Inspector, Godot may keep many links intact. If you use load() / preload() paths, update them manually.
Minimal Migration Checklist
- Back up your project and saves.
- Replace the addon folder with
addons/calendar_time/. - Replace old time system nodes with
TimeHost. - Create/assign
GameClockresources and bind them toTimeHost.clocks. - Move calendar assignment onto each
GameClock. - Move signal connections to
clock.signal_bus. - Update app-level load paths to
time_host.load_state(save_data). - Update ageing class/property names if your project uses ageing.
- Bind each
AgeComponent.clockto the sameGameClockwhose age registry it should use. - Remove any old
AgeStateRegistry.get_singleton()usage and replace it withclock.age_registry. - Re-save scenes and resources in the Godot editor.
- Run your save/load smoke test and confirm clocks reload to the exact saved time.
Demo Example Scripts (No Migration Required Unless You Used Them Directly)
Three small helper scripts used to live in the addon. They are now example scripts in the demo because they are conveniences, not core runtime. Consumers who need the behavior can copy the script from the demo into their own project.
| Demo script | Purpose | Replace with |
|---|---|---|
res://demo/scripts/calendar_time_examples/close_button.gd |
Button that hides a target Control on press. |
Inline pressed signal handler: button.pressed.connect(func(): target.hide()). |
res://demo/scripts/calendar_time_examples/time_of_day_animated_sprite_2d.gd |
AnimatedSprite2D that switches its animation on time_of_day_changed. |
Plain AnimatedSprite2D + subscribe to clock.signal_bus.time_of_day_changed and call play("..."). |
res://demo/scripts/calendar_time_examples/time_of_day_toggle.gd |
Node that toggles a target property on time_of_day_changed. |
Plain Node + subscribe to clock.signal_bus.time_of_day_changed and set(...) the target property yourself. |
If you were using these classes in your own scenes, the migration is copy-paste: drag the example script into your project's scripts folder and update the script reference in the scene. The class identifiers (class_name) were removed so two projects can include the example scripts without colliding on global class names.
These example scripts are part of the Calendar Time demo (demo.tar.gz) and the test scene (test/calendar_time/scenes/test_calendar_display.tscn) references them through the demo path. The plugin payload (plugin.tar.gz) no longer contains them.
Troubleshooting
Time does not advance
Check that TimeHost.clocks contains at least one GameClock, drive_mode is not MANUAL unless you call drive_microseconds() / drive_seconds(), and time_scale.delta_multiplier is not zero.
UI does not refresh after load
Use time_host.load_state(save_data) instead of calling the group serializer's from_dict() directly. The host method publishes clock_state_loaded after restore.
A runtime clock loads as a different clock next session
Set clock.clock_id_override to a stable StringName before saving.
Old Inspector fields show missing property warnings
Open the scene/resource, reassign renamed exports if needed, and save. This is expected for clean-break renamed fields.
Source
MIGRATION.md
Plugin docs root:gdscript/plugins/calendar_time_dev/docs