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.
📋 Table of Contents
🏗️ Multiplayer Architectures
Client-Server vs Peer-to-Peer
Hybrid Architecture (Recommended)
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
Experiment with each solution using small projects
Measure performance of your game under different network conditions
Implement metrics to monitor player experience
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