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;
Related
- Time Fundamentals — type overview
- Custom Time Scale — TimeScale configuration
- Saving & Loading — serialization patterns
- API: HoursTime
- API: GameDateTime
- API: TimeFormat