Skip to content
ct

Calendar Time v2.0

Guide: Time, precision & multiplayer

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

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.

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 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.

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.