Deterministic Identity
DeterministicIdentity<STATE> is a variant designed for strict, bit‑stable simulation. It mirrors the lifecycle of PredictedIdentity<STATE> but uses deterministic math (sfloat) and can validate state equality across client/server when enabled.
When to Use
- Systems that can produce identical results on all machines from the same inputs (e.g., strategy sim, deterministic AI, fixed math gameplay).
If you don’t require strict bit‑determinism, prefer PredictedIdentity<STATE> for simpler float‑based logic.
Key Properties
- Uses
sfloatdelta inSimulate/LateSimulatefor deterministic time steps. - History, rollback, and interpolation are the same pattern as stateful identities.
- Networking: it doesn’t send simulation state each tick; ownership metadata syncs when it changes, and the optional desync policy detects divergence from compact state hashes.
Prediction Manager settings (Determinism section):
- Desync Policy: how the server responds when a client’s deterministic state diverges from its own.
Ignore(default) does no hashing and has zero overhead.ReportraisespredictionManager.onDesyncDetectedon the server andonLocalDesyncon the diverged client, with no automatic recovery.Resyncre‑baselines the diverged client’s entire timeline with a full frame.Correctheals just the diverged identity by resending its authoritative state. - Desync Check Interval: how often clients report tick‑salted hashes of settled deterministic state to the server, in seconds (default 0.25). Hashing only runs when at least one identity’s resolved policy is not
Ignore.
Each DeterministicIdentity also has its own Desync Policy field (Inherit by default) that overrides the global setting; it is resolved once during setup. Hashes are compared at the same settled tick on both peers, so latency, jitter, and frame hitches never produce false positives.
Prediction policies:
FullPrediction,ServerRelay, andPredictedIfOwnedare supported without turning deterministic identities into ordinary per-tick state replication.SoftCorrectionis not supported because deterministic identities do not receive the authoritative state deltas required to build a correction target. Selecting it keepsSoftCorrectionas the configured policy, but the identity behaves asServerRelayon clients. The non‑owner branch ofPredictedIfOwnedWithSoftFallbackfalls back toServerRelaythe same way.
See Prediction Policies for the client timeline behavior.
Overrides
protected virtual void GetUnityState(ref STATE state)/protected virtual void SetUnityState(STATE state)protected virtual void SimulationStart()protected virtual void Simulate(ref STATE state, sfloat delta)protected virtual void LateSimulate(ref STATE state, sfloat delta)protected virtual void UpdateView(STATE viewState, STATE? verified)protected virtual STATE Interpolate(STATE from, STATE to, float t)
Note the sfloat delta: use deterministic math throughout your simulation. You can also mix in FP for fixed point math.
STATE Requirements
STATE : struct, IPredictedData<STATE>, same as other identities.- Provide deterministic operations via
IMath<STATE>(used by default interpolation):Add,Scale,Negate. - Avoid non‑deterministic data (raw
floatcomputation); prefersfloator integer/fixed‑point math inside state operations.
Example
using PurrNet.Prediction;
public struct OscState : IPredictedData<OscState> {
public sfloat phase; public sfloat speed; public sfloat amplitude;
public void Dispose() {}
}
public class Oscillator : DeterministicIdentity<OscState>
{
protected override OscState GetInitialState() => new OscState {
speed = (sfloat)1.5f, amplitude = (sfloat)2f, phase = 0
};
protected override void GetUnityState(ref OscState s) { /* read if needed */ }
protected override void SetUnityState(OscState s) { /* apply on rollback if needed */ }
protected override void Simulate(ref OscState s, sfloat dt)
{
s.phase += s.speed * dt;
var y = sfloat.Sin(s.phase) * s.amplitude;
// use y for downstream deterministic effects; drive visuals in UpdateView
}
protected override void UpdateView(OscState view, OscState? verified)
{
// Convert to float for rendering only
}
}Inputs and Convergence
DeterministicIdentity<INPUT, STATE> mirrors PredictedIdentity<INPUT, STATE>: implement GetFinalInput, UpdateInput, and SanitizeInput the same way, and Simulate(INPUT input, ref STATE state, sfloat delta) receives the deterministic delta. Because every peer must feed identical inputs into the simulation, these inputs are delivered on a guaranteed transcript rather than best‑effort: the server includes every input tick since the client’s last acknowledged frame in each outgoing frame, up to a 32 tick window, so a lost packet is repaired by the next one.
If a client falls further behind than that window, the server sends a full frame that re‑anchors its deterministic timeline. Either way the simulation converges; packet loss can delay deterministic state but never permanently fork it.
Best Practices
- Use
sfloatand integer math for all simulation‑impacting calculations. - Avoid sampling Unity time or random APIs directly; use
PredictedTimeandPredictedRandom. - Keep any conversions to
floatpurely inUpdateView. - Only mutate state from the simulated timeline:
SimulationStart,Simulate, or events that fire from inside another identity's simulation (likePredictedPlayers.onPlayerAdded). Never mutate state fromAwake,LateAwake, or other Unity callbacks; those run at setup time, which differs per peer, and deterministic state is never corrected afterwards.SimulationStartis the right place for one-time setup that reads other identities, since it runs at the first simulated tick and its executed flag is part of the rollback state. - Leave Desync Policy on
Ignoreunless you need divergence detection.Reportis a good development default;ResyncorCorrectare safe to ship when you want automatic recovery, at the cost of periodic hashing and reporting.