Lesson 3: Player Spawn and Connection Lifecycle (Approval, Disconnect, and Cleanup)
Your multiplayer bootstrap runs. Great. Now the next risk appears fast: players join in weird states, double-spawn after reconnects, or leave stale objects behind when sessions end.
This lesson gives you a practical lifecycle flow that handles:
- Connection approval
- Spawn timing and ownership
- Disconnect teardown
- Clean rejoin behavior during repeated tests
Lesson objective
By the end of this lesson you will have:
- One deterministic join path from request to spawned player
- Approval checks that reject invalid clients early
- A server-owned cleanup path on disconnect
- A repeatable lifecycle smoke test for host/client restart loops
Why this matters
Most early multiplayer bugs are lifecycle bugs, not gameplay bugs. If join, spawn, and disconnect are inconsistent, every later system gets harder: inventory sync, match flow, checkpoints, and anti-cheat validation.
Fixing lifecycle now means later lessons stay predictable.
Lifecycle model for this vertical slice
Use this order as your source of truth:
- Client requests connection
- Server validates and approves (or rejects)
- Server assigns spawn point and creates player object
- Session runs with authoritative ownership rules
- Disconnect callback triggers server cleanup
- Rejoin starts from fresh state, never stale leftovers
Keep this flow documented in your team notes so debugging starts from the same mental model.
Step-by-step implementation
Step 1 - Enable and scope connection approval
In NetworkManager, enable connection approval and implement a minimal gate.
Use approval for:
- Version checks
- Session capacity checks
- Optional lightweight auth token checks for later relay flow
Do not overbuild this yet. A thin rule set is enough for the slice.
Step 2 - Register lifecycle callbacks once
Create one lifecycle handler script and wire it at startup:
OnClientConnectedCallbackOnClientDisconnectCallbackConnectionApprovalCallback
Avoid scattered callback subscriptions in many scripts. One orchestrator prevents duplicate handling and missing cleanup.
Step 3 - Control spawn ownership and spawn-point selection
When approval passes:
- Pick a spawn point from a deterministic strategy (round-robin or first-free)
- Spawn the player prefab with the joining
clientId - Register server-side references in a dictionary keyed by
clientId
Your spawn logic should be server-authoritative. Clients can request join, but server decides spawn and ownership.
Step 4 - Implement disconnect cleanup on server
In disconnect callback, server should:
- Despawn owned player object
- Remove
clientIdentries from registries - Clear temporary match/session state tied to that client
Cleanup must run even when disconnect is abrupt. Assume clients can drop unexpectedly at any time.
Step 5 - Validate reconnect behavior
Run a quick reconnect loop:
- Host + client connect
- Client disconnects
- Same client reconnects
- Confirm only one active player object exists for that client
If reconnect creates duplicates, your cleanup path is incomplete.
Reference lifecycle script
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public class NetworkLifecycleCoordinator : MonoBehaviour
{
[SerializeField] private NetworkObject playerPrefab;
[SerializeField] private Transform[] spawnPoints;
private readonly Dictionary<ulong, NetworkObject> playersByClient = new();
private int nextSpawnIndex = 0;
private void Start()
{
var nm = NetworkManager.Singleton;
nm.ConnectionApprovalCallback += ApproveConnection;
nm.OnClientConnectedCallback += OnClientConnected;
nm.OnClientDisconnectCallback += OnClientDisconnected;
}
private void ApproveConnection(NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
bool roomAvailable = NetworkManager.Singleton.ConnectedClients.Count < 8;
response.Approved = roomAvailable;
response.CreatePlayerObject = false;
response.Reason = roomAvailable ? string.Empty : "Session full";
}
private void OnClientConnected(ulong clientId)
{
if (!NetworkManager.Singleton.IsServer) return;
if (playersByClient.ContainsKey(clientId)) return;
Transform spawn = spawnPoints[nextSpawnIndex % spawnPoints.Length];
nextSpawnIndex++;
NetworkObject player = Instantiate(playerPrefab, spawn.position, spawn.rotation);
player.SpawnAsPlayerObject(clientId, true);
playersByClient[clientId] = player;
}
private void OnClientDisconnected(ulong clientId)
{
if (!NetworkManager.Singleton.IsServer) return;
if (!playersByClient.TryGetValue(clientId, out NetworkObject player)) return;
if (player != null && player.IsSpawned)
player.Despawn(true);
playersByClient.Remove(clientId);
}
}
Mini challenge
Add a connection log panel or console log that prints:
- Client approved/rejected
- Spawn index used
- Disconnect reason (if available)
- Cleanup completion state
Then run five connect/disconnect loops and verify zero duplicate player objects remain.
Pro tips
- Keep lifecycle orchestration server-side first; expose only minimal client hooks.
- Treat spawn-point assignment as deterministic data, not random each frame.
- Add a simple "session full" rejection reason so QA can distinguish gate behavior from transport failures.
Common mistakes
- Spawning player object in both approval callback and connect callback
- Forgetting to remove dictionaries/lists on disconnect
- Allowing client-side scripts to authoritatively create networked player objects
Troubleshooting
"Client connects but no player appears."
Confirm response.CreatePlayerObject = false is paired with manual spawn logic in OnClientConnected.
"Disconnect leaves ghost players."
Verify server executes cleanup path and Despawn(true) runs before registry removal.
"Second reconnect creates two player objects."
Check for duplicate callback subscriptions or stale playersByClient entries.
Recap
You now have a stable player connection lifecycle: approval gate, deterministic spawn ownership, and disconnect cleanup that supports rejoin testing without leaked state.
Next lesson teaser
Lesson 4 builds the networked movement baseline so movement feels responsive while staying authoritative under host-client conditions.
Related links
- Lesson 2: Netcode Package Choice and Project Setup
- Unity Multiplayer Guide Chapter
- Official docs: Connection approval in NGO, NetworkManager callbacks
Bookmark this lesson and run the reconnect smoke loop any time you change spawn, join flow, or session teardown logic.