Calendar Time should answer when the game is. Your game should decide what that time means for shops, NPC routines, crops, quests, weather, and other world rules.
TimeHost
↓ drives
GameClock ← one calendar-time authority
↓ exposes date/time events
WorldScheduleService ← game-owned policy
↓ updates
Shop, NPC, crop, quest, and presentation systemsThis guide is intentionally advanced. Start with Getting Started if you only need a calendar and clock.
Ownership boundary
| Concern | Owner |
|---|---|
| Calendar structure, dates, time-of-day phases | GameCalendar |
| Current calendar instant and clock events | GameClock |
| Engine-to-calendar driving and pause/rate bridge | TimeHost |
| Shop hours, NPC routines, crop rules, quests, weather policy | Your game |
| UI and presentation choices | Your game |
| Simulation tick order and fixed-step loop | Your game |
A schedule service may store domain state such as shop_open, opening hours, or
the last evaluated date. It must not store a second elapsed-time counter or
advance a second clock.
Minimal shop-hours example
Copy this small game-owned service into your own project and rename it for your domain. The example uses only the public Calendar Time API:
class_name WorldScheduleService
extends RefCounted
var clock :GameClock
var opening_hour :int = 9
var closing_hour :int = 17
var shop_open :bool = false:
set(value):
if shop_open== value:
return
shop_open= value
shop_open_changed.emit(shop_open)
signal shop_open_changed(is_open :bool)
func _init(p_clock :GameClock)-> void:
clock= p_clock
clock.signal_bus.date_time_changed.connect(_on_date_time_changed)
clock.signal_bus.clock_state_loaded.connect(_on_clock_state_loaded)
refresh()
func _on_date_time_changed(p_new :DateTime, _p_old :DateTime)-> void:
_refresh(p_new)
func _on_clock_state_loaded()-> void:
refresh()
func refresh()-> void:
_refresh(clock.date_time()if clock!= null else null)
func _refresh(p_date_time :DateTime)-> void:
if p_date_time== null or p_date_time.time== null:
shop_open= false
return
shop_open= p_date_time.time.hours>= opening_hour \\
and p_date_time.time.hours< closing_hourThe service reacts to the clock's authoritative date_time_changed event. It
owns the shop_open decision and emits a game-local signal; Calendar Time does
not know that a shop exists.
The repository demo contains the same runnable example at
demo/scripts/calendar_time_examples/world_schedule_service.gd, with focused
coverage in test/demo/simulation/test_world_schedule_service.gd.
Pause and speed behavior
Do not use Engine.time_scale as the shop's gameplay-time authority. Choose the
clock or host API according to the intended scope:
# Pause this clock while menus remain responsive.
clock.speed_multiplier= 0.0
# Resume the same clock.
clock.speed_multiplier= 1.0
# Change the rate for every clock driven by this host.
time_host.time_scale.delta_multiplier= 2.0When a clock is paused, its calendar does not advance and the schedule does not
change. When it resumes, the next canonical clock event reevaluates the rule.
For a fixed-step game, set time_host.drive_mode to MANUAL and call
time_host.drive_microseconds(step_us) from the game-owned simulation loop.
Use Engine.time_scale only when the whole engine session should change pace,
including physics and presentation. It is not a substitute for calendar
progression and should not be a second clock.
Save and load
Save Calendar Time through its existing authority:
var save_data :Dictionary = time_host.get_group_serializer().to_dict()
time_host.load_state(save_data)Keep game-owned schedule state in the game save envelope only when it is truly
domain state. Do not save a duplicate current time, elapsed counter, or derived
shop_open flag. Restore in this order:
- Restore the Calendar Time clock through
TimeHost.load_state(). - Restore any independent game-domain state.
- Re-evaluate schedules from
clock.date_time()or the nextclock_state_loadedcallback.
A schedule that subscribes to clock_state_loaded handles a restored instant
immediately, without waiting for another tick. Calendar Time's clock snapshot
and group serializer remain responsible for clock/calendar state; the game
remains responsible for its own save envelope and world policy.
Common mistakes
- Creating a second
WorldClockor incrementingelapsed_secondsin the game. - Calling
Engine.time_scaleto implement a shop's opening hours. - Putting shop/NPC/crop concepts into the Calendar Time addon runtime.
- Persisting
shop_openas if it were authoritative time. It is derived state. - Running both an automatic
TimeHostand a game-owned manual clock driver. - Re-deriving day/night from wall-clock hours when the calendar has authored time-of-day phases. Use the clock/calendar contract instead.
For the distinction between Calendar Time, Godot delta, and a game-owned fixed step, see What Should Drive What?. For exact integer driving and pause/rate ownership, see TimeScale & DriveMode.