Skip to content
ct

Calendar Time v2.0

Getting Started with Calendar Time

A beginner-friendly walkthrough. If you've never used the plugin before, this is where to start. For why the time model is the way it is, see [time-model.md](./time-model.md). For exact API names, see

Status
Current
Version
v2.0
Updated
Current with plugin v2.0

A beginner-friendly walkthrough. If you've never used the plugin before, this is where to start. For why the time model is the way it is, see time-model.md. For exact API names, see the addon README.

The 30-second mental model

There are three things in the plugin, and each has a clear role:

  1. Engine time — Godot's own delta (real seconds). Drives rendering, physics, input, animation. The plugin never touches this.
  2. Game time — your game's calendar time: "Day 3 of Spring, year 2, 14:35". This is what crop timers, NPC schedules, cooldowns, and save files care about. The plugin manages this.
  3. The bridgeTimeScale.delta_multiplier. One number that controls how fast game time flows relative to engine time. Default is 1000.0 (1 real second ≈ 16 game minutes). You can change it for slow-mo, fast-forward, or pause.

The plugin gives you one or more shared runtime clocks — GameClocks — driven by a TimeHost. The host builds an internal ClockGroup from its clocks array. Each clock derives its date, time of day, and day-night phase from its own µs counter.

Engine delta (real seconds) ──► TimeHost ──► ClockGroup ──► GameClock.current_microseconds
  (Godot's _process)              (driver)    (N clocks)    (each clock, hermetic)

                                                                       ├─► DateTime
                                                                       ├─► TimeOfDay phase
                                                                       └─► Save / load (per clock)

That's the whole picture. The rest of this guide is "how to author each piece."

What the demo looks like

The 2.0 demo includes a live TimeHost, GameClock, calendar display, time dial, event log, speed controls, and save/load buttons wired through the same public APIs described below. Screenshots are available in the hosted web documentation.

Setup (10 minutes)

1. Install the addon

Copy addons/calendar_time/ from this plugin into your Godot project's addons/ folder, then enable it under Project Settings → Plugins → Calendar Time → Enable. No autoload is registered — everything is explicit.

2. Add the services scene

The plugin ships a ready-made scene called calendar_time_services.tscn (in the templates/calendar_time/ folder — copy it into your project). Instantiate it once somewhere in your main scene tree. It includes a TimeHost already wired to the bundled starter GameClock:

Node What it does
TimeHost The engine→game driver. Drives every clock in TimeHost.clocks each frame via delta × TimeScale.delta_multiplier.
GameClock Shared Resource assigned in TimeHost.clocks; owns the current time, calendar, signal bus, serializer, and optional age service.

AgeService is a Resource carried by the shared GameClock.tres (see step 4). It tracks the main AgeState in the clock's own per-clock registry (clock.age_registry). Every GameClock owns its own AgeStateRegistry — two clocks running side-by-side never see each other's age state. Each AgeService registers its AgeState under its configured service_id (default "age_service"). Multi-clock games with multiple age services set distinct service_id per .tres to keep their main AgeStates separate.

AgeComponent is a Godot scene component Node, not a data component stored on another system. If your game keeps age data in its own architecture, keep that data in your adapter layer and bind it to Calendar Time's AgeComponent / AgeState API at the boundary.

You can add these nodes individually instead (the "Add Child Node" menu has them), but the bundled scene is faster and the wiring is correct out of the box.

3. Configure the calendar

A GameCalendar is a .tres that defines the shape of your time: how many months, how many days, how many hours, event days (festivals, harvest days, etc.). Create one:

  1. In the FileSystem panel, right-click your project folder → New → Resource… → pick GameCalendar → save it as my_game_calendar.tres.
  2. Inside the calendar:
    • Add one or more GameYear sub-resources. Each year has months.
    • Add GameMonth sub-resources inside each year. Each month has a day count and optional EventDay resources (e.g., "Harvest Festival").
    • Optionally set a TimeScale to define calendar units such as seconds per minute, hour, and day. This is calendar structure, not the host's runtime speed.

Don't want to build one from scratch? The plugin ships a four_seasons starter calendar in templates/calendar_time/resources/four_seasons/ — copy and tweak it.

4. Author your shared GameClock

Every clock consumer binds the same GameClock .tres so they share one runtime timeline. Author it once:

  1. New → Resource… → GameClock → save as my_clock.tres.
  2. Set calendar to my_game_calendar.tres.
  3. Optionally set epoch to your calendar's epoch.
  4. Optionally assign an AgeService Resource if you use aging.

Bind this same my_clock.tres everywhere: TimeHost.clocks, UI displays, AgeComponents, lighting, and game code.

5. Wire TimeHost

Select the TimeHost node (inside the services scene you added). In the Inspector:

  • clocks → add your my_clock.tres
  • time_scale → your TimeScale bridge multiplier, or leave empty for the default 1000× bridge

Press Play. The host is now ticking every clock in clocks. You're done with setup.

6. (Optional) Add a UI

The templates/calendar_time/ui/ folder has ready-made UI scenes:

  • date_time_display.tscn — shows the current date/time
  • time_of_day_display.tscn — shows dawn/day/dusk/night
  • calendar_month_display.tscn — shows the full month grid
  • engine_time_scale_control.tscn — a slider for fast-forward/slow-mo

Add them under a CanvasLayer in your UI hierarchy, then assign the same shared my_clock.tres to each (the templates ship with a default four_seasons_clock.tres so they work standalone — override for your game's clock). UI displays bind a single GameClock (they display one clock's state); for multi-clock UIs, bind the ClockGroup and iterate.

How time flows (the short version)

Godot's _process(delta) runs on your TimeHost. The host converts the engine delta into host game time with TimeHost.time_scale.delta_multiplier, then routes that delta through its internal ClockGroup. Each GameClock applies its own speed_multiplier, advances its microsecond counter, and fires boundary events on its own bus. You subscribe to clock.signal_bus from your game code and react.

engine delta
→ TimeHost.time_scale.delta_multiplier
→ ClockGroup routes to each clock
→ GameClock.speed_multiplier
→ clock.signal_bus events

Use TimeHost.time_scale for host-wide calibration. Use GameClock.speed_multiplier when only one timeline should speed up, slow down, or pause.

Want the full picture? See time-model.md for the long version — boundary events, ownership of the signal bus, why the clock is a Resource, how TimeScale fits in, and the per-clock vs group-level save/load split.

When do I need multiple clocks?

You don't, for most games. A single clock covers the vast majority of cases — your world's calendar, ageing, and day-night cycle are all driven by the same µs counter. The default CalendarTimeServices scene ships with a one-element ClockGroup that satisfies this 95% case.

You do need multiple clocks if your game wants different timelines to advance independently. Common scenarios:

  • World clock + festival timeline. The world clock drives crop growth, NPC schedules, and day-night. A separate festival clock tracks when festivals are scheduled (decoupled from the world clock so a festival can be paused while the world keeps running, or vice versa).
  • Lockstep multiplayer. Each player has their own clock; the game syncs them at deterministic ticks.
  • Save-file-time vs game-time. A meta-game clock that runs only when the main menu is up, separate from the in-game clock.

For each additional clock, add a GameClock.tres to TimeHost.clocks. Subscribe to each clock's signal_bus for its own events; use host.get_group_serializer() for unified save/load. Multi-clock saves are reorder-safe by default: each clock has a stable clock_id (auto-derived from resource_path), so adding, removing, or reordering clocks between save and load does not cross-pollinate state. See Per-clock identity in the save/load section below for the resolution chain and the clock_id_override seam for runtime clocks.

Tip: Don't reach for multiple clocks unless you have a concrete reason. A single clock with time_scale tweaks covers most pacing needs. Multiple clocks add real complexity (event correlation, save ordering, multi-clock UI). Start with one.

What should use clock time vs engine delta?

The beginner rule is: Calendar Time owns world-calendar facts; Godot owns presentation feel; your game-owned fixed-step loop owns deterministic simulation order. See What Should Drive What? for the longer decision guide.

Route world-calendar simulation through GameClock:

  • calendar progression
  • day/night state and lighting
  • event schedules
  • weather changes tied to date/time
  • crop or object ageing
  • save/load world time
  • deterministic replay/offline ticks

Use raw Godot delta directly for presentation and immediate feel:

  • character input response
  • camera smoothing
  • UI tweens/transitions
  • presentation-only particles/VFX
  • sprite/AnimationPlayer playback unless it represents game-time simulation
  • physics unless your game intentionally couples physics to simulation time

Use your game-owned fixed-step simulation tick directly when simulation ordering is already owned by that loop. In that setup, set TimeHost.drive_mode to MANUAL and call time_host.drive_microseconds(...) from the same fixed step. Calendar Time records and publishes the calendar state for that tick; it does not schedule the rest of your game.

Avoid using Engine.time_scale as the normal way to accelerate calendar time. It changes the whole Godot runtime. Calendar Time demos and gameplay controls should prefer GameClock.speed_multiplier for timeline speed.

How to react to time

Subscribe on the clock's signal bus:

@export var clock :GameClock

func _ready()-> void:
    clock.signal_bus.date_changed.connect(_on_date_changed)
    clock.signal_bus.time_of_day_changed.connect(_on_time_of_day)
    clock.signal_bus.event_day_started.connect(_on_event_day)

func _on_date_changed(event:DateChangeEvent)-> void:
    print("New day:%s" % event.new_date)

func _on_time_of_day(tod:TimeOfDay, _last:TimeOfDay)-> void:
    print("It's now%s" % tod.resource_name)

func _on_event_day(event_day:EventDay)-> void:
    print("Festival!%s" % event_day.display_name)

How to save and load

Per-clock (the canonical surface)

Every GameClock owns its own TimeSnapshotSerializer and exposes to_dict() / from_dict():

# Save a single clock
var save_data:= clock.to_dict()

# Load it back
clock.from_dict(save_data)

Multi-clock facade (all clocks in one dict)

For games with a ClockGroup, use the host's get_group_serializer() to save time + age combined per clock, namespaced under the group's save_key:

# Save
var facade:= time_host.get_group_serializer()
var save_data:= facade.to_dict()
var file:= FileAccess.open("user://savegame.json",FileAccess.WRITE)
file.store_string(JSON.stringify(save_data))
file.close()

# Load
var file:= FileAccess.open("user://savegame.json",FileAccess.READ)
var save_data:= JSON.parse_string(file.get_as_text())
time_host.load_state(save_data)
file.close()

TimeHost.load_state() restores the group through the same facade and emits clock_state_loaded for each loaded clock, so bound UI, day/night, and other derived consumers recompute immediately after load.

The top-level shape is { group.save_key: { <clock_id>: <per-clock dict>, ... } }. Each per-clock dict carries:

  • clock_id — the stable identity the host uses to route the entry back to the right clock on load (see Per-clock identity below).
  • "time_state" — the legacy 1.x snapshot top key (preserved for save migration; contains serialization_id, game_microseconds, date_time).
  • "age_states" / "age_service_state" — present if the clock has an age_service configured.

Saves reload to the exact microsecond the player saved at. Current saves write the grouped shape above. Legacy loads are also accepted through the same time_host.load_state(save_data) call.

Legacy save migration

You do not need a separate migration script for existing pre-2.0 save files. Pass the parsed save dictionary to time_host.load_state(save_data). Calendar Time detects the old top-level shape and loads it into the first bound clock.

The true 1.x top-level save shape used these root keys:

{
    "time_state": {
        "id":"world_clock_legacy",
        "game_seconds":3600.5,
        "date_time": { ... },
    },
    "age_states": { ... },
    "world_age_system_state": { ... },
}

On load, Calendar Time adapts that shape to the current per-clock format:

  • "time_state" loads through TimeSnapshot and migrates legacy "id", float game_seconds, and legacy game_ticks to canonical integer game_microseconds.
  • "age_states" loads into the clock's AgeStateRegistry if the clock has an AgeService.
  • "world_age_system_state" maps to the current per-clock "age_service_state" payload.

Early grouped compatibility saves under "clocks" are still accepted too. New saves should always use the current grouped shape from time_host.get_group_serializer().to_dict().

The save dict key stays "time_state" for backward compatibility even though the DTO class is TimeSnapshot.

Per-clock identity (reorder-safe saves)

Each GameClock exposes a stable clock_id getter. The save format keys per-clock dicts by clock_id, so reordering the host's clocks between save and load does not cross-pollinate state — each clock's state lands in the clock matching the id, regardless of position.

Default resolution chain (no user setup required):

  1. clock_id_override (an @export var on the resource) — if set, used as-is. Empty by default.
  2. Hash of the clock's resource_path — for clocks loaded from a .tres file, two clones of the same file get the same id automatically.
  3. Monotonic runtime counter — for GameClock.new() calls with no resource path (e.g. a clock built in _ready()). Each call gets a distinct id; no coordination required.

For most games, the default resolution is enough — .tres-loaded clocks get stable ids automatically, and runtime-only clocks get distinct ids. The advanced override section below covers the rare cases where you need to set clock_id_override explicitly.

When to set clock_id_override explicitly (advanced):

  • Runtime clocks that need stable identity across runs. A clock built via GameClock.new() in _ready() gets a runtime id that resets on process start. If you need save data to round-trip across two separate game sessions (e.g. save in session A, load in session B), override with a stable string.

    func _ready()-> void:
        var clock:= GameClock.new(my_calendar)
        clock.clock_id_override= &"runtime_world_clock"
        $TimeHost.clocks= [clock]
  • Resource_path is volatile — e.g. a clock saved via ResourceSaver.save() to user:// and reloaded at a different path on the next run. The auto-derive keys off resource_path, so a different path gets a different id. Override with a stable string to keep identity stable across path changes.

  • Cross-deployment deduplication — same logical clock in two builds (e.g. the same .pck released twice) should map to the same id even if the .tres was touched.

@export var clock_id_override :StringName = &""

Setting clock_id_override to a non-empty StringName short-circuits the resolution chain. Two clocks with the same override value are treated as the same clock for save/load purposes (state is loaded into whichever one is in host.clocks at load time, the other is left untouched). Don't share overrides across two clocks that hold different state — the load will route the saved data to the wrong clock.

For the deeper contract (the positional fallback for very early unreleased saves, the trade-off with stable ids over a ClockGroup-shaped {0, 1} mapping, and the reordering gotchas that no longer apply), see Multi-clock Save Identity.

Common beginner questions

Q: My clock isn't ticking. What's wrong? A: Check that TimeHost.clocks has at least one GameClock assigned. The host refuses to advance without a clock.

Q: Can I have more than one clock? A: Yes — add more GameClock resources to TimeHost.clocks. Each clock is self-contained (its own calendar, signal bus, age registry); the host drives all of them automatically. See When do I need multiple clocks? above.

Q: How do I speed up or slow down time? A: For one timeline, change GameClock.speed_multiplier. For every clock on a host, change TimeHost.time_scale.delta_multiplier. Higher = faster game time. Use GameClockSpeedSlider-style controls for calendar speed; use the engine time-scale demo control only when you intentionally want to affect Godot itself.

Q: How do I pause game time without pausing the game? A: Set GameClock.speed_multiplier = 0.0 for one timeline, or TimeHost.time_scale.delta_multiplier = 0.0 for all clocks on that host. The clock stops, but rendering, physics, and input keep running — perfect for an inventory menu.

Q: How do I attach an AgeComponent to my game object? A: In the Inspector, set @export var clock : GameClock to your my_clock.tres. The component binds to clock.age_registry automatically. Multi-clock scenes bind the clock your age service is configured for. Don't leave clock unset — the component will push_error and refuse to register state.

Q: My save uses multiple clocks. Will the data cross-pollinate if I reorder them? A: No. Each clock is identified by a stable clock_id (auto-derived from resource_path for .tres-loaded clocks, or from a monotonic counter for runtime clocks). Saves key per-clock by clock_id, so adding, removing, or reordering clocks between save and load does not cross-pollinate state. If you need explicit stable identity for a runtime clock, set clock_id_override on it (see Per-clock identity in the save/load section above).

Q: How do I make sure two separate game runs identify the same runtime clock? A: Set clock_id_override to a stable string on the runtime clock:

func _ready()-> void:
    var clock:= GameClock.new(my_calendar)
    clock.clock_id_override= &"runtime_world_clock"  # any stable string
    $TimeHost.clocks= [clock]

Without the override, a GameClock.new()-built clock gets a runtime id from a process-wide counter that resets on each process start — so two separate runs of the same code produce different ids. The override pins the id. (See Per-clock identity in the save/load section above for the full resolution chain and the three override scenarios.)

Q: Where do I learn more?

  • time-model.md — the full time-model design doc
  • The addon README (addons/calendar_time/README.md) — full API reference