Back to Gaming

Unity Game Engine Series Part 12: Multiplayer & Networking

March 31, 2026 Wasil Zafar 42 min read

Multiplayer is the ultimate test of a game developer's skills. From client-server architecture to lag compensation, from RPCs to state synchronization, this part covers everything you need to build responsive, fair, and scalable networked games in Unity.

Table of Contents

  1. Networking Models
  2. Netcode for GameObjects
  3. Synchronization
  4. Multiplayer Challenges
  5. Exercises & Self-Assessment
  6. Networking Spec Generator
  7. Conclusion & Next Steps

Introduction: The Multiplayer Challenge

Series Overview: This is Part 12 of our 16-part Unity Game Engine Series. Multiplayer networking is widely considered the hardest problem in game development. We'll demystify it by covering networking models, Unity's Netcode for GameObjects, synchronization strategies, and the real-world challenges of building networked games.

Multiplayer networking is the art of making geographically separated players feel like they're in the same room. The fundamental challenge is that the speed of light imposes a minimum latency of ~30ms even between nearby cities, and real-world internet adds routing hops, packet loss, and jitter on top of that. Every multiplayer game is, in essence, a distributed systems problem wrapped in a fun experience.

The history of multiplayer gaming stretches from the LAN parties of the 1990s (Doom, Quake) through the early internet era (EverQuest, StarCraft on Battle.net) to today's global matchmaking services handling millions of concurrent players. The core problems have remained constant: how do you keep everyone in sync, make the game feel responsive, and prevent cheating?

Key Insight: There is no "correct" multiplayer architecture. Every model is a trade-off between responsiveness (how fast does the game feel?), consistency (does everyone see the same thing?), and security (can players cheat?). The right choice depends on your game's genre, player count, and competitive requirements.

A Brief History of Multiplayer Gaming

Era Technology Milestone
1993 Peer-to-peer LAN Doom: 4-player deathmatch over IPX/SPX
1996 Client-server, QuakeWorld prediction Quake: first client-side prediction for internet play
2001 Dedicated servers, server-side hit detection Counter-Strike 1.6: authoritative server model
2007 Lag compensation, tick-rate systems Halo 3: advanced netcode for console shooters
2015 Rollback netcode, GGPO Fighting games adopt rollback for fair online play
2020s Cloud gaming, relay services, EOS Unity Netcode, Epic Online Services, global matchmaking
Case Study

Rocket League: 60Hz Physics Over the Internet

Rocket League by Psyonix runs its physics simulation at 120Hz on the server and sends state snapshots to clients at 60Hz. The game uses client-side prediction for the local car, server reconciliation to correct mispredictions, and interpolation for remote players. Since the game is entirely physics-driven (ball trajectory, car collisions), even tiny desynchronizations are visible. Psyonix solved this with aggressive snapshot interpolation and prediction correction that blends the corrected position over several frames rather than snapping, creating the illusion of a perfectly synchronized physics world even at 80ms+ ping.

120Hz Server Client Prediction Smooth Correction Physics Sync

1. Networking Models

1.1 Client-Server Architecture

In a client-server model, one machine (the server) is the authoritative source of truth. All game state changes must be validated by the server, and clients receive state updates. This is the industry standard for competitive games because it provides a single point of authority that prevents most forms of cheating.

Variant How It Works Best For
Authoritative (no prediction) Client sends input, waits for server response Turn-based games, strategy, card games
Authoritative + Prediction Client predicts locally, server corrects mispredictions FPS, action games, platformers
Listen Server One player's machine acts as both client and server Small-scale multiplayer, co-op games
Dedicated Server Headless server running on cloud infrastructure Competitive games, large player counts

1.2 Peer-to-Peer & Relay Servers

In peer-to-peer (P2P), all clients communicate directly with each other without a central server. Each client runs the full game simulation. This eliminates server hosting costs but introduces challenges around trust (who resolves conflicts?) and NAT traversal (how do players behind routers connect?).

Relay servers are a hybrid approach: a lightweight server forwards packets between players without running the game simulation. This solves NAT traversal while keeping costs low. Unity Relay Service provides this out of the box.

