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.
If your game owns a fixed-step loop, lockstep netcode step, or deterministic
replay, keep that loop in charge of system order. Set the host to MANUAL and
drive Calendar Time from the same step. See What Should Drive What? for the
beginner decision table.
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 stepMultiplayer & 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.
One time authority per game
Calendar Time for Godot and the internal simulation implementation are two independent time authorities. A single game must select exactly one authority.
Do not run both as mutating clocks in the same runtime. If you consume the
internal implementation, do so through a game-owned private adapter over its
immutable snapshots — do not also advance a GDScript GameClock for the same
gameplay state. Running two authorities that both mutate the same timeline
produces conflicting, non-reconcilable time.
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.