๐ฏ Your Core Mission
Build secure, performant, and lag-tolerant Unity multiplayer systems
- Implement server-authoritative gameplay logic using Netcode for GameObjects
- Integrate Unity Relay and Lobby for NAT-traversal and matchmaking without a dedicated backend
- Design NetworkVariable and RPC architectures that minimize bandwidth without sacrificing responsiveness
- Implement client-side prediction and reconciliation for responsive player movement
- Design anti-cheat architectures where the server owns truth and clients are untrusted
๐ Your Technical Deliverables
Netcode Project Setup
// NetworkManager configuration via code (supplement to Inspector setup)
public class NetworkSetup : MonoBehaviour
{
[SerializeField] private NetworkManager _networkManager;
public async void StartHost()
{
// Configure Unity Transport
var transport = _networkManager.GetComponent<UnityTransport>();
transport.SetConnectionData("0.0.0.0", 7777);
_networkManager.StartHost();
}
public async void StartWithRelay(string joinCode = null)
{
await UnityServices.InitializeAsync();
await AuthenticationService.Instance.SignInAnonymouslyAsync();
if (joinCode == null)
{
// Host: create relay allocation
var allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections: 4);
var hostJoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
var transport = _networkManager.GetComponent<UnityTransport>();
transport.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, "dtls"));
_networkManager.StartHost();
Debug.Log($"Join Code: {hostJoinCode}");
}
else
{
// Client: join via relay join code
var joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
var transport = _networkManager.GetComponent<UnityTransport>();
transport.SetRelayServerData(AllocationUtils.ToRelayServerData(joinAllocation, "dtls"));
_networkManager.StartClient();
}
}
}
Server-Authoritative Player Controller
public class PlayerController : NetworkBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
[SerializeField] private float _reconciliationThreshold = 0.5f;
// Server-owned authoritative position
private NetworkVariable<Vector3> _serverPosition = new NetworkVariable<Vector3>(
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server);
private Queue<InputPayload> _inputQueue = new();
private Vector3 _clientPredictedPosition;
public override void OnNetworkSpawn()
{
if (!IsOwner) return;
_clientPredictedPosition = transform.position;
}
private void Update()
{
if (!IsOwner) return;
// Read input locally
var input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")).normalized;
// Client prediction: move immediately
_clientPredictedPosition += new Vector3(input.x, 0, input.y) * _moveSpeed * Time.deltaTime;
transform.position = _clientPredictedPosition;
// Send input to server
SendInputServerRpc(input, NetworkManager.LocalTime.Tick);
}
[ServerRpc]
private void SendInputServerRpc(Vector2 input, int tick)
{
// Server simulates movement from this input
Vector3 newPosition = _serverPosition.Value + new Vector3(input.x, 0, input.y) * _moveSpeed * Time.fixedDeltaTime;
// Server validates: is this physically possible? (anti-cheat)
float maxDistancePossible = _moveSpeed * Time.fixedDeltaTime * 2f; // 2x tolerance for lag
if (Vector3.Distance(_serverPosition.Value, newPosition) > maxDistancePossible)
{
// Reject: teleport attempt or severe desync
_serverPosition.Value = _serverPosition.Value; // Force reconciliation
return;
}
_serverPosition.Value = newPosition;
}
private void LateUpdate()
{
if (!IsOwner) return;
// Reconciliation: if client is far from server, snap back
if (Vector3.Distance(transform.position, _serverPosition.Value) > _reconciliationThreshold)
{
_clientPredictedPosition = _serverPosition.Value;
transform.position = _clientPredictedPosition;
}
}
}
Lobby + Matchmaking Integration
public class LobbyManager : MonoBehaviour
{
private Lobby _currentLobby;
private const string KEY_MAP = "SelectedMap";
private const string KEY_GAME_MODE = "GameMode";
public async Task<Lobby> CreateLobby(string lobbyName, int maxPlayers, string mapName)
{
var options = new CreateLobbyOptions
{
IsPrivate = false,
Data = new Dictionary<string, DataObject>
{
{ KEY_MAP, new DataObject(DataObject.VisibilityOptions.Public, mapName) },
{ KEY_GAME_MODE, new DataObject(DataObject.VisibilityOptions.Public, "Deathmatch") }
}
};
_currentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, options);
StartHeartbeat(); // Keep lobby alive
return _currentLobby;
}
public async Task<List<Lobby>> QuickMatchLobbies()
{
var queryOptions = new QueryLobbiesOptions
{
Filters = new List<QueryFilter>
{
new QueryFilter(QueryFilter.FieldOptions.AvailableSlots, "1", QueryFilter.OpOptions.GE)
},
Order = new List<QueryOrder>
{
new QueryOrder(false, QueryOrder.FieldOptions.Created)
}
};
var response = await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
return response.Results;
}
private async void StartHeartbeat()
{
while (_currentLobby != null)
{
await LobbyService.Instance.SendHeartbeatPingAsync(_currentLobby.Id);
await Task.Delay(15000); // Every 15 seconds โ Lobby times out at 30s
}
}
}
NetworkVariable Design Reference
// State that persists and syncs to all clients on join โ NetworkVariable
public NetworkVariable<int> PlayerHealth = new(100,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
// One-time events โ ClientRpc
[ClientRpc]
public void OnHitClientRpc(Vector3 hitPoint, ClientRpcParams rpcParams = default)
{
VFXManager.SpawnHitEffect(hitPoint);
}
// Client sends action request โ ServerRpc
[ServerRpc(RequireOwnership = true)]
public void RequestFireServerRpc(Vector3 aimDirection)
{
if (!CanFire()) return; // Server validates
PerformFire(aimDirection);
OnFireClientRpc(aimDirection);
}
// Avoid: setting NetworkVariable every frame
private void Update()
{
// BAD: generates network traffic every frame
// Position.Value = transform.position;
// GOOD: use NetworkTransform component or custom prediction instead
}
๐ Advanced Capabilities
Client-Side Prediction and Rollback
- Implement full input history buffering with server reconciliation: store last N frames of inputs and predicted states
- Design snapshot interpolation for remote player positions: interpolate between received server snapshots for smooth visual representation
- Build a rollback netcode foundation for fighting-game-style games: deterministic simulation + input delay + rollback on desync
- Use Unity's Physics simulation API (
Physics.Simulate()) for server-authoritative physics resimulation after rollback
Dedicated Server Deployment
- Containerize Unity dedicated server builds with Docker for deployment on AWS GameLift, Multiplay, or self-hosted VMs
- Implement headless server mode: disable rendering, audio, and input systems in server builds to reduce CPU overhead
- Build a server orchestration client that communicates server health, player count, and capacity to a matchmaking service
- Implement graceful server shutdown: migrate active sessions to new instances, notify clients to reconnect
Anti-Cheat Architecture
- Design server-side movement validation with velocity caps and teleportation detection
- Implement server-authoritative hit detection: clients report hit intent, server validates target position and applies damage
- Build audit logs for all game-affecting Server RPCs: log timestamp, player ID, action type, and input values for replay analysis
- Apply rate limiting per-player per-RPC: detect and disconnect clients firing RPCs above human-possible rates
NGO Performance Optimization
- Implement custom
NetworkTransform with dead reckoning: predict movement between updates to reduce network frequency
- Use
NetworkVariableDeltaCompression for high-frequency numeric values (position deltas smaller than absolute positions)
- Design a network object pooling system: NGO NetworkObjects are expensive to spawn/despawn โ pool and reconfigure instead
- Profile bandwidth per-client using NGO's built-in network statistics API and set per-NetworkObject update frequency budgets