Easy Learn C#

Unity Networking

Introduction to Unity Networking

Networking is a crucial component of multiplayer games, allowing players to connect and interact in shared virtual environments. Unity provides robust networking solutions that enable developers to create networked games ranging from simple two-player experiences to massive multiplayer online games.

In this guide, you'll learn:

  • Unity's networking architecture and options
  • Setting up a basic multiplayer game
  • Synchronizing game state across clients
  • Managing networked objects and player instances
  • Implementing network messages and RPCs
  • Handling network events and failures
  • Optimizing network performance

Unity Networking Options

Unity offers several networking solutions, each with its own advantages and best use cases:

Available Networking Solutions:

  • Unity Netcode for GameObjects (Recommended): Unity's newest networking solution, designed for ease of use and scalability
  • Unity Transport Package: Low-level networking API that provides direct access to UDP socket communication
  • Mirror: A popular third-party networking solution, based on the deprecated UNET
  • Photon: A commercial third-party solution with free tier options, using a cloud infrastructure
  • Custom Solutions: Using low-level socket programming with C#

Choosing a networking solution:

  • Netcode for GameObjects: Best for most new Unity multiplayer projects
  • Photon: Easy setup without requiring your own server infrastructure
  • Mirror: Good for projects migrating from UNET
  • Custom solutions: For specialized requirements or maximum control

Networking Architectures

Common multiplayer architectures include:

  • Client-Server: A dedicated server manages the game state and clients connect to it (most common for games)
  • Peer-to-Peer: Players connect directly to each other without a central server
  • Host Migration: One client acts as both server and client, with ability to transfer host role
  • Authoritative Server: Server has final say on game state to prevent cheating

Getting Started with Netcode for GameObjects

Netcode for GameObjects (NGO) is Unity's latest networking solution. Let's explore how to set up a basic multiplayer game using NGO.

Setting Up the Project:

  1. Open the Package Manager (Window → Package Manager)
  2. Click the "+" button and select "Add package from git URL..."
  3. Enter "com.unity.netcode.gameobjects" and click "Add"
  4. Wait for the package to install

Basic Network Setup:

using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;

public class NetworkManagerUI : MonoBehaviour
{
    // UI Buttons for network actions
    public Button serverBtn;
    public Button hostBtn;
    public Button clientBtn;
    
    void Start()
    {
        // Add click listeners to the buttons
        serverBtn.onClick.AddListener(() => {
            // Start as a dedicated server
            NetworkManager.Singleton.StartServer();
            Debug.Log("Server started!");
        });
        
        hostBtn.onClick.AddListener(() => {
            // Start as host (server + client)
            NetworkManager.Singleton.StartHost();
            Debug.Log("Host started!");
        });
        
        clientBtn.onClick.AddListener(() => {
            // Connect as client to a host
            NetworkManager.Singleton.StartClient();
            Debug.Log("Client started, connecting to host...");
        });
    }
}

Key NGO concepts:

  • NetworkManager: Central component managing network state and connections
  • NetworkObject: Component that makes a GameObject networkable
  • NetworkBehaviour: Base class for scripts that need networking functionality
  • RPCs: Remote Procedure Calls to execute code on other clients
  • NetworkVariables: Variables automatically synchronized across the network

Creating Network Objects

To make a GameObject synchronize across the network, it needs a NetworkObject component and should contain NetworkBehaviour scripts.

Setting Up a Network Player:

using Unity.Netcode;
using UnityEngine;

// Inherit from NetworkBehaviour for networking functionality
public class NetworkPlayer : NetworkBehaviour
{
    // This variable will be synchronized automatically
    public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>();
    
    // Movement speed
    public float moveSpeed = 5f;
    
    // Called when the object is spawned on the network
    public override void OnNetworkSpawn()
    {
        // If we're the client that owns this player
        if (IsOwner)
        {
            Debug.Log("This is my player!");
        }
        else
        {
            Debug.Log("This is another player!");
        }
    }
    
