Time Precision

How WorldTime stores time internally and how to convert between the internal int64 representation and the float formats your game logic needs.

Why int64 Microseconds?

Godot's _Process(double delta) delivers float delta time — this is unavoidable. WorldTime converts that float to int64 microseconds once per tick, then performs all subsequent arithmetic as pure integer math.

Godot engine  →  float deltaTime  →  RoundToMicroseconds()  →  int64 microseconds
                 (per-tick loss)              ONE TIME loss             rest is exact

What this fixes vs pure float

Scenario Pure float int64 microseconds
AddSeconds(86400) (1 day) Date unchanged (carry bug) Date advances correctly ✓
Pause → resume after 1h ~0.5s game-time drift Exact ✓
100 consecutive day boundaries Drift compounds Each boundary exact ✓
Sub-minute time display Drift invisible at hour scale Exact within the truncation limit

The one-way door: Godot's float boundary

The conversion at the boundary (long)(deltaTime * 1_000_000f) truncates, not rounds. At 60 Hz (16.666ms/delta):

True:     16.666... ms  × 1_000_000 = 16,666.666... µs
Truncated:                → 16,666 µs  (off by 0.667 µs/tick)
Per real-day:  86,400 ticks × 0.667 µs  ≈  57,700 µs  ≈  0.058 seconds/day
Per game-year:  2920 real-days × 0.058s  ≈  169 seconds  ≈  2.8 minutes/year

This is deterministic (same hardware always gives same result) and small enough that no gameplay system cares. It is not zero — if you need zero drift at the Godot boundary, that requires engine-level changes.

The Internal Representation

HoursTime — int64 microseconds for time-of-day

HoursTime stores time-within-day as three fields:

// Internal storage in HoursTime
private int  _hours;        // 0-23
private int  _minutes;      // 0-59
private long _microseconds; // [0, 60_000_000)  — sub-minute only

Total microseconds = _hours × 3_600_000_000 + _minutes × 60_000_000 + _microseconds

AccumulatedRealMicroseconds — int64 for real elapsed time

WorldTimeStateComponent.AccumulatedRealMicroseconds tracks total real microseconds since game start. This is the authoritative source for:

  • Pause/resume (lossless save/restore)
  • Save/load
  • Any UI that shows total real time played
// Pause: save
long savedMicroseconds = state.AccumulatedRealMicroseconds;

// Resume: restore
state.AccumulatedRealMicroseconds = savedMicroseconds;

Converting to Float Formats

HoursTime → float game seconds

Use AsGameSeconds(TimeScale):

var time = new HoursTime(14, 30, 0f); // 2:30 PM
float seconds = time.AsGameSeconds(timeScale);
// seconds = 14×3600 + 30×60 + 0 = 52,200 game seconds

The Seconds property is a float for API compatibility — it recomputes from microseconds on every access:

float secs = time.Seconds;  // returns _microseconds / 1_000_000f as float

GameTimeDuration — structured duration with float seconds

For durations (time differences, cooldowns, etc.), use GameTimeDuration:

// Create from float seconds
var duration = GameTimeDuration.FromSeconds(3661.5f, timeScale);
// → 0 days, 1 hour, 1 minute, 1.5 seconds

// Convert back to float
float totalSeconds = duration.ToTotalSeconds(timeScale);  // 3661.5f

AccumulatedRealMicroseconds → float real seconds

float realSeconds = state.AccumulatedRealMicroseconds / 1_000_000f;

GameDateTime → total game seconds from calendar epoch

float totalGameSeconds = calendar.GetSecondsFromDateTime(dateTime);
// or for a time span between two DateTimes:
float span = calendar.GetSecondsBetween(startDateTime, endDateTime);

Converting from Float

float seconds → HoursTime

// Game seconds to HoursTime using a TimeScale
var time = HoursTime.FromGameSeconds(3661f, timeScale); // 1h 1m 1s

float seconds → GameTimeDuration

var duration = GameTimeDuration.FromSeconds(90.5f, timeScale);
// → 0 days, 0 hours, 1 minute, 30.5 seconds

float seconds → GameDateTime (add to existing datetime)

var tomorrow = currentDateTime.AddSeconds(86400f);  // advances by 1 day exactly
// delegates to AddMicroseconds internally — uses int64 path

Converting to TimeFormat Strings

TimeFormat handles display formatting independently of the int64 backend:

var fmt = TimeFormat.Default();
// → formats like "15 / 03 / 1 14 : 30 : 00"

var minimal = TimeFormat.Minimal();
// → formats like "3/15 14:30"

string display = fmt.GetFormattedDate(dateTime.Date, calendar);
string timeDisplay = fmt.GetFormattedHoursTime(dateTime.Time);

TimeFormat has no awareness of the int64 layer — it consumes the public HoursTime and GameDate APIs which already present reconstructed float/integer values.

Quick Reference

From To Method
float delta (Godot) int64 µs HoursTime.RoundToMicroseconds(delta)
HoursTime float game-sec time.AsGameSeconds(timeScale)
HoursTime int µs time.Microseconds (internal)
long µs (Accumulated) float real-sec µs / 1_000_000f
float game-sec HoursTime HoursTime.FromGameSeconds(sec, scale)
float real-sec GameTimeDuration GameTimeDuration.FromSeconds(sec, scale)
float game-sec GameDateTime dt.AddSeconds(sec) — int64 path
GameDateTime calendar game-sec calendar.GetSecondsFromDateTime(dt)

Common Mistakes

❌ Constructing HoursTime with float seconds then checking Seconds

// WRONG — Seconds is a computed property, not the input value
var t = new HoursTime(14, 30, 90.5f);  // 90.5 seconds normalizes to 1m 30.5s
float s = t.Seconds;  // returns 30.5f, not 90.5f

❌ Modifying Seconds property

// WRONG — Seconds is read-only (it's a computed property)
time.Seconds = 30f;  // compile error

❌ Comparing float times directly

// WRONG — float comparison is fragile
if (time.Seconds == 30.0f) { }

// CORRECT — use epsilon for float comparison
if (Math.Abs(time.Seconds - 30.0f) < 0.001f) { }

❌ Storing float for pause/resume

// WRONG — float drift accumulates
saveData["elapsedSeconds"] = state.ElapsedGameSeconds;

// CORRECT — int64 microseconds is lossless
saveData["elapsedMicroseconds"] = state.AccumulatedRealMicroseconds;