Chris' Tutorials
Docs/Calendar Time

Calendar Time v2.0

Time Model

Understand engine time, game time, TimeHost, ClockGroup, GameClock ownership, and deterministic driving.

StatusCurrent
Versionv2.0
UpdatedFrom plugin repo source
Source note:This page rendered plugin-owned docs or generated metadata inside unified Astro docs shell.

Status: describes the Calendar Time 2.0 time model. The deterministic integer-microsecond core is implementedgame_microseconds is canonical; game_seconds/DateTime are derived; clock.advance_microseconds() and host.drive_microseconds() are the deterministic paths. The internal design contract is docs/internal/time-model.md; this guide is the consumer-facing view.

Beginner version

For normal games, you do not need to think about ticks, microseconds, or network determinism.

  1. Put a GameClock resource in TimeHost.clocks.
  2. Assign that same GameClock to UI, lighting, aging components, and gameplay scripts that need the current calendar time.
  3. Subscribe to clock.signal_bus when your game needs to react to a new day, time-of-day change, or event day.
@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_tod_changed)

TimeHost drives time forward. GameClock is the timeline your game reads and listens to.

The current API shape

Use explicit bindings instead of host shortcuts:

@export var time_host : TimeHost
@export var clock : GameClock

func advance_two_hours_for_a_test_or_tool() -> void:
    # Exact integer path: 2 in-game hours.
    time_host.drive_microseconds(2 * 3_600_000_000)

func advance_two_hours_from_friendly_seconds() -> void:
    # Friendly float path. Rounds once at the public seam.
    time_host.drive_seconds(2.0 * 3600.0)

There is no host-level get_clock() / get_signal_bus() shortcut. Consumers bind the GameClock they care about, which keeps single-clock scenes simple and multi-clock scenes explicit.

The GameClock is a self-contained Resource: it holds the canonical µs, the calendar that gives them meaning, its TimeSignalBus, per-clock save/load, and optional AgeService. Every mutation through clock.advance_microseconds() or clock.progress_time() fires boundary events on the clock's own bus.

TimeHost is the engine→game driver. In automatic modes it pumps delta × TimeScale.delta_multiplier into every clock in TimeHost.clocks and carries fractional microseconds between frames. In manual mode it does not tick from _process / _physics_process; your code calls drive_microseconds() or drive_seconds() directly.

What "precision" means here

Under the hood, time is tracked as an exact integer count of game-microseconds, so:

  • Drift-free — long sessions don't accumulate floating-point error.
  • Save-exact — a saved clock reloads to the exact microsecond.
  • Uniform resolution — the same precision at hour 1 and year 1000.

You still pass and read seconds/hours/days; the precision is automatic.

The determinism boundary

There are two ways to advance time, with different guarantees:

You call Guarantee
clock.progress_time(seconds) or time_host.drive_seconds(seconds) Drift-free over time and easy to author. Not guaranteed bit-identical across machines because a float entered the path.
clock.advance_microseconds(n) or time_host.drive_microseconds(n) Bit-reproducible for the plugin clock: the same integer inputs produce the same clock timeline.

The rule: for deterministic replay or lockstep, advance by integer microseconds — never from a per-frame delta. A float delta varies by machine, frame rate, and hitch pattern; rounding it to microseconds stores it exactly but does not make two machines agree.

The host's automatic PROCESS / PHYSICS_PROCESS modes are fine for everyday single-player gameplay. For deterministic sims, switch to manual mode and drive time from your own fixed-step loop:

time_host.drive_mode = TimeHost.DriveMode.MANUAL
time_host.drive_microseconds(16_667) # one 60 Hz fixed step

Multiplayer & netcode

The time model is a clean integration seam for netcode — but how much it does depends on your netcode model:

  • Lockstep — clients exchange inputs and each simulates identically. Advance the clock by a constant integer µs per fixed simulation step, such as time_host.drive_microseconds(16_667) at 60 Hz. Send step numbers and inputs, not float deltas.
  • Server-authoritative / rollback — exact clock determinism is usually not load-bearing, but the integer clock still gives you exact snapshots and no accumulated drift.

What it does and doesn't do:

  • ✅ Gives you an exact, integer, serializable time authority.
  • ✅ Makes time easy to include in replay, rollback, and save/load systems.
  • ⚠️ Does not make your whole simulation deterministic. True lockstep also requires deterministic physics, RNG, animation, and gameplay logic. The clock is one clean piece, not the entire solution.

Practical guidance: drive the clock from your fixed-step simulation loop with a constant integer advance per step. Use delta only to decide when to step — never to decide how much game time a step represents.

Mirrors Godot's own clock

This isn't fighting the engine. Godot's authoritative clock is already integer microseconds (Time.get_ticks_usec()); float delta is a convenient frame boundary value. Calendar Time keeps game time the same way: integer canonical, float at the boundary.

Source

docs/guides/time-model.md

Plugin docs root:gdscript/plugins/calendar_time_dev/docs