Table of Contents

Multiplayer Architecture

Core rule: the server is authoritative. Clients predict locally and reconcile when the server corrects them. All networked state lives in explicit state structs — never in loose fields.


High-Level Architecture

flowchart TB
    subgraph S[" SERVER "]
        direction LR
        S1[Input Queue] --> S2[Simulate] --> S3[Snapshot] --> S4[Broadcast]
    end

    S4 -- "state + corrections" --> C1

    subgraph C[" CLIENT "]
        direction LR
        C1[Collect Input] --> C2[Predict] --> C3[Receive] --> C4[Reconcile] --> C5[Render]
    end

NetworkMode Flags

namespace PixelEngine.Networking
{
    [Flags]
    public enum NetworkMode
    {
        None            = 0,
        Client          = 1 << 0,   // Receives state, sends input
        Server          = 1 << 1,   // Authoritative simulation
        Host            = Client | Server,  // Listen server
        DedicatedServer = 1 << 2,   // Headless server, no local player
    }
}
Mode Simulates Renders Accepts Input Sends State
Client Predict only Yes Local No
Server Yes No Remote Yes
Host Yes Yes Local + Remote Yes
DedicatedServer Yes No Remote Yes

Dual-State Pattern

Every networked entity stores two state structs to separate concerns.

State Scope Replicated Typical Contents
GlobalState Visible to all clients Yes Position, health, status effects, team ID
InternalState Server + owning client only No Cooldown timers, ammo counts, ability charges
namespace PixelEngine.Networking
{
    public struct GlobalState : INetworkState
    {
        public Vector3 Position;
        public Quaternion Rotation;
        public int Health;
        public byte StatusFlags;
    }

    public struct InternalState : INetworkState
    {
        public float CooldownTimer;
        public int ResourceCount;
        public byte AbilityCharges;
    }
}

Rule: never read InternalState on a remote proxy — it is not replicated.


Network Tick Pipeline

flowchart LR
    A["1. INPUT\nCOLLECTION"] --> B["2. FIXED\nSIMULATION"] --> C["3. STATE\nSYNC"] --> D["4. PRESENTATION"]
Stage Where What
Input Collection Client, before FixedUpdate Read input devices, write to InputPayload
Fixed Simulation FixedUpdate Apply input, advance physics, mutate state
State Sync After simulation Server broadcasts GlobalState snapshots
Presentation Update / LateUpdate Interpolate, animate, render

NetworkExecutionArgs

Passed into every networked system's tick method. Carries everything the system needs without global lookups.

namespace PixelEngine.Networking
{
    public readonly struct NetworkExecutionArgs
    {
        public readonly int               Tick;
        public readonly float             DeltaTime;
        public readonly NetworkMode       Mode;
        public readonly ExecutionMode     Execution;
        public readonly bool              IsResimulation;
        public readonly INetworkRunner    Runner;
    }
}
public void NetworkTick(NetworkExecutionArgs args)
{
    if (args.IsResimulation)
        return; // skip VFX / audio on re-sim

    if (args.Execution.HasFlag(ExecutionMode.Simulation))
        Simulate(args.DeltaTime);
}

Rollback & Reconciliation

How It Works

flowchart LR
    T10["T=10"] --> T11["T=11"] --> T12["T=12"] --> T13["T=13"] --> T14["T=14\n(predicted)"]
    T12 -.->|"Server says\nstate differs"| R["Rollback"]
    R -->|"1. Restore server state at T=12\n2. Re-simulate T=12→T=14\n3. Continue from corrected state"| T14

RollbackBuffer<T>

namespace PixelEngine.Networking
{
    public class RollbackBuffer<T> where T : struct, INetworkState
    {
        private readonly T[] _buffer;
        private readonly int _capacity;

        public RollbackBuffer(int capacity)
        {
            _capacity = capacity;
            _buffer = new T[capacity];
        }

        /// <summary>Store state for a given tick.</summary>
        public void Save(int tick, in T state)
            => _buffer[tick % _capacity] = state;

        /// <summary>Retrieve state for a given tick.</summary>
        public ref readonly T Load(int tick)
            => ref _buffer[tick % _capacity];

        /// <summary>Check if server state matches predicted state.</summary>
        public bool NeedsRollback(int tick, in T serverState)
            => !_buffer[tick % _capacity].Equals(serverState);
    }
}

Reconciliation Flow

Step Action
1 Receive authoritative state for tick T from server
2 Compare with RollbackBuffer.Load(T)
3 If mismatch → restore server state at T
4 Re-apply stored inputs from T+1 to current tick
5 Resume normal prediction

Interpolation for Remote Players

Remote entities do not predict. They interpolate between two received snapshots.

// Remote entity — render buffer is N ticks behind
int renderTick = currentTick - interpolationDelay;

ref readonly var from = ref stateBuffer.Load(renderTick);
ref readonly var to   = ref stateBuffer.Load(renderTick + 1);

float alpha = SimulationTime.updateInterpolationAlpha;

transform.position = Vector3.Lerp(from.Position, to.Position, alpha);
transform.rotation = Quaternion.Slerp(from.Rotation, to.Rotation, alpha);
Entity Type Technique Latency
Local player Client-side prediction ~0 (instant)
Remote player Snapshot interpolation Render delay (2–3 ticks)
Server entity Authoritative, no interpolation 0

Network Lifecycle Callbacks

namespace PixelEngine.Networking
{
    public interface INetworkCallbacks
    {
        /// <summary>Called when entity is spawned on the network.</summary>
        void OnNetworkSpawn(NetworkExecutionArgs args);

        /// <summary>Called every fixed network tick.</summary>
        void OnNetworkTick(NetworkExecutionArgs args);

        /// <summary>Called when entity is despawned from the network.</summary>
        void OnNetworkDespawn(NetworkExecutionArgs args);

        /// <summary>Called when authority changes (e.g., host migration).</summary>
        void OnAuthorityChanged(bool hasAuthority);
    }
}
Callback When Use For
OnNetworkSpawn Entity first appears on network Initialize state, subscribe to events
OnNetworkTick Every fixed network tick Core simulation logic
OnNetworkDespawn Entity removed from network Cleanup, unsubscribe
OnAuthorityChanged Authority transfers Enable/disable prediction

Client vs Server Frame Comparison

flowchart TD
    subgraph CL[" CLIENT "]
        direction TB
        CI[Read Input] --> CP[Predict]
        CP --> CR[Receive State]
        CR --> CRR{Match?}
        CRR -->|No| CRB[Rollback]
        CRR -->|Yes| CV[Render]
        CRB --> CV
    end

    subgraph SV[" SERVER "]
        direction TB
        SI[Receive Inputs] --> SS[Simulate]
        SS --> SB[Broadcast State]
    end

    CI -. "input" .-> SI
    SB -. "snapshots" .-> CR

Quick Rules

# Rule
1 Server is always authoritative — clients predict, never dictate.
2 All networked state lives in typed structs (GlobalState, InternalState).
3 Never read InternalState on remote proxies.
4 Always save predicted state to RollbackBuffer before advancing.
5 Skip VFX / audio during re-simulation (args.IsResimulation).
6 Remote entities use interpolation, not prediction.
7 Stamp every input with its tick number.
8 Validate all client input on the server — never trust the client.

Unity Documentation