Status: describes the Calendar Time 2.0 time model. The deterministic integer-microsecond core is implemented —
game_microsecondsis canonical;game_seconds/DateTimeare derived;clock.advance_microseconds()andhost.drive_microseconds()are the deterministic paths. The internal design contract isdocs/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.
- Put a
GameClockresource inTimeHost.clocks. - Assign that same
GameClockto UI, lighting, aging components, and gameplay scripts that need the current calendar time. - Subscribe to
clock.signal_buswhen 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