Validated SyncVar

This is a more advanced variation of the SyncVar which allows you to get the best of both world in terms of client sided responsiveness with server authorized changes.

The idea of the validated SyncVar is quite easy: - Client makes local change and gets immediate response - Server is notified of this change and can validate whether the change should be validated - If the change is validated, it's sent to everyone - If the change is not validated, it's returned to the owner gets a callback

This allows you to easily add cheat proofing to the Validated SyncVar whilst still keeping it fully responsive in fair/valid scenarios.

Below is a simple example usage, which only allows the value to be counted up, but never down.

private ValidatedSyncVar<int> _testVar = new();

private void OnEnable()
{
    _testVar.serverValidation += ServerValidator;
    _testVar.onValidationFail += OnValidationFail;

    _testVar.onChangedWithOld += OnChanged;
}

private void OnDisable()
{
    _testVar.serverValidation -= ServerValidator;
    _testVar.onValidationFail -= OnValidationFail;
    
    _testVar.onChangedWithOld -= OnChanged;
}

private bool ServerValidator(int oldValue, int newValue)
{
    //Only the server will reach this. This is called whenever a change occurs. 
    if (oldValue > newValue)
        return false;
    
    return true;
}

private void OnValidationFail(int failedValue, int authoritativeValue)
{
    //Only the owner will receive this if the validation fails
    Debug.Log($"Validation failed on {failedValue}. Returning to {authoritativeValue}");
}

private void OnChanged(int oldValue, int newValue, bool validated)
{
    //This is called immediately with validated false for the owner, 
    //and everyone received with validated = true if it goes through
    Debug.Log($"Value changed: {oldValue} -> {newValue} | Validated: {validated}");
}

Behavior & Expectations

  • Authority

    • Uses parent.IsController(true) for writes.

    • If owner is null, only server can write.

    • Module is owner-authed and bound to its NetworkIdentity.

  • Events

    • onChanged(old, new, validated)

    • onChangedWithOld(old, new, validated)

    • Owner emits twice on success: first with validated=false (local predict), then validated=true (server accept).

    • On reject, owner emits validated=true when reverting to authoritative, and fires onValidationFail(failed, authoritative).

    • Non-owners receive only one event with validated=true.

    • Host also emits predict then validated (consistent with clients).

  • Validation

    • serverValidation is multicast; all handlers must return true to accept.

    • Runs on server for every candidate change (including host).

  • IDs & Ordering

    • Each candidate has a PackedUInt packetId.

    • Client ignores AcceptOwner/RejectOwner with packetId < _pendingId (stale).

    • This keeps the display at the most recent local prediction until the matching latest ack arrives.

  • Common Scenarios

    • Owner set accepted: owner gets (old→new, false) then (old→new, true); others get (old→new, true).

    • Owner set rejected: owner gets (old→candidate, false) then (candidate→authoritative, true) + onValidationFail(candidate, authoritative); others get nothing.

    • Rapid 1→2→3 with latency: owner shows 3 immediately; stale acks for 1/2 are ignored; only ack for 3 applies.

    • Server sets value (no prediction): everyone gets a single (old→new, true). Owner does not get a false event in this path.

    • No owner: only server may set; client attempts are rejected.

    • Owner change: non-owner listeners subscribe to authoritative stream; pending owner state is cleared on change.

    • Late joiner: receives current authoritative via inner SyncVar; only a single (old→new, true) if different.

Last updated