PurrDiction

Predicted Identities

Predicted identities are regular Unity components that PurrDiction simulates on its tick timeline. The system snapshots and reconciles their STATE against server authority, while your code focuses on clean, single-player-style logic.

Variants

  • PredictedIdentity<STATE>
    • For stateful systems without direct player input (AI, timers, pooled objects).
    • Implement: GetInitialState, GetUnityState, SetUnityState, Simulate, LateSimulate (optional), UpdateView (optional).
  • PredictedIdentity<INPUT, STATE>
    • Adds an INPUT pipe for local/remote control. Implement: GetFinalInput, UpdateInput (optional), SanitizeInput, Simulate(INPUT, ref STATE, float), ModifyExtrapolatedInput (optional).
  • StatelessPredictedIdentity
    • For pure event/logic systems that donโ€™t carry custom state. Override Simulate(float delta).

Both INPUT and STATE must be structs that implement the appropriate prediction interfaces (IPredictedData, IPredictedData<T>).


Lifecycle Hooks

  • LateAwake() โ€” Called once after fresh spawn initialization. Viewโ€‘only setup is appropriate here.
  • SimulationStart() โ€” First tick only, before simulation begins. Good for caching or oneโ€‘time transitions.
  • Simulate(...) โ€” Replayable simulation each tick. Use only STATE, INPUT, and predicted or deterministic data sources.
  • LateSimulate(...) โ€” Optional late pass after Simulate each tick (e.g., composing derived values).
  • Destroyed() โ€” Called when despawning/cleanup occurs (pool or destroy).
  • ResetState() โ€” Clears ownership/IDs and resets interpolation and internal caches for pooling.

Unity bridging:

  • GetUnityState(ref STATE state) โ€” Read Unity components into STATE.
  • SetUnityState(STATE state) โ€” Apply STATE back to Unity components after rollback.

View & interpolation:

  • UpdateView(STATE viewState, STATE? verified) โ€” Render using the current interpolated viewState. verified holds the last server snapshot, when present.
  • ResetInterpolation() โ€” Clear internal smoothing/error.

Ownership and Control

  • owner โ€” Optional PlayerID who owns this identity.
  • isOwner โ€” True if owner == PredictionManager.localPlayer on this client.
  • isController โ€” True for the owner on clients; on server, also true for bots/AI cases.
  • OnViewOwnerChanged(oldOwner, newOwner) โ€” Viewโ€‘only callback when ownership changes; do not mutate simulation here.

Use these flags for visuals (camera, highlights, UI). Keep simulation deterministic and independent of local presentation.


Prediction Policy

Every identity resolves a client timeline from its local setting or the nearest PredictionPolicyScope. The default is FullPrediction. Use PredictedIfOwned for the common local-player/remote-player split, ServerRelay for verified-only client copies, and SoftCorrection only on identities that explicitly support it.

// Persistent local override, including after a scope refresh.
SetPredictionPolicyOverride(PredictionPolicy.PredictedIfOwned);

// Return to the nearest active scope.
UsePredictionPolicyScope();

See Prediction Policies for the behavior, scope rules, and runtime APIs.


Example Skeleton (Stateful with Input)

public struct MyInput : IPredictedData {
    public Vector2? input; public void Dispose() {}
}

public struct MyState : IPredictedData<MyState> {
    public Vector3 pos; public Quaternion rot; public void Dispose() {}
}

public class MyPredicted : PredictedIdentity<MyInput, MyState>
{
    protected override MyState GetInitialState() => new MyState {
        pos = transform.position, rot = transform.rotation
    };

    protected override void GetUnityState(ref MyState s)
    { s.pos = transform.position; s.rot = transform.rotation; }

    protected override void SetUnityState(MyState s)
    { transform.SetPositionAndRotation(s.pos, s.rot); }

    protected override void GetFinalInput(ref MyInput i)
    { i.input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")); }

    protected override void SanitizeInput(ref MyInput i)
    {
        if (!i.input.HasValue) return;
        var v = Vector2.ClampMagnitude(new Vector2(i.input.Value.x, i.input.Value.y), 1f);
        i.input = v;
    }

    protected override void Simulate(MyInput i, ref MyState s, float dt)
    {
        if (!i.input.HasValue) return;
        transform.position += new Vector3(i.input.Value.x, 0, i.input.Value.y) * dt * 5f;
    }

    protected override void UpdateView(MyState view, MyState? verified)
    { transform.SetPositionAndRotation(view.pos, view.rot); }
}