Bevy–chrono timestamp strategy: UTC persistence vs zone-aware for multiplayer determinism
0 reputation · 21 Aug 2023, 00:35 UTC
0 reputation · 21 Aug 2023, 00:35 UTC
Bevy's time resources—Time<Real>, Timer, and Stopwatch—provide delta-oriented elapsed time but do not model wall-clock calendar dates or time zones. The documented integration path requires adding chrono (or the time crate) as a Cargo dependency and wrapping DateTime<Utc> or zone-aware DateTime in a Bevy Resource, with timezone conversion handled at the application boundary via chrono-tz. This design choice creates an unresolved interoperability question: should all timestamps be stored in UTC and localized only at display, or should zone-aware timestamps be persisted directly, knowing that Bevy imposes no convention and the decision impacts save-file compatibility and multiplayer determinism? In deterministic fixed-timestep gameplay, depending on system wall-clock calls can produce non-deterministic results across machines, further complicating the choice.
Should timestamps be stored in UTC for interoperability, or zone-aware for user accuracy? How does the choice affect deterministic replay across clients? What is the recommended pattern for preserving zone preferences within Bevy Resources without breaking fixed-step determinism?
31775 reputation · 21 Aug 2023, 08:38 UTC
Persist every timestamp as chrono::DateTime (or an epoch integer) and store the user’s time‑zone only for UI. This guarantees that the same instant is written identically on every machine, so save files are portable and replays match across clients.
TZ env or OS settings.Utc::now(), so gameplay state is identical.chrono-tz database versions can change offsets, breaking hash‑based replay checks.Deterministic replay relies on identical serialized state across all participants. If the saved timestamp includes a zone offset, two clients will have different binary blobs, causing diverging hash values even though the underlying event occurred at the same instant. Using UTC (or epoch) removes that source of variance.
use bevy::prelude::*;
use chrono::{DateTime, Utc};
use chrono_tz::Tz;
/// Holds the canonical time used for all persistent events.
#[derive(Resource, Serialize, Deserialize)]
struct UtcClock {
epoch: DateTime,
}
/// User‑specific time‑zone preference used only for UI.
#[derive(Resource)]
struct UserTz {
tz: Tz,
}
fn setup(mut commands: Commands) {
// Capture the session start time once.
commands.insert_resource(UtcClock { epoch: Utc::now() });
// Default to the system zone; let UI change it.
commands.insert_resource(UserTz { tz: chrono_tz::UTC });
}
// Example of converting for display
fn format_time(time: DateTime, tz: &Tz) -> String {
time.with_timezone(tz).format("%Y-%m-%d %H:%M:%S %Z").to_string()
}
i64 (seconds) or i128 (nanoseconds) over the network. Integers compare, sort, and hash identically on every client.2024-10-13T05:46:42Z), but keep the underlying value in UTC.TZ settings.format_time with each machine’s chosen Tz; the displayed string should differ, but the underlying data remains the same.Time::Virtual only, compare state hashes, then repeat with Utc::now() injected; the first pair should match, the second should diverge.If your application must keep the original local time for audit or compliance reasons (e.g., legal logs), you can store an additional field containing the local representation. Keep the canonical UTC field for all game logic and replay. In that case, the extra field is never used for deterministic calculations.
Does your game need to record the exact local time of past events for audit or compliance purposes? If not, stick to UTC storage only.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.