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:
- Open the Package Manager (Window → Package Manager)
- Click the "+" button and select "Add package from git URL..."
- Enter "com.unity.netcode.gameobjects" and click "Add"
- 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:
- Network Culling: Only send updates for objects relevant to each client
- Serialization Optimization: Minimize data size with appropriate serialization methods
- Update Rate Management: Adjust update frequency based on importance
- Bandwidth Control: Monitor and limit network usage
- 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
- Design with networking in mind from the beginning
- Maintain server authority for important game state
- Minimize network traffic by only sending necessary data
- Use tick-based systems for consistent simulation
- Implement client-side prediction for responsive gameplay
- Plan for disconnections and reconnections
- Add proper validation to prevent cheating
- Use appropriate serialization for different data types
- Understand and handle latency in gameplay design
- Test with realistic network conditions including packet loss and high latency