1.3 Choosing the Right Model

Game Type Recommended Model Reason
Competitive FPS Dedicated server + prediction Anti-cheat, consistent tick rate, fair play
Co-op PvE Listen server or relay Lower cost, cheating is less critical
Fighting Game P2P with rollback Minimal latency (only 1 hop), 2 players
MMO Dedicated server clusters Thousands of players, persistent world state
Mobile Casual Relay + authoritative validation Simple networking, NAT-friendly, low cost
Party Game Relay or listen server Low complexity, small player counts

2. Netcode for GameObjects

Unity's official networking solution is Netcode for GameObjects (NGO). It provides NetworkManager, NetworkBehaviour, NetworkVariable, RPCs, and object spawning out of the box.

2.1 NetworkManager & NetworkBehaviour

using Unity.Netcode;
using UnityEngine;

// NetworkBehaviour is the networked version of MonoBehaviour
public class PlayerNetwork : NetworkBehaviour
{
    [SerializeField] private float moveSpeed = 5f;

    // NetworkVariable: automatically synced from server to all clients
    private NetworkVariable<Vector3> netPosition = new NetworkVariable<Vector3>(
        default,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Server
    );

    private NetworkVariable<int> netHealth = new NetworkVariable<int>(
        100,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Server
    );

    public override void OnNetworkSpawn()
    {
        // Called when this object is spawned on the network
        if (IsOwner)
        {
            Debug.Log("I am the local player!");
            // Set up camera, input, etc.
        }

        // Listen for health changes
        netHealth.OnValueChanged += OnHealthChanged;
    }

    public override void OnNetworkDespawn()
    {
        netHealth.OnValueChanged -= OnHealthChanged;
    }

    private void Update()
    {
        if (!IsOwner) return; // Only the owner processes input

        Vector3 input = new Vector3(
            Input.GetAxisRaw("Horizontal"),
            0,
            Input.GetAxisRaw("Vertical")
        );

        if (input.sqrMagnitude > 0)
        {
            // Send movement request to server
            MoveServerRpc(input.normalized);
        }
    }

    private void OnHealthChanged(int oldValue, int newValue)
    {
        Debug.Log($"Health changed: {oldValue} -> {newValue}");
        // Update health bar UI...
    }
}

2.2 ServerRpc & ClientRpc

RPCs (Remote Procedure Calls) are the primary way clients and servers communicate specific actions. ServerRpc flows from client to server; ClientRpc flows from server to all (or specific) clients.

using Unity.Netcode;
using UnityEngine;

public class PlayerCombat : NetworkBehaviour
{
    [SerializeField] private float attackRange = 2f;
    [SerializeField] private int attackDamage = 25;
    [SerializeField] private float attackCooldown = 0.5f;

    private float lastAttackTime;

    private void Update()
    {
        if (!IsOwner) return;

        if (Input.GetMouseButtonDown(0) && Time.time > lastAttackTime + attackCooldown)
        {
            lastAttackTime = Time.time;
            // Tell the server we want to attack
            AttackServerRpc();
        }
    }

    // ServerRpc: called on a client, executed on the server
    [ServerRpc]
    private void AttackServerRpc()
    {
        // Server validates and processes the attack
        // (The server is the authority; it decides if the attack hits)
        Collider[] hits = Physics.OverlapSphere(transform.position, attackRange);
        foreach (var hit in hits)
        {
            if (hit.TryGetComponent<PlayerHealth>(out var health) &&
                hit.GetComponent<NetworkObject>().OwnerClientId != OwnerClientId)
            {
                health.TakeDamage(attackDamage);
                // Notify all clients about the hit
                OnAttackHitClientRpc(hit.transform.position);
            }
        }
    }

    // ClientRpc: called on the server, executed on all clients
    [ClientRpc]
    private void OnAttackHitClientRpc(Vector3 hitPosition)
    {
        // All clients play hit effect at the position
        // Spawn particles, play sound, screen shake, etc.
        Debug.Log($"Hit at {hitPosition}!");
    }

    // ServerRpc with parameters and RequireOwnership
    [ServerRpc]
    private void MoveServerRpc(Vector3 direction)
    {
        // Server validates and applies movement
        Vector3 newPos = transform.position + direction * 5f * Time.deltaTime;
        transform.position = newPos;
    }

    // ClientRpc targeting specific clients
    [ClientRpc]
    private void NotifyClientRpc(string message, ClientRpcParams rpcParams = default)
    {
        Debug.Log($"Server says: {message}");
    }
}

2.3 NetworkVariable & Ownership

NetworkVariables automatically synchronize data from the server to clients. They support value change callbacks and configurable read/write permissions:

using Unity.Netcode;
using UnityEngine;

public class PlayerHealth : NetworkBehaviour
{
    // Only the server can write; everyone can read
    public NetworkVariable<int> Health = new NetworkVariable<int>(
        100,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Server
    );

    // Player name: set by owner, readable by all
    public NetworkVariable<Unity.Collections.FixedString64Bytes> PlayerName =
        new NetworkVariable<Unity.Collections.FixedString64Bytes>(
            default,
            NetworkVariableReadPermission.Everyone,
            NetworkVariableWritePermission.Owner
        );

    // NetworkList for collections
    public NetworkList<int> Inventory;

    private void Awake()
    {
        Inventory = new NetworkList<int>();
    }

    public override void OnNetworkSpawn()
    {
        if (IsOwner)
        {
            PlayerName.Value = $"Player_{OwnerClientId}";
        }

        Health.OnValueChanged += (oldVal, newVal) =>
        {
            Debug.Log($"{PlayerName.Value} health: {oldVal} -> {newVal}");
            if (newVal <= 0) HandleDeath();
        };
    }

    // Only callable on the server (since Health is server-write)
    public void TakeDamage(int amount)
    {
        if (!IsServer) return;
        Health.Value = Mathf.Max(0, Health.Value - amount);
    }

    public void Heal(int amount)
    {
        if (!IsServer) return;
        Health.Value = Mathf.Min(100, Health.Value + amount);
    }

    private void HandleDeath()
    {
        if (IsServer)
        {
            // Server handles respawn logic
            RespawnClientRpc();
        }
    }

    [ClientRpc]
    private void RespawnClientRpc()
    {
        Debug.Log($"{PlayerName.Value} died! Respawning...");
    }
}
Pro Tip: Use NetworkVariable for persistent state (health, score, position) and RPCs for events (shoot, jump, emote). NetworkVariables are automatically synced to late-joining clients, while RPCs are fire-and-forget, meaning late joiners miss them.

3. Synchronization

3.1 State Sync vs Input Sync

Approach How It Works Pros Cons
State Sync Server sends world state snapshots to clients Simple, handles late joiners, flexible Higher bandwidth, visual smoothing needed
Input Sync Clients exchange inputs, all run deterministic simulation Low bandwidth, perfect sync if deterministic Requires deterministic engine, hard to handle late join

3.2 Client-Side Prediction & Server Reconciliation

Client-side prediction is the key technique that makes networked games feel responsive. Instead of waiting for the server to confirm each action, the client immediately predicts the result locally. When the server's authoritative response arrives, the client corrects any misprediction.

using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;

public class PredictedPlayerMovement : NetworkBehaviour
{
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private CharacterController controller;

    // Buffer of predicted inputs for reconciliation
    private struct InputPayload
    {
        public uint Tick;
        public Vector3 Direction;
    }

    private struct StatePayload
    {
        public uint Tick;
        public Vector3 Position;
    }

    private Queue<InputPayload> inputBuffer = new Queue<InputPayload>();
    private uint currentTick = 0;
    private const int BUFFER_SIZE = 1024;

    // Server-confirmed state
    private NetworkVariable<StatePayload> serverState = new NetworkVariable<StatePayload>();

    private StatePayload[] predictionBuffer = new StatePayload[BUFFER_SIZE];

    public override void OnNetworkSpawn()
    {
        serverState.OnValueChanged += OnServerStateChanged;
    }

    private void Update()
    {
        if (!IsOwner) return;

        currentTick++;
        Vector3 input = new Vector3(
            Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")
        ).normalized;

        // 1. Apply input locally (prediction)
        controller.Move(input * moveSpeed * Time.deltaTime);

        // 2. Store prediction for later reconciliation
        predictionBuffer[currentTick % BUFFER_SIZE] = new StatePayload
        {
            Tick = currentTick,
            Position = transform.position
        };

        // 3. Send input to server
        SendInputServerRpc(new InputPayload { Tick = currentTick, Direction = input });
    }

    [ServerRpc]
    private void SendInputServerRpc(InputPayload input)
    {
        // Server applies input authoritatively
        controller.Move(input.Direction * moveSpeed * Time.fixedDeltaTime);

        // Broadcast authoritative state
        serverState.Value = new StatePayload
        {
            Tick = input.Tick,
            Position = transform.position
        };
    }

    private void OnServerStateChanged(StatePayload oldState, StatePayload newState)
    {
        if (!IsOwner) return;

        // Compare server state with our prediction
        StatePayload predicted = predictionBuffer[newState.Tick % BUFFER_SIZE];
        float error = Vector3.Distance(predicted.Position, newState.Position);

        if (error > 0.01f)
        {
            // Misprediction! Snap to server position and re-simulate
            Debug.Log($"Reconciliation: error={error:F3}m at tick {newState.Tick}");
            transform.position = newState.Position;

            // Re-apply all inputs since the corrected tick
            // (This is the reconciliation step)
        }
    }
}

3.3 Lag Compensation & Interpolation

Snapshot interpolation smooths remote player movement by rendering them slightly in the past (typically 100-200ms) and interpolating between received snapshots. This eliminates jitter from irregular packet delivery.

using UnityEngine;
using System.Collections.Generic;

public class NetworkInterpolation : MonoBehaviour
{
    [SerializeField] private float interpolationDelay = 0.1f; // 100ms behind

    private struct Snapshot
    {
        public float Timestamp;
        public Vector3 Position;
        public Quaternion Rotation;
    }

    private List<Snapshot> snapshotBuffer = new List<Snapshot>();
    private float renderTimestamp;

    public void ReceiveSnapshot(Vector3 position, Quaternion rotation, float serverTime)
    {
        snapshotBuffer.Add(new Snapshot
        {
            Timestamp = serverTime,
            Position = position,
            Rotation = rotation
        });

        // Keep buffer trimmed (max 30 snapshots)
        while (snapshotBuffer.Count > 30)
            snapshotBuffer.RemoveAt(0);
    }

    private void Update()
    {
        renderTimestamp = Time.time - interpolationDelay;

        // Find the two snapshots surrounding our render time
        for (int i = 0; i < snapshotBuffer.Count - 1; i++)
        {
            if (snapshotBuffer[i].Timestamp <= renderTimestamp &&
                snapshotBuffer[i + 1].Timestamp >= renderTimestamp)
            {
                float t = Mathf.InverseLerp(
                    snapshotBuffer[i].Timestamp,
                    snapshotBuffer[i + 1].Timestamp,
                    renderTimestamp
                );

                // Interpolate between snapshots
                transform.position = Vector3.Lerp(
                    snapshotBuffer[i].Position,
                    snapshotBuffer[i + 1].Position, t
                );
                transform.rotation = Quaternion.Slerp(
                    snapshotBuffer[i].Rotation,
                    snapshotBuffer[i + 1].Rotation, t
                );
                return;
            }
        }

        // If no valid pair found, extrapolate from last known
        if (snapshotBuffer.Count > 0)
        {
            transform.position = snapshotBuffer[snapshotBuffer.Count - 1].Position;
        }
    }
}
Critical Concept: Lag compensation is essential for hit detection in shooters. When a player fires, the server must rewind all other players to where they were at the shooter's render time (accounting for latency), check the hit, then restore the current state. Without this, high-ping players could never hit moving targets because by the time their shot reaches the server, the target has already moved.

4. Multiplayer Challenges

4.1 Latency & Bandwidth Optimization

Metric Good Acceptable Poor
RTT (Round Trip Time) < 50ms 50-150ms > 150ms
Packet Loss < 1% 1-3% > 3%
Jitter < 10ms variance 10-30ms > 30ms
Bandwidth per player < 5 KB/s 5-20 KB/s > 20 KB/s

Bandwidth optimization techniques include:

  • Delta compression: Only send values that changed since the last snapshot
  • Quantization: Reduce float precision (e.g., 2 bytes instead of 4 for position)
  • Interest management: Only send data about nearby objects (area of interest)
  • Variable tick rate: Send important objects more frequently than background objects
  • Bit packing: Pack boolean flags into single bytes

4.2 Cheating Prevention & Security

Golden Rule of Multiplayer Security: Never trust the client. Validate everything on the server. If the client says "I moved 100 units this frame," the server should check if that's physically possible within the time elapsed. If the client says "I hit the enemy," the server should verify line of sight and range.
using Unity.Netcode;
using UnityEngine;

public class ServerValidation : NetworkBehaviour
{
    private const float MAX_SPEED = 10f;
    private const float SPEED_TOLERANCE = 1.2f; // 20% tolerance for lag
    private Vector3 lastValidatedPosition;
    private float lastValidationTime;

    [ServerRpc]
    public void MoveRequestServerRpc(Vector3 requestedPosition)
    {
        if (!IsServer) return;

        float timeDelta = Time.time - lastValidationTime;
        float maxDistance = MAX_SPEED * timeDelta * SPEED_TOLERANCE;
        float actualDistance = Vector3.Distance(lastValidatedPosition, requestedPosition);

        if (actualDistance > maxDistance)
        {
            // Possible speed hack! Reject and snap back
            Debug.LogWarning($"Client {OwnerClientId}: speed violation " +
                           $"(moved {actualDistance:F1}m, max {maxDistance:F1}m)");
            CorrectPositionClientRpc(lastValidatedPosition);
            return;
        }

        // Validate: is the position inside valid bounds?
        if (!IsPositionValid(requestedPosition))
        {
            Debug.LogWarning($"Client {OwnerClientId}: invalid position");
            CorrectPositionClientRpc(lastValidatedPosition);
            return;
        }

        // Accept the move
        lastValidatedPosition = requestedPosition;
        lastValidationTime = Time.time;
        transform.position = requestedPosition;
    }

    private bool IsPositionValid(Vector3 pos)
    {
        // Check bounds, collision, no-clip, etc.
        return pos.y >= 0 && pos.y < 100 &&
               !Physics.CheckSphere(pos, 0.3f, LayerMask.GetMask("Walls"));
    }

    [ClientRpc]
    private void CorrectPositionClientRpc(Vector3 correctedPosition)
    {
        if (IsOwner) transform.position = correctedPosition;
    }
}

4.3 Transport Layer & Protocols

Protocol Reliability Speed Use In Games
TCP Guaranteed delivery, ordered Slower (retransmission delays) Chat, inventory, matchmaking, login
UDP No guarantee, unordered Fast (no retransmission overhead) Position updates, game state, real-time data
Reliable UDP Selective reliability, sequenced channels Best of both worlds Most game networking (Unity Transport default)
WebSocket Reliable, TCP-based, browser-compatible Moderate WebGL games, browser-based multiplayer
Case Study

Among Us: Simple Networking Done Right

Among Us by InnerSloth (built in Unity) demonstrates that simple networking is often best. The game uses a client-server model with the host acting as server. Since the game is slow-paced (no real-time physics, no twitch shooting), it can tolerate higher latency. Movement is synced at a low rate, and most game-critical actions (voting, killing, tasks) are discrete events handled as reliable RPCs. The lesson: match your networking complexity to your gameplay needs. Among Us works perfectly without client-side prediction, lag compensation, or 60Hz tick rates.

Simple Architecture Event-Driven Host-as-Server Latency Tolerant
Case Study

Fall Guys: Server Architecture at Scale

Fall Guys by Mediatonic handles 60 players per match with a dedicated server architecture. The game runs physics authoritatively on the server, which is critical because player-vs-player collisions determine who wins. Clients send inputs and receive state snapshots with interpolation. The servers are hosted on cloud infrastructure (originally AWS, later Epic's backend) that auto-scales based on player demand. During the game's launch, traffic surged to over 1.5 million concurrent players, requiring aggressive horizontal scaling and regional server deployment across multiple continents.

60 Players Authoritative Physics Cloud Scaling Regional Servers

Exercises & Self-Assessment

Exercise 1

Basic Multiplayer Lobby

Set up a minimal multiplayer game with Netcode for GameObjects:

  1. Install the Netcode for GameObjects package from Package Manager
  2. Create a NetworkManager with a player prefab (cube + NetworkObject + NetworkBehaviour)
  3. Implement Host/Join buttons using NetworkManager.Singleton.StartHost() and StartClient()
  4. Sync player position using NetworkVariable or NetworkTransform
  5. Add a chat system using ServerRpc (send message) and ClientRpc (broadcast to all)
  6. Display connected player names using NetworkVariable<FixedString64Bytes>
Exercise 2

Authoritative Server Validation

Build a movement system with server-side cheat prevention:

  1. Client sends movement input to the server via ServerRpc
  2. Server validates: speed check, boundary check, collision check
  3. Server applies validated movement and syncs via NetworkVariable
  4. Intentionally try to "cheat" by sending invalid movement and verify the server rejects it
  5. Add a server-side log that flags suspicious movement patterns
Exercise 3

Snapshot Interpolation Visualizer

Build a visual tool that demonstrates network interpolation:

  1. Create a "remote player" that receives position snapshots at simulated intervals (5-20 Hz)
  2. Implement snapshot interpolation with a configurable delay (50-200ms)
  3. Add a "no interpolation" mode to see raw jittery updates
  4. Add a "extrapolation" mode for when snapshots arrive late
  5. Visualize: server position (green), predicted position (blue), interpolated position (red)
Exercise 4

Reflective Questions

  1. Why do fighting games prefer P2P with rollback over client-server? What unique requirements does the fighting game genre have?
  2. Explain the difference between NetworkVariable and RPCs. When would you use each? Give three examples of each.
  3. A player reports that their character "rubberbands" (snaps back to a previous position). What causes this and how would you mitigate it?
  4. Design the networking architecture for a 100-player battle royale. What model would you use? How would you handle bandwidth with 100 players?
  5. Why is UDP preferred over TCP for real-time game data? What problems does TCP's guaranteed delivery cause in a fast-paced game?

Networking Specification Document Generator

Generate a professional networking specification for your multiplayer game. Download as Word, Excel, PDF, or PowerPoint.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Next Steps

Multiplayer networking is the most challenging aspect of game development, but with the right knowledge and tools, it's entirely manageable. Here are the key takeaways from Part 12:

  • Client-server authoritative is the industry standard for competitive games; the server validates all game state and clients trust the server's authority
  • Netcode for GameObjects provides NetworkManager, NetworkBehaviour, NetworkVariable, and RPCs (ServerRpc/ClientRpc) for building networked games in Unity
  • NetworkVariables are for persistent state (auto-synced to late joiners); RPCs are for events (fire-and-forget)
  • Client-side prediction makes games feel responsive by predicting locally and reconciling with server corrections
  • Snapshot interpolation smooths remote player movement by rendering 100-200ms behind and lerping between snapshots
  • Lag compensation rewinds the world to the shooter's perspective time for fair hit detection
  • Never trust the client: validate all inputs on the server to prevent speed hacks, teleport hacks, and damage manipulation
  • UDP is preferred for real-time game data; TCP is used for reliable events like chat and matchmaking

Next in the Series

In Part 13: Tools & Editor Scripting, we'll build the tools that make you productive: custom inspectors, editor windows, scene visualization tools, automated build pipelines, CI/CD with GitHub Actions, and the Unity Test Framework.

Gaming