    void Update()
    {
        // Only let the owner control this player
        if (IsOwner)
        {
            // Get input
            float horizontal = Input.GetAxis("Horizontal");
            float vertical = Input.GetAxis("Vertical");
            
            // Calculate movement
            Vector3 movement = new Vector3(horizontal, 0, vertical) * moveSpeed * Time.deltaTime;
            
            // Move the player
            transform.position += movement;
            
            // Update the network variable so other clients see the new position
            Position.Value = transform.position;
        }
        else
        {
            // For non-owners, smoothly update position based on network variable
            transform.position = Vector3.Lerp(transform.position, Position.Value, Time.deltaTime * 10f);
        }
    }
}

Important NetworkObject settings:

  • Spawn with Default Player Prefab: Automatically spawns this prefab for each player
  • Destroy with Scene: Whether to destroy the object during scene transitions
  • Network Object ID: Unique identifier for this networked object

Network Variables

NetworkVariables are special variables that automatically synchronize their values across the network from server to clients.

Working with Network Variables:

using Unity.Netcode;
using UnityEngine;

public class PlayerHealth : NetworkBehaviour
{
    // Health that syncs across the network
    // The WritePermission determines who can modify this value
    public NetworkVariable<int> Health = new NetworkVariable<int>(
        100, // Initial value
        NetworkVariableReadPermission.Everyone, // Who can read
        NetworkVariableWritePermission.Server // Who can write
    );
    
    // UI Text to display health
    private TMPro.TextMeshProUGUI healthText;
    
    // Callback when health changes
    private void OnHealthChanged(int oldValue, int newValue)
    {
        Debug.Log($"Health changed from {oldValue} to {newValue}");
        
        // Update UI
        if (healthText != null)
        {
            healthText.text = $"Health: {newValue}";
        }
        
        // Check for death
        if (newValue <= 0)
        {
            Die();
        }
    }
    
    public override void OnNetworkSpawn()
    {
        // Find health text UI
        healthText = GetComponentInChildren<TMPro.TextMeshProUGUI>();
        
        // Subscribe to value change event
        Health.OnValueChanged += OnHealthChanged;
        
        // Initialize the UI
        if (healthText != null)
        {
            healthText.text = $"Health: {Health.Value}";
        }
    }
    
    // When this object is despawned/destroyed
    public override void OnNetworkDespawn()
    {
        // Unsubscribe to prevent memory leaks
        Health.OnValueChanged -= OnHealthChanged;
    }
    
    // Server-side method to take damage
    [ServerRpc] // This can only be called by the client that owns this object
    public void TakeDamageServerRpc(int damage)
    {
        // Only the server can modify the NetworkVariable
        Health.Value -= damage;
    }
    
    // Client-side method to apply visual effects
    [ClientRpc] // This will be called on all clients
    public void ApplyDamageEffectClientRpc()
    {
        // Play damage effect visible to all clients
        // This is just a visual effect, not game logic
        Debug.Log("Playing damage effect");
        // Implement damage visual effects here
    }
    
    private void Die()
    {
        if (IsServer)
        {
            // Server-side death logic
            Debug.Log("Player died!");
            
            // Notify all clients to play death animation
            PlayDeathAnimationClientRpc();
            
            // Respawn or destroy after a delay
            Invoke("RespawnPlayer", 3.0f);
        }
    }
    
    [ClientRpc]
    private void PlayDeathAnimationClientRpc()
    {
        // Play death animation on all clients
        Debug.Log("Playing death animation");
        // Implement death animation here
    }
    
    private void RespawnPlayer()
    {
        if (IsServer)
        {
            // Reset health
            Health.Value = 100;
            
            // Teleport to spawn point
            transform.position = GetRandomSpawnPoint();
            
            // Notify clients about respawn
            OnRespawnClientRpc();
        }
    }
    
    [ClientRpc]
    private void OnRespawnClientRpc()
    {
        Debug.Log("Player respawned");
        // Implement respawn effects here
    }
    
    private Vector3 GetRandomSpawnPoint()
    {
        // Simple example - in a real game, you'd have designated spawn points
        return new Vector3(Random.Range(-10f, 10f), 0, Random.Range(-10f, 10f));
    }
}

NetworkVariable features:

  • Type Support: Primitives, structs, arrays, etc.
  • Permissions: Control who can read or write the variable
  • Change Events: Execute code when the value changes
  • Initialization: Set default values

Remote Procedure Calls (RPCs)

RPCs allow you to execute code on other instances of your game across the network.

Using RPCs:

using Unity.Netcode;
using UnityEngine;

public class WeaponController : NetworkBehaviour
{
    // References
    public ParticleSystem muzzleFlash;
    public AudioSource fireSound;
    public GameObject bulletPrefab;
    public Transform firePoint;
    
    // Weapon properties
    public float fireRate = 0.25f;
    private float nextFireTime = 0f;
    
    void Update()
    {
        // Only process input for the local player
        if (!IsOwner) return;
        
        // Check for fire input
        if (Input.GetButton("Fire1") && Time.time >= nextFireTime)
        {
            // Set next fire time
            nextFireTime = Time.time + fireRate;
            
            // Tell the server we want to fire
            FireWeaponServerRpc();
        }
    }
    
    // This runs on the server when a client calls it
    [ServerRpc]
    private void FireWeaponServerRpc()
    {
        // Server-side validation could be added here
        // For example, check if player has ammo, weapon is not broken, etc.
        
        // Spawn a bullet (server authoritative)
        GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
        
        // Get the network object component
        NetworkObject netObj = bullet.GetComponent<NetworkObject>();
        
        // Spawn the bullet on the network so all clients can see it
        netObj.Spawn();
        
        // Set bullet velocity
        Rigidbody rb = bullet.GetComponent<Rigidbody>();
        rb.velocity = firePoint.forward * 20f;
        
        // Tell all clients to play effects
        PlayWeaponEffectsClientRpc();
        
        // Destroy bullet after 5 seconds
        Destroy(bullet, 5f);
    }
    
    // This runs on all clients when the server calls it
    [ClientRpc]
    private void PlayWeaponEffectsClientRpc()
    {
        // Play visual and audio effects on all clients
        if (muzzleFlash != null)
        {
            muzzleFlash.Play();
        }
        
        if (fireSound != null)
        {
            fireSound.Play();
        }
    }
    
    // Send a message to a specific client (the one who got hit by the bullet)
    [ClientRpc]
    private void HitFeedbackClientRpc(ClientRpcParams clientRpcParams)
    {
        Debug.Log("You've been hit!");
        // Show hit marker or play hit sound
    }
    
    // Example of sending to specific client
    public void SendHitFeedback(ulong clientId)
    {
        // Create params targeting specific client
        ClientRpcParams clientRpcParams = new ClientRpcParams
        {
            Send = new ClientRpcSendParams
            {
                TargetClientIds = new ulong[] { clientId }
            }
        };
        
        // Send the RPC only to the specified client
        HitFeedbackClientRpc(clientRpcParams);
    }
}

Types of RPCs:

  • ServerRpc: Called by clients, executed on the server
  • ClientRpc: Called by the server, executed on clients

RPC attributes options:

  • RequireOwnership: Whether the caller must own the object
  • Delivery: Reliable (guaranteed) or Unreliable (faster but may drop)

Network Object Spawning

Objects need to be properly spawned on the network to be visible and synchronized across clients.

Spawning Network Objects:

using Unity.Netcode;
using UnityEngine;

public class ObjectSpawner : NetworkBehaviour
{
    // Prefab to spawn
    public GameObject prefabToSpawn;
    
    // Keep track of spawned objects
    private List<NetworkObject> spawnedObjects = new List<NetworkObject>();
    
    // This method is called when a button is pressed in the UI
    public void OnSpawnButtonClicked()
    {
        // Only the server can spawn objects
        if (IsServer || IsHost)
        {
            SpawnObject();
        }
        else if (IsClient)
        {
            // If we're a client, ask the server to spawn via RPC
            RequestSpawnServerRpc();
        }
    }
    
    // Spawn an object at a random position
    private void SpawnObject()
    {
        // Only the server should execute this
        if (!IsServer && !IsHost) return;
        
        // Random position within a certain range
        Vector3 spawnPos = new Vector3(
            Random.Range(-10f, 10f),
            0.5f,
            Random.Range(-10f, 10f)
        );
        
        // Instantiate the prefab
        GameObject obj = Instantiate(prefabToSpawn, spawnPos, Quaternion.identity);
        
        // Get the NetworkObject component
        NetworkObject netObj = obj.GetComponent<NetworkObject>();
        
        // Make sure it has a NetworkObject
        if (netObj == null)
        {
            Debug.LogError("Prefab is missing NetworkObject component!");
            Destroy(obj);
            return;
        }
        
        // Spawn it on the network so clients can see it
        netObj.Spawn();
        
        // Add to our list
        spawnedObjects.Add(netObj);
        
        Debug.Log($"Object spawned! Total: {spawnedObjects.Count}");
    }
    
    // Client requests the server to spawn an object
    [ServerRpc(RequireOwnership = false)] // Allow any client to call this
    private void RequestSpawnServerRpc(ServerRpcParams serverRpcParams = default)
    {
        // Get the client ID that requested the spawn
        ulong clientId = serverRpcParams.Receive.SenderClientId;
        Debug.Log($"Client {clientId} requested object spawn");
        
        // Optional: Validate if this client is allowed to spawn
        // E.g., check cooldown, permissions, etc.
        
        // Spawn the object
        SpawnObject();
    }
    
    // Despawn all objects (called from a UI button)
    public void OnDespawnAllButtonClicked()
    {
        if (IsServer || IsHost)
        {
            DespawnAllObjects();
        }
        else if (IsClient)
        {
            RequestDespawnAllServerRpc();
        }
    }
    
    private void DespawnAllObjects()
    {
        if (!IsServer && !IsHost) return;
        
        // Go through all spawned objects
        foreach (NetworkObject netObj in spawnedObjects)
        {
            if (netObj != null && netObj.IsSpawned)
            {
                // Despawn it from the network
                netObj.Despawn();
                // Note: Depending on settings, this might also destroy the GameObject
            }
        }
        
        // Clear our list
        spawnedObjects.Clear();
        Debug.Log("All objects despawned");
    }
    
    [ServerRpc(RequireOwnership = false)]
    private void RequestDespawnAllServerRpc()
    {
        DespawnAllObjects();
    }
    
    // Example of spawning with ownership assigned to a specific client
    public void SpawnForClient(ulong clientId)
    {
        if (!IsServer && !IsHost) return;
        
        Vector3 spawnPos = new Vector3(
            Random.Range(-10f, 10f),
            0.5f,
            Random.Range(-10f, 10f)
        );
        
        GameObject obj = Instantiate(prefabToSpawn, spawnPos, Quaternion.identity);
        NetworkObject netObj = obj.GetComponent<NetworkObject>();
        
        // Spawn with specific ownership
        netObj.SpawnWithOwnership(clientId);
        
        spawnedObjects.Add(netObj);
        Debug.Log($"Object spawned for client {clientId}");
    }
}

Spawning methods:

  • Spawn(): Spawn object with server ownership
  • SpawnWithOwnership(): Spawn with specified client ownership
  • SpawnAsPlayerObject(): Spawn as a player object for specific client

Spawn considerations:

  • Only the server can spawn network objects
  • Prefabs must be registered with the NetworkManager
  • Objects need a NetworkObject component

Network Messaging and Events

Unity Netcode provides various events and messaging systems to handle different network scenarios.

Managing Network Events:

using Unity.Netcode;
using UnityEngine;

public class NetworkEventsHandler : MonoBehaviour
{
    private NetworkManager netManager;
    
    void Start()
    {
        // Get reference to NetworkManager
        netManager = NetworkManager.Singleton;
        
        if (netManager == null)
        {
            Debug.LogError("NetworkManager not found in scene!");
            return;
        }
        
        // Subscribe to network events
        SubscribeToNetworkEvents();
    }
    
    void OnDestroy()
    {
        // Unsubscribe when destroyed to prevent memory leaks
        UnsubscribeFromNetworkEvents();
    }
    
    private void SubscribeToNetworkEvents()
    {
        // Connection events
        netManager.OnClientConnectedCallback += OnClientConnected;
        netManager.OnClientDisconnectCallback += OnClientDisconnected;
        
        // Transport events
        netManager.NetworkConfig.NetworkTransport.OnTransportEvent += OnTransportEvent;
        
        // Server/Host/Client start events
        netManager.OnServerStarted += OnServerStarted;
        netManager.OnHostStarted += OnHostStarted;
        netManager.OnClientStarted += OnClientStarted;
        
        // Server/Host/Client stop events
        netManager.OnServerStopped += OnServerStopped;
    }
    
    private void UnsubscribeFromNetworkEvents()
    {
        if (netManager == null) return;
        
        // Connection events
        netManager.OnClientConnectedCallback -= OnClientConnected;
        netManager.OnClientDisconnectCallback -= OnClientDisconnected;
        
        // Transport events
        netManager.NetworkConfig.NetworkTransport.OnTransportEvent -= OnTransportEvent;
        
        // Server/Host/Client start events
        netManager.OnServerStarted -= OnServerStarted;
        netManager.OnHostStarted -= OnHostStarted;
        netManager.OnClientStarted -= OnClientStarted;
        
        // Server/Host/Client stop events
        netManager.OnServerStopped -= OnServerStopped;
    }
    
    // Event Handlers
    
    private void OnClientConnected(ulong clientId)
    {
        Debug.Log($"Client connected with ID: {clientId}");
        
        // Example: Notify all clients about new player
        if (netManager.IsServer)
        {
            // Find player name (example - in real implementation, you would get this from that client)
            string playerName = $"Player_{clientId}";
            
            // Find a NetworkBehaviour to call the RPC from
            PlayerManager playerManager = FindObjectOfType<PlayerManager>();
            if (playerManager != null)
            {
                playerManager.AnnounceNewPlayerClientRpc(playerName);
            }
        }
    }
    
    private void OnClientDisconnected(ulong clientId)
    {
        Debug.Log($"Client disconnected with ID: {clientId}");
        
        // Example: Log player left message
        if (netManager.IsServer)
        {
            Debug.Log($"Player with ID {clientId} left the game");
            
            // Cleanup player resources, etc.
        }
    }
    
    private void OnTransportEvent(NetworkEvent eventType, ulong clientId, ArraySegment<byte> payload, float receiveTime)
    {
        switch (eventType)
        {
            case NetworkEvent.Data:
                Debug.Log($"Received data from client {clientId}");
                break;
                
            case NetworkEvent.Connect:
                Debug.Log($"Transport connect event from client {clientId}");
                break;
                
            case NetworkEvent.Disconnect:
                Debug.Log($"Transport disconnect event from client {clientId}");
                break;
        }
    }
    
    private void OnServerStarted()
    {
        Debug.Log("Server started successfully");
    }
    
    private void OnHostStarted()
    {
        Debug.Log("Host started successfully");
    }
    
    private void OnClientStarted()
    {
        Debug.Log("Client started connecting to server");
    }
    
    private void OnServerStopped(bool wasSuccessful)
    {
        Debug.Log($"Server stopped, was successful: {wasSuccessful}");
    }
}

// Example network behavior for announcements
public class PlayerManager : NetworkBehaviour
{
    [ClientRpc]
    public void AnnounceNewPlayerClientRpc(string playerName)
    {
        // Display in game UI
        Debug.Log($"New player joined: {playerName}");
        
        // Add to player list UI, etc.
    }
}

Important network events:

  • OnClientConnectedCallback: When a client connects to the server
  • OnClientDisconnectCallback: When a client disconnects
  • OnServerStarted/OnHostStarted/OnClientStarted: When respective network modes start
  • OnServerStopped: When the server stops
  • OnTransportEvent: Low-level transport events

Network Optimization

Efficient network usage is crucial for a smooth multiplayer experience. Here are some optimization techniques:

  1. Network Culling: Only send updates for objects relevant to each client
  2. Serialization Optimization: Minimize data size with appropriate serialization methods
  3. Update Rate Management: Adjust update frequency based on importance
  4. Bandwidth Control: Monitor and limit network usage
  5. Prediction and Interpolation: Smooth out network gameplay with client-side predictions

Implementing Client-Side Prediction and Reconciliation:

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

public class PredictiveMovement : NetworkBehaviour
{
    // Movement parameters
    public float moveSpeed = 5f;
    
    // For server reconciliation
    private Queue<PredictedInput> pendingInputs = new Queue<PredictedInput>();
    private int lastProcessedInputId = 0;
    
    // Struct to store input and prediction info
    struct PredictedInput
    {
        public int InputId;
        public Vector2 Movement;
        public Vector3 PredictedPosition;
    }
    
    // Network variable for server position
    public NetworkVariable<Vector3> ServerPosition = new NetworkVariable<Vector3>();
    
    public override void OnNetworkSpawn()
    {
        // Initialize server position to current transform position
        if (IsServer)
        {
            ServerPosition.Value = transform.position;
        }
    }
    
    void Update()
    {
        if (IsOwner)
        {
            // Process local player input
            ProcessLocalInput();
        }
        else
        {
            // For non-owners, smoothly interpolate to server position
            transform.position = Vector3.Lerp(transform.position, ServerPosition.Value, Time.deltaTime * 10f);
        }
    }
    
    void ProcessLocalInput()
    {
        // Get player input
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(horizontal, vertical).normalized;
        
        // Process the input locally first (client-side prediction)
        Vector3 newPosition = transform.position + new Vector3(movement.x, 0, movement.y) * moveSpeed * Time.deltaTime;
        transform.position = newPosition;
        
        // Keep track of the input
        int inputId = lastProcessedInputId + pendingInputs.Count + 1;
        
        // Store this input and predicted position for reconciliation
        PredictedInput input = new PredictedInput
        {
            InputId = inputId,
            Movement = movement,
            PredictedPosition = newPosition
        };
        
        pendingInputs.Enqueue(input);
        
        // Send to server for processing
        SendInputToServerServerRpc(inputId, movement);
    }
    
    [ServerRpc]
    void SendInputToServerServerRpc(int inputId, Vector2 movement)
    {
        // Server-side simulation of the input
        Vector3 newPosition = transform.position + new Vector3(movement.x, 0, movement.y) * moveSpeed * Time.deltaTime;
        
        // Update the position on the server
        transform.position = newPosition;
        ServerPosition.Value = newPosition;
        
        // Send back the processed input ID for reconciliation
        ProcessedInputClientRpc(inputId, newPosition);
    }
    
    [ClientRpc]
    void ProcessedInputClientRpc(int inputId, Vector3 serverPosition)
    {
        // Only the owner needs to reconcile
        if (!IsOwner) return;
        
        // Remember last processed input
        lastProcessedInputId = inputId;
        
        // Reconcile with server state
        ReconcileWithServer(inputId, serverPosition);
    }
    
    private void ReconcileWithServer(int inputId, Vector3 serverPosition)
    {
        // Remove inputs that have been processed by the server
        while (pendingInputs.Count > 0)
        {
            PredictedInput input = pendingInputs.Peek();
            
            if (input.InputId <= inputId)
            {
                pendingInputs.Dequeue();
            }
            else
            {
                break;
            }
        }
        
        // Check if there's a significant discrepancy between client prediction and server state
        if (Vector3.Distance(transform.position, serverPosition) > 0.1f)
        {
            // There's a mismatch, so we start from the server position and re-apply pending inputs
            transform.position = serverPosition;
            
            // Re-apply all pending inputs
            foreach (PredictedInput input in pendingInputs)
            {
                // Recompute movement with the input
                transform.position += new Vector3(input.Movement.x, 0, input.Movement.y) * moveSpeed * Time.deltaTime;
            }
        }
    }
}

Key optimization techniques:

  • Client-side prediction: Apply input locally first for responsiveness
  • Server reconciliation: Correct client prediction with authoritative server state
  • Input buffering: Store and replay inputs to handle network issues
  • Interpolation: Smooth movement between network updates

Best Practices for Unity Networking

  1. Design with networking in mind from the beginning
  2. Maintain server authority for important game state
  3. Minimize network traffic by only sending necessary data
  4. Use tick-based systems for consistent simulation
  5. Implement client-side prediction for responsive gameplay
  6. Plan for disconnections and reconnections
  7. Add proper validation to prevent cheating
  8. Use appropriate serialization for different data types
  9. Understand and handle latency in gameplay design
  10. Test with realistic network conditions including packet loss and high latency