Page cover

Unity Multiplayer Technologies

This comprehensive guide covers the main strategies, tools, and best practices for developing multiplayer games in Unity, including solutions like AWS GameLift, Photon PUN, and advanced techniques to optimize the online experience.

Unity Multiplayer Overview

📋 Table of Contents


🏗️ Multiplayer Architectures

Client-Server vs Peer-to-Peer

Advantages of Hybrid Architecture:

  • ✅ Better security against cheats

  • ✅ Horizontal scalability

  • ✅ State consistency

  • ✅ Lower latency than pure P2P


🎮 Unity Netcode for GameObjects

Basic Configuration

using Unity.Netcode;

public class PlayerController : NetworkBehaviour
{
    private NetworkVariable<float> networkPositionX = new NetworkVariable<float>();
    
    public override void OnNetworkSpawn()
    {
        if (IsOwner)
        {
            // Only the owner can move
            enabled = true;
        }
    }
    
    void Update()
    {
        if (IsOwner)
        {
            float horizontal = Input.GetAxis("Horizontal");
            transform.Translate(horizontal * Time.deltaTime * 5f, 0, 0);
            
            // Synchronize position
            UpdatePositionServerRpc(transform.position.x);
        }
    }
    
    [ServerRpc]
    void UpdatePositionServerRpc(float newX)
    {
        networkPositionX.Value = newX;
    }
}

Netcode Architecture


⚡ Photon PUN

Initial Configuration

using Photon.Pun;
using Photon.Realtime;

public class NetworkManager : MonoBehaviourPunPV, IConnectionCallbacks
{
    void Start()
    {
        // Connect to Photon
        PhotonNetwork.ConnectUsingSettings();
        PhotonNetwork.GameVersion = "1.0";
    }
    
    public override void OnConnectedToMaster()
    {
        Debug.Log("Connected to master server");
        PhotonNetwork.JoinLobby();
    }
    
    public override void OnJoinedLobby()
    {
        Debug.Log("Joined lobby");
        // Create or join room
        RoomOptions roomOptions = new RoomOptions();
        roomOptions.MaxPlayers = 4;
        PhotonNetwork.JoinOrCreateRoom("GameRoom", roomOptions, TypedLobby.Default);
    }
}

Object Synchronization

public class PlayerMovement : MonoBehaviourPunPV, IPunObservable
{
    private Vector3 networkPosition;
    private Quaternion networkRotation;
    
    void Update()
    {
        if (photonView.IsMine)
        {
            // Local movement
            HandleMovement();
        }
        else
        {
            // Smooth interpolation for other players
            transform.position = Vector3.Lerp(transform.position, networkPosition, Time.deltaTime * 10f);
            transform.rotation = Quaternion.Lerp(transform.rotation, networkRotation, Time.deltaTime * 10f);
        }
    }
    
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            // Send our position
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            // Receive position from others
            networkPosition = (Vector3)stream.ReceiveNext();
            networkRotation = (Quaternion)stream.ReceiveNext();
        }
    }
}

Photon Architecture


☁️ AWS GameLift

Server Configuration

using Amazon.GameLift.Server;
using Amazon.GameLift.Server.Model;

public class GameLiftServer : MonoBehaviour
{
    void Start()
    {
        // Initialize GameLift
        var initOutcome = GameLiftServerAPI.InitSDK();
        if (initOutcome.Success)
        {
            var processParams = new ProcessParameters(
                onStartGameSession: OnStartGameSession,
                onUpdateGameSession: OnUpdateGameSession,
                onProcessTerminate: OnProcessTerminate,
                onHealthCheck: OnHealthCheck,
                port: 7777,
                logParameters: new LogParameters(new List<string>()
                {
                    "/local/game/logs/myserver.log"
                })
            );

            var processReadyOutcome = GameLiftServerAPI.ProcessReady(processParams);
        }
    }

    void OnStartGameSession(GameSession gameSession)
    {
        // Activate game session
        GameLiftServerAPI.ActivateGameSession();
    }

    bool OnHealthCheck()
    {
        return true; // Healthy server
    }

    void OnProcessTerminate()
    {
        GameLiftServerAPI.ProcessEnding();
    }
}

GameLift Client

using Amazon.GameLift;
using Amazon.GameLift.Model;

public class GameLiftClient : MonoBehaviour
{
    private AmazonGameLiftClient gameLiftClient;
    
    void Start()
    {
        // Configure client
        var config = new AmazonGameLiftConfig();
        config.RegionEndpoint = Amazon.RegionEndpoint.USWest2;
        gameLiftClient = new AmazonGameLiftClient(config);
    }
    
    public async void SearchGameSession()
    {
        var request = new SearchGameSessionsRequest();
        request.FleetId = "fleet-12345";
        request.FilterExpression = "hasAvailablePlayerSessions=true";
        
        try
        {
            var response = await gameLiftClient.SearchGameSessionsAsync(request);
            if (response.GameSessions.Count > 0)
            {
                JoinGameSession(response.GameSessions[0]);
            }
            else
            {
                CreateGameSession();
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Error searching for session: {e.Message}");
        }
    }
}

GameLift Architecture


🔗 Mirror Networking

Basic Configuration

using Mirror;

public class PlayerController : NetworkBehaviour
{
    [SyncVar]
    public string playerName = "Player";
    
    [SyncVar(hook = nameof(OnHealthChanged))]
    public int health = 100;
    
    void OnHealthChanged(int oldHealth, int newHealth)
    {
        Debug.Log($"Health changed from {oldHealth} to {newHealth}");
    }
    
    [Command]
    void CmdTakeDamage(int damage)
    {
        if (isServer)
        {
            health -= damage;
        }
    }
    
    [ClientRpc]
    void RpcShowDamageEffect()
    {
        // Show damage effect on all clients
        Debug.Log("Damage received!");
    }
}

🚀 Anti-Lag Tips

1. Interpolation and Extrapolation

public class SmoothNetworkTransform : NetworkBehaviour
{
    private Vector3 targetPosition;
    private float lerpRate = 15f;
    
    void Update()
    {
        if (!isLocalPlayer)
        {
            // Smooth interpolation
            transform.position = Vector3.Lerp(transform.position, targetPosition, lerpRate * Time.deltaTime);
        }
    }
    
    [ClientRpc]
    void RpcUpdatePosition(Vector3 newPosition, float timestamp)
    {
        // Latency compensation
        float timeDiff = Time.time - timestamp;
        targetPosition = newPosition + (GetComponent<Rigidbody>().velocity * timeDiff);
    }
}

2. Data Compression

public struct CompressedVector3
{
    public short x, y, z;
    
    public static implicit operator Vector3(CompressedVector3 compressed)
    {
        return new Vector3(
            compressed.x / 100f,
            compressed.y / 100f,
            compressed.z / 100f
        );
    }
    
    public static implicit operator CompressedVector3(Vector3 vector)
    {
        return new CompressedVector3
        {
            x = (short)(vector.x * 100f),
            y = (short)(vector.y * 100f),
            z = (short)(vector.z * 100f)
        };
    }
}

3. Delta Compression

public class DeltaSync : NetworkBehaviour
{
    private Vector3 lastSentPosition;
    private float sendThreshold = 0.1f;
    
    void Update()
    {
        if (isLocalPlayer)
        {
            float distance = Vector3.Distance(transform.position, lastSentPosition);
            if (distance > sendThreshold)
            {
                CmdUpdatePosition(transform.position);
                lastSentPosition = transform.position;
            }
        }
    }
}

4. Optimization Architecture

flowchart TD
    subgraph "Client Side Optimization"
        CP[Client Prediction] --> LI[Local Input]
        LI --> IR[Immediate Response]
        IR --> SC[Server Confirmation]
    end
    
    subgraph "Network Optimization"
        DC[Delta Compression] --> PQ[Priority Queue]
        PQ --> RU[Reliable/Unreliable]
        RU --> BC[Batch Commands]
    end
    
    subgraph "Server Optimization"
        SV[Server Validation] --> CL[Culling Distance]
        CL --> LOD[Level of Detail]
        LOD --> UP[Update Frequency]
    end
    
    CP --> DC
    BC --> SV

❌ Common Errors

1. Synchronizing Everything

// ❌ BAD: Constantly synchronizing position
void Update()
{
    if (isLocalPlayer)
    {
        CmdUpdatePosition(transform.position); // Sent 60 times per second!
    }
}

// ✅ GOOD: Only synchronize significant changes
void Update()
{
    if (isLocalPlayer && Vector3.Distance(transform.position, lastSentPosition) > 0.1f)
    {
        CmdUpdatePosition(transform.position);
        lastSentPosition = transform.position;
    }
}

2. Not Validating on Server

// ❌ BAD: Blindly trusting the client
[Command]
void CmdTakeDamage(int damage)
{
    health -= damage; // The client can send any value!
}

// ✅ GOOD: Validate on server
[Command]
void CmdTakeDamage(int damage)
{
    if (damage > 0 && damage <= maxDamagePerHit)
    {
        health -= damage;
    }
}

3. Blocking the Main Thread

// ❌ BAD: Synchronous operation that blocks
void ConnectToServer()
{
    NetworkManager.singleton.StartClient(); // Blocks until connected
    // UI freezes...
}

// ✅ GOOD: Use asynchronous callbacks
void ConnectToServer()
{
    NetworkManager.singleton.StartClient();
    StartCoroutine(WaitForConnection());
}

IEnumerator WaitForConnection()
{
    while (!NetworkManager.singleton.IsClientConnected())
    {
        yield return new WaitForSeconds(0.1f);
    }
    OnConnectedToServer();
}

4. Ignoring Latency

// ❌ BAD: Not compensating for latency
[ClientRpc]
void RpcFireProjectile(Vector3 startPos, Vector3 direction)
{
    // The projectile appears where the player was 100ms ago
    Instantiate(projectilePrefab, startPos, Quaternion.LookRotation(direction));
}

// ✅ GOOD: Compensate for latency
[ClientRpc]
void RpcFireProjectile(Vector3 startPos, Vector3 direction, float timestamp)
{
    float lagCompensation = Time.time - timestamp;
    Vector3 compensatedPos = startPos + direction * projectileSpeed * lagCompensation;
    Instantiate(projectilePrefab, compensatedPos, Quaternion.LookRotation(direction));
}

🎯 Best Practices

1. Robust Network Architecture

2. State System

(Please don't just use enum as a state machine, this is a simple example)

public class NetworkGameManager : NetworkBehaviour
{
    public enum GameState
    {
        Waiting,
        Starting,
        Playing,
        Ending
    }
    
    [SyncVar(hook = nameof(OnGameStateChanged))]
    public GameState currentState = GameState.Waiting;
    
    void OnGameStateChanged(GameState oldState, GameState newState)
    {
        switch (newState)
        {
            case GameState.Starting:
                StartCoroutine(StartCountdown());
                break;
            case GameState.Playing:
                EnablePlayerControls();
                break;
            case GameState.Ending:
                ShowResults();
                break;
        }
    }
}

3. Bandwidth Optimization

public class EfficientNetworkSync : NetworkBehaviour
{
    // Use events for important changes
    [SyncEvent]
    public event System.Action<int> OnScoreChanged;
    
    // SyncVar only for critical data
    [SyncVar]
    public int playerScore;
    
    // Custom serialization for complex data
    public override bool OnSerialize(NetworkWriter writer, bool initialState)
    {
        if (initialState)
        {
            writer.WriteInt32(playerScore);
            return true;
        }
        
        bool dataChanged = false;
        
        if (scoreChanged)
        {
            writer.WriteInt32(playerScore);
            dataChanged = true;
            scoreChanged = false;
        }
        
        return dataChanged;
    }
}

4. Testing and Debugging

public class NetworkDebugger : NetworkBehaviour
{
    [Header("Debug Info")]
    public float ping;
    public int packetsReceived;
    public int packetsLost;
    
    void Update()
    {
        if (isLocalPlayer)
        {
            ping = NetworkTime.rtt * 1000f; // Convert to milliseconds
            UpdateDebugUI();
        }
    }
    
    void UpdateDebugUI()
    {
        debugText.text = $"Ping: {ping:F1}ms\nPackets: {packetsReceived}\nLost: {packetsLost}";
    }
    
    #if UNITY_EDITOR
    void OnGUI()
    {
        if (isLocalPlayer)
        {
            GUILayout.Label($"Network Stats:");
            GUILayout.Label($"RTT: {NetworkTime.rtt * 1000f:F1}ms");
            GUILayout.Label($"Bandwidth: {GetComponent<NetworkIdentity>().connectionToServer.Send}");
        }
    }
    #endif
}

🔧 Development Tools

Network Profiler

public class NetworkProfiler : MonoBehaviour
{
    private float bytesPerSecond;
    private float messagesPerSecond;
    
    void Update()
    {
        if (NetworkManager.singleton != null)
        {
            var stats = NetworkManager.singleton.GetNetworkStatistics();
            bytesPerSecond = stats.BytesReceived + stats.BytesSent;
            messagesPerSecond = stats.MessagesReceived + stats.MessagesSent;
        }
    }
}

Latency Simulator

public class LatencySimulator : NetworkBehaviour
{
    [Range(0, 500)]
    public int artificialLatency = 0;
    
    private Queue<DelayedMessage> messageQueue = new Queue<DelayedMessage>();
    
    struct DelayedMessage
    {
        public float sendTime;
        public System.Action action;
    }
    
    void Update()
    {
        while (messageQueue.Count > 0 && Time.time >= messageQueue.Peek().sendTime)
        {
            messageQueue.Dequeue().action.Invoke();
        }
    }
    
    void SendWithLatency(System.Action action)
    {
        messageQueue.Enqueue(new DelayedMessage
        {
            sendTime = Time.time + (artificialLatency / 1000f),
            action = action
        });
    }
}

🎮 Complete Example: Simple Multiplayer Game

using Unity.Netcode;
using UnityEngine;

public class MultiplayerTank : NetworkBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float rotateSpeed = 90f;
    [SerializeField] private Transform firePoint;
    [SerializeField] private GameObject bulletPrefab;
    
    private NetworkVariable<Vector3> networkPosition = new NetworkVariable<Vector3>();
    private NetworkVariable<Quaternion> networkRotation = new NetworkVariable<Quaternion>();
    
    public override void OnNetworkSpawn()
    {
        if (IsOwner)
        {
            // Configure camera for local player
            Camera.main.GetComponent<CameraFollow>().SetTarget(transform);
        }
        else
        {
            // Subscribe to network changes for other players
            networkPosition.OnValueChanged += OnPositionChanged;
            networkRotation.OnValueChanged += OnRotationChanged;
        }
    }
    
    void Update()
    {
        if (IsOwner)
        {
            HandleInput();
        }
        else
        {
            InterpolateMovement();
        }
    }
    
    void HandleInput()
    {
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");
        
        // Movement
        Vector3 movement = transform.forward * vertical * moveSpeed * Time.deltaTime;
        transform.position += movement;
        
        // Rotation
        float rotation = horizontal * rotateSpeed * Time.deltaTime;
        transform.Rotate(0, rotation, 0);
        
        // Fire
        if (Input.GetKeyDown(KeyCode.Space))
        {
            FireBulletServerRpc();
        }
        
        // Update network position
        UpdateTransformServerRpc(transform.position, transform.rotation);
    }
    
    void InterpolateMovement()
    {
        // Smooth interpolation for other players
        transform.position = Vector3.Lerp(transform.position, networkPosition.Value, Time.deltaTime * 10f);
        transform.rotation = Quaternion.Lerp(transform.rotation, networkRotation.Value, Time.deltaTime * 10f);
    }
    
    [ServerRpc]
    void UpdateTransformServerRpc(Vector3 position, Quaternion rotation)
    {
        networkPosition.Value = position;
        networkRotation.Value = rotation;
    }
    
    [ServerRpc]
    void FireBulletServerRpc()
    {
        // Create bullet on server
        GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
        bullet.GetComponent<NetworkObject>().Spawn();
        
        // Notify all clients
        FireBulletClientRpc();
    }
    
    [ClientRpc]
    void FireBulletClientRpc()
    {
        // Visual/audio firing effects
        AudioSource.PlayClipAtPoint(fireSound, firePoint.position);
        Instantiate(muzzleFlash, firePoint.position, firePoint.rotation);
    }
    
    void OnPositionChanged(Vector3 oldPos, Vector3 newPos)
    {
        // Additional logic when position changes
    }
    
    void OnRotationChanged(Quaternion oldRot, Quaternion newRot)
    {
        // Additional logic when rotation changes
    }
}

📚 Additional Resources

Official Documentation

Testing Tools

  • Network Emulation: Unity Network Test Tool

  • Load Testing: Unity Cloud Build + Custom Scripts

  • Analytics: Unity Analytics + Custom Metrics

Communities

  • Unity Multiplayer Discord

  • Photon Community Forums

  • AWS GameTech Community


🚀 Next Steps

  1. Experiment with each solution using small projects

  2. Measure performance of your game under different network conditions

  3. Implement metrics to monitor player experience

  4. Iterate based on real user feedback


Multiplayer development is a complex but rewarding field. The key is to start simple, iterate quickly, and always prioritize the player experience.

Last updated