From unity
Unity 6 multiplayer and networking guide. Use when building multiplayer games, working with Netcode for GameObjects, NetworkManager, NetworkObject, NetworkBehaviour, RPCs (ServerRpc, ClientRpc), NetworkVariables, or Unity multiplayer services (Relay, Lobby). Covers client-server architecture, state synchronization, and scene management. Based on Unity 6.3 LTS documentation.
npx claudepluginhub cdata/aria-skills --plugin unityThis skill uses the workspace's default tool permissions.
Unity's multiplayer ecosystem comprises several layers:
Guides Unity multiplayer networking using NGO, Mirror, Photon PUN/Fusion, Fish-Net; covers RPCs, NetworkVariables, synchronization, lobbies, UGS, Firebase, PlayFab.
Implements multiplayer basics in Godot 4.3+ using MultiplayerAPI, ENet/WebSocket peers, RPCs, and authority model for client-server architecture.
Multiplayer systems, netcode, game servers, synchronization, and anti-cheat. Build scalable, responsive multiplayer experiences.
Share bugs, ideas, or general feedback.
Unity's multiplayer ecosystem comprises several layers:
| Layer | Package/Service | Purpose |
|---|---|---|
| High-Level | Netcode for GameObjects | GameObject-based networking logic |
| High-Level | Netcode for Entities | DOTS-based networking |
| Low-Level | Unity Transport (com.unity.transport) | UDP/WebSocket communication with optional reliability, ordering, fragmentation |
| Services | Relay | NAT traversal via cloud relay servers |
| Services | Lobby | Matchmaking and session discovery |
| Services | Sessions SDK | Player group management |
| Tools | Multiplayer Play Mode | Simulate up to 4 players in-editor |
| Tools | Multiplayer Tools | Analysis, debugging, testing utilities |
Topology options:
Use the Multiplayer Center (Window > Multiplayer > Multiplayer Center) to get package recommendations based on your game's needs.
Install com.unity.netcode.gameobjects (v2.10+) via Package Manager. This automatically pulls in com.unity.transport.
Add a NetworkManager component to a GameObject in your scene. It is the singleton entry point for all networking.
using Unity.Netcode;
public class GameLauncher : MonoBehaviour
{
public void StartAsHost()
{
NetworkManager.Singleton.StartHost();
}
public void StartAsServer()
{
NetworkManager.Singleton.StartServer();
}
public void StartAsClient()
{
NetworkManager.Singleton.StartClient();
}
public void Shutdown()
{
NetworkManager.Singleton.Shutdown();
}
}
Key NetworkManager properties:
IsServer, IsClient, IsHost -- execution contextConnectedClients -- dictionary of connected clientsConnectedClientsIds -- read-only list of client IDsLocalClientId -- local client's IDSceneManager -- NetworkSceneManager instanceSpawnManager -- NetworkSpawnManager instanceNetworkConfig -- project network configurationKey NetworkManager events: OnClientConnectedCallback, OnClientDisconnectCallback, OnConnectionEvent, OnServerStarted/OnServerStopped, OnClientStarted/OnClientStopped, OnTransportFailure
Every networked GameObject needs a NetworkObject component. It provides identity, ownership, and visibility.
Key properties:
NetworkObjectId (ulong) -- unique ID synchronized across networkIsSpawned -- whether spawned on the networkOwnerClientId -- client ID of current ownerIsOwner -- true if local player owns this objectHasAuthority -- true if local instance has authoritySpawning (server-side only):
// Basic spawn
NetworkObject netObj = Instantiate(prefab).GetComponent<NetworkObject>();
netObj.Spawn();
// Spawn with specific owner
netObj.SpawnWithOwnership(clientId);
// Spawn as player object
netObj.SpawnAsPlayerObject(clientId);
// Despawn
netObj.Despawn(destroy: true);
Ownership (server-side only):
netObj.ChangeOwnership(newClientId);
netObj.RemoveOwnership();
Visibility:
netObj.NetworkShow(clientId);
netObj.NetworkHide(clientId);
bool visible = netObj.IsNetworkVisibleTo(clientId);
All networked scripts inherit from NetworkBehaviour instead of MonoBehaviour.
Lifecycle methods (in order):
OnNetworkPreSpawn(ref NetworkManager) -- before any spawningOnNetworkSpawn() -- after NetworkObject spawns; register handlers hereOnNetworkPostSpawn() -- after all sibling NetworkBehaviours spawnOnNetworkPreDespawn() -- before despawnOnNetworkDespawn() -- on despawnOwnership callbacks:
OnGainedOwnership() / OnLostOwnership()OnOwnershipChanged(ulong previous, ulong current) -- fires on all clientsusing Unity.Netcode;
public class PlayerController : NetworkBehaviour
{
public override void OnNetworkSpawn()
{
if (IsOwner)
{
// Initialize local player controls
EnableInput();
}
}
void Update()
{
if (!IsOwner) return;
// Only the owner processes input
HandleMovement();
}
public override void OnNetworkDespawn()
{
// Cleanup
}
}
Status checks: IsServer, IsClient, IsHost, IsOwner, IsSpawned, IsLocalPlayer, HasAuthority
NetworkVariable<T> synchronizes state from server to all clients automatically. Type T must be unmanaged (primitives, unmanaged structs).
public class PlayerHealth : NetworkBehaviour
{
public NetworkVariable<int> Health = new NetworkVariable<int>(
value: 100,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server
);
public override void OnNetworkSpawn()
{
Health.OnValueChanged += OnHealthChanged;
}
public override void OnNetworkDespawn()
{
Health.OnValueChanged -= OnHealthChanged;
}
private void OnHealthChanged(int oldValue, int newValue)
{
Debug.Log($"Health changed: {oldValue} -> {newValue}");
UpdateHealthUI(newValue);
}
// Server-side only (due to WritePerm.Server)
public void TakeDamage(int amount)
{
if (!IsServer) return;
Health.Value -= amount;
}
}
Write permissions:
NetworkVariableWritePermission.Server (default) -- only server can writeNetworkVariableWritePermission.Owner -- only owner can writeNetworkList -- synchronized list (T must be unmanaged + IEquatable<T>):
public class Inventory : NetworkBehaviour
{
public NetworkList<int> Items;
void Awake()
{
Items = new NetworkList<int>();
}
public override void OnNetworkSpawn()
{
Items.OnListChanged += OnItemsChanged;
}
private void OnItemsChanged(NetworkListEvent<int> changeEvent)
{
Debug.Log($"List changed: {changeEvent.Type}");
}
}
RPCs are remote procedure calls between server and clients. Methods must be in a NetworkBehaviour and use the [Rpc] attribute with a SendTo target.
public class CombatSystem : NetworkBehaviour
{
// Server executes this when any client calls it
[Rpc(SendTo.Server)]
void AttackRpc(int targetId, RpcParams rpcParams = default)
{
ulong senderId = rpcParams.Receive.SenderClientId;
ProcessAttack(senderId, targetId);
}
// All clients (and host) execute this
[Rpc(SendTo.ClientsAndHost)]
void ShowDamageEffectRpc(Vector3 position, int damage)
{
SpawnDamagePopup(position, damage);
}
// Only the owner executes this
[Rpc(SendTo.Owner)]
void NotifyOwnerRpc(string message)
{
Debug.Log(message);
}
// Everyone including sender
[Rpc(SendTo.Everyone)]
void PlaySoundRpc(int soundId)
{
AudioManager.Play(soundId);
}
}
| Target | Description |
|---|---|
Server | Executes on server; locally if called on server |
NotServer | All clients except server (excludes host) |
Owner | Object's owner only |
NotOwner | Everyone except owner |
Authority | Server in client-server; owner in distributed authority |
NotAuthority | All non-authority instances |
ClientsAndHost | All clients including host |
Everyone | All instances on the observer list |
Me | Local execution only |
NotMe | Everyone except sender |
SpecifiedInParams | Target set at runtime via RpcSendParams |
[ServerRpc]
void RequestSpawnServerRpc(ServerRpcParams rpcParams = default)
{
// Runs on server; only owner can call by default
}
[ClientRpc]
void UpdateUIClientRpc(int score)
{
// Runs on all clients
}
Use ConnectionApprovalCallback on NetworkManager to validate connecting clients.
// Server-side: register approval callback
NetworkManager.Singleton.ConnectionApprovalCallback = (request, response) =>
{
string password = System.Text.Encoding.UTF8.GetString(request.Payload);
response.Approved = (password == "secret");
response.CreatePlayerObject = response.Approved;
// Optionally set: response.PlayerPrefabHash, response.Position, response.Rotation
if (!response.Approved) response.Reason = "Invalid password";
response.Pending = false; // Signal decision is made
};
// Client-side: set payload before connecting
NetworkManager.Singleton.NetworkConfig.ConnectionData =
System.Text.Encoding.UTF8.GetBytes("secret");
NetworkManager.Singleton.StartClient();
NetworkSceneManager (accessed via NetworkManager.Singleton.SceneManager) handles synchronized scene loading.
public class GameSceneManager : NetworkBehaviour
{
public void LoadGameScene()
{
if (!IsServer) return;
// Server-only: loads scene on all clients
NetworkManager.Singleton.SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
}
public void LoadAdditiveScene()
{
if (!IsServer) return;
NetworkManager.Singleton.SceneManager.LoadScene("Arena", LoadSceneMode.Additive);
}
public override void OnNetworkSpawn()
{
NetworkManager.Singleton.SceneManager.OnSceneEvent += OnSceneEvent;
}
void OnSceneEvent(SceneEvent sceneEvent)
{
switch (sceneEvent.SceneEventType)
{
case SceneEventType.LoadComplete:
Debug.Log($"Client {sceneEvent.ClientId} loaded {sceneEvent.SceneName}");
break;
case SceneEventType.LoadEventCompleted:
Debug.Log($"All clients loaded {sceneEvent.SceneName}");
break;
case SceneEventType.SynchronizeComplete:
Debug.Log($"Client {sceneEvent.ClientId} fully synchronized");
break;
}
}
}
Scene event types: OnLoad, OnUnload, OnSynchronize, OnLoadComplete, OnUnloadComplete, OnLoadEventCompleted, OnUnloadEventCompleted, OnSynchronizeComplete
Important: Do NOT start new scene events within scene event callbacks.
Full integration examples in
references/transport-layer.md
Relay provides NAT punchthrough via cloud relay servers (no port forwarding needed). Requires UGS authentication.
Host flow: Create allocation, get join code, configure transport, start host. Client flow: Join allocation with code, configure transport, start client.
// Host: create relay and start
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxPlayers);
string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
transport.SetRelayServerData(allocation.ToRelayServerData("dtls")); // "dtls" = encrypted UDP
NetworkManager.Singleton.StartHost();
// Client: join relay and connect
JoinAllocation join = await RelayService.Instance.JoinAllocationAsync(joinCode);
transport.SetRelayServerData(join.ToRelayServerData("dtls"));
NetworkManager.Singleton.StartClient();
Lobby provides session discovery and matchmaking. Store the Relay join code in lobby data.
// Create lobby with relay join code
_lobby = await LobbyService.Instance.CreateLobbyAsync(name, maxPlayers, new CreateLobbyOptions {
Data = new Dictionary<string, DataObject> {
{ "JoinCode", new DataObject(DataObject.VisibilityOptions.Member, relayJoinCode) }
}
});
// Query available lobbies
QueryResponse response = await Lobbies.Instance.QueryLobbiesAsync(new QueryLobbiesOptions {
Filters = new List<QueryFilter> {
new QueryFilter(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT)
}
});
// Join and extract relay code
Lobby lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
string joinCode = lobby.Data["JoinCode"].Value;
// IMPORTANT: Send heartbeats every 15s or lobby expires
await LobbyService.Instance.SendHeartbeatPingAsync(lobby.Id);
// In a NetworkBehaviour on the server:
NetworkManager.Singleton.OnClientConnectedCallback += (ulong clientId) => {
GameObject player = Instantiate(playerPrefab);
player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
};
public class PlayerMovement : NetworkBehaviour
{
public NetworkVariable<Vector3> Position = new(writePerm: NetworkVariableWritePermission.Owner);
void Update()
{
if (!IsOwner) return;
// Note: Uses legacy Input for brevity. See unity-input for the new Input System.
Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
transform.position += move * Time.deltaTime * 5f;
Position.Value = transform.position;
}
}
public class ServerAuthMovement : NetworkBehaviour
{
[Rpc(SendTo.Server)]
void MoveRpc(Vector3 input) { transform.position += input * Time.deltaTime * 5f; }
void Update()
{
if (!IsOwner) return;
MoveRpc(new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))); // legacy Input; see unity-input
}
}
Writing to NetworkVariable without authority -- Only the server (or owner with WritePerm.Owner) can write. Client writes are silently ignored.
Forgetting IsOwner checks in Update -- Without owner checks, all clients run input logic, causing conflicting state.
Using Instantiate without Spawn -- Objects created with Instantiate alone are local-only. Always call Spawn() on the NetworkObject for network visibility.
Spawning from client code -- Only the server can spawn NetworkObjects. Clients must send an RPC to request spawning.
Heavy data in RPCs instead of NetworkVariables -- RPCs are fire-and-forget; late joiners miss them. Use NetworkVariables for persistent state.
Not unsubscribing from OnValueChanged -- Subscribe in OnNetworkSpawn, unsubscribe in OnNetworkDespawn to prevent leaks.
Starting scene events inside scene event callbacks -- NetworkSceneManager forbids this; causes undefined behavior.
Not sending Lobby heartbeats -- Lobbies expire without periodic SendHeartbeatPingAsync calls (every 15-30 seconds).
Using NetworkVariable for frequent small updates -- For high-frequency data (position), prefer NetworkTransform or custom serialization.
Calling RPCs before OnNetworkSpawn -- RPCs require the NetworkObject to be spawned. Defer to OnNetworkSpawn.
| Class | Key Members |
|---|---|
NetworkManager | StartHost(), StartServer(), StartClient(), Shutdown(), Singleton, ConnectedClients, LocalClientId, SceneManager, ConnectionApprovalCallback |
NetworkObject | Spawn(), SpawnWithOwnership(), SpawnAsPlayerObject(), Despawn(), ChangeOwnership(), NetworkShow(), NetworkHide(), NetworkObjectId, OwnerClientId, IsOwner, HasAuthority |
NetworkBehaviour | OnNetworkSpawn(), OnNetworkDespawn(), OnGainedOwnership(), OnLostOwnership(), IsServer, IsClient, IsHost, IsOwner, IsSpawned, HasAuthority, RpcTarget |
NetworkVariable<T> | .Value, OnValueChanged, ReadPerm, WritePerm, CheckDirtyState() |
NetworkList<T> | Add(), Remove(), Insert(), Clear(), Count, OnListChanged |
NetworkSceneManager | LoadScene(), UnloadScene(), OnSceneEvent |
[Rpc(SendTo.X)] | Server, Owner, NotOwner, ClientsAndHost, Everyone, NotMe, Authority, SpecifiedInParams |
NetworkRigidbody for synced physics