Why Game Networking Matters
Multiplayer games dominate the industry. From battle royales to cooperative adventures, players crave social gaming experiences. Understanding networking fundamentals opens doors to creating games that bring people together.
The Challenge: Networked games must handle latency, synchronization, and connection issues while maintaining smooth gameplay. A poorly implemented networking system can ruin even the best game design.
The Opportunity: Well-implemented multiplayer creates replayability, community, and long-term engagement that single-player games struggle to match.
Understanding Network Architectures
Client-Server Architecture
Most modern multiplayer games use a client-server model where one authoritative server manages game state and clients send input to the server.
How It Works:
- Server: Authoritative game state, validates all actions
- Clients: Send input, receive state updates
- Benefits: Prevents cheating, easier to manage, scalable
- Drawbacks: Requires server infrastructure, latency concerns
Unity Netcode Example:
using Unity.Netcode;
using UnityEngine;
public class NetworkPlayer : NetworkBehaviour
{
private NetworkVariable<Vector3> position = new NetworkVariable<Vector3>();
private NetworkVariable<float> health = new NetworkVariable<float>(100f);
public override void OnNetworkSpawn()
{
if (IsOwner)
{
// This client owns this player
SetupLocalPlayer();
}
else
{
// This is a remote player
SetupRemotePlayer();
}
}
[ServerRpc]
public void MovePlayerServerRpc(Vector3 newPosition)
{
// Server validates and updates position
if (IsValidPosition(newPosition))
{
position.Value = newPosition;
}
}
[ClientRpc]
public void TakeDamageClientRpc(float damage)
{
// All clients receive damage update
health.Value -= damage;
UpdateHealthUI();
}
private void SetupLocalPlayer()
{
// Enable input, camera, etc.
GetComponent<PlayerController>().enabled = true;
Camera.main.transform.SetParent(transform);
}
private void SetupRemotePlayer()
{
// Disable local input, use network position
GetComponent<PlayerController>().enabled = false;
}
}
Peer-to-Peer Architecture
In peer-to-peer, all players connect directly to each other without a central server.
How It Works:
- Peers: Each player hosts their own game state
- Synchronization: Players exchange state directly
- Benefits: No server costs, lower latency for small groups
- Drawbacks: Vulnerable to cheating, harder to scale, connection issues
When to Use:
- Small player counts (2-4 players)
- Local multiplayer games
- Prototyping and testing
- Games where cheating isn't critical
Local Multiplayer Implementation
Split-Screen Setup
Split-screen is the simplest form of multiplayer, perfect for couch co-op games.
Unity Split-Screen Example:
using UnityEngine;
public class SplitScreenManager : MonoBehaviour
{
public Camera player1Camera;
public Camera player2Camera;
public int playerCount = 2;
void Start()
{
SetupSplitScreen();
}
void SetupSplitScreen()
{
if (playerCount == 2)
{
// Two-player split screen
player1Camera.rect = new Rect(0f, 0.5f, 1f, 0.5f); // Top half
player2Camera.rect = new Rect(0f, 0f, 1f, 0.5f); // Bottom half
}
else if (playerCount == 4)
{
// Four-player split screen
player1Camera.rect = new Rect(0f, 0.5f, 0.5f, 0.5f); // Top-left
player2Camera.rect = new Rect(0.5f, 0.5f, 0.5f, 0.5f); // Top-right
player3Camera.rect = new Rect(0f, 0f, 0.5f, 0.5f); // Bottom-left
player4Camera.rect = new Rect(0.5f, 0f, 0.5f, 0.5f); // Bottom-right
}
}
}
Input Management for Multiple Players
Unity Input System:
using UnityEngine;
using UnityEngine.InputSystem;
public class MultiplayerInput : MonoBehaviour
{
public PlayerInput[] players;
void Start()
{
// Create input for each player
for (int i = 0; i < players.Length; i++)
{
players[i] = PlayerInput.Instantiate(
playerPrefab,
playerIndex: i,
controlScheme: "Gamepad",
pairWithDevice: Gamepad.all[i]
);
}
}
}
Online Multiplayer Fundamentals
Network Synchronization
Synchronizing game state across the network is critical. You need to decide what to sync and how often.
Synchronization Strategies:
1. State Synchronization
- Server sends complete state to clients
- Simple but bandwidth-intensive
- Good for small games or turn-based
2. Delta Compression
- Only send changes since last update
- Reduces bandwidth significantly
- More complex implementation
3. Input Synchronization
- Clients send input, server simulates
- Deterministic simulation required
- Most common for action games
Unity Netcode State Sync:
using Unity.Netcode;
using UnityEngine;
public class NetworkedObject : NetworkBehaviour
{
// Network variables automatically sync
private NetworkVariable<Vector3> networkPosition =
new NetworkVariable<Vector3>(default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private NetworkVariable<Quaternion> networkRotation =
new NetworkVariable<Quaternion>(default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
void Update()
{
if (IsServer)
{
// Server updates position
networkPosition.Value = transform.position;
networkRotation.Value = transform.rotation;
}
else
{
// Clients interpolate to network position
transform.position = Vector3.Lerp(
transform.position,
networkPosition.Value,
Time.deltaTime * 10f
);
transform.rotation = Quaternion.Lerp(
transform.rotation,
networkRotation.Value,
Time.deltaTime * 10f
);
}
}
}
Lag Compensation
Network latency causes delays between player actions and their effects. Lag compensation techniques make games feel responsive despite network delays.
Client-Side Prediction:
using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
public class LagCompensatedPlayer : NetworkBehaviour
{
private Vector3 serverPosition;
private Vector3 predictedPosition;
private Queue<InputState> inputHistory = new Queue<InputState>();
void Update()
{
if (IsOwner)
{
// Client predicts movement immediately
Vector3 input = GetInput();
predictedPosition += input * Time.deltaTime;
transform.position = predictedPosition;
// Store input for reconciliation
inputHistory.Enqueue(new InputState
{
position = predictedPosition,
timestamp = Time.time,
input = input
});
// Send input to server
MoveServerRpc(input);
}
}
[ServerRpc]
void MoveServerRpc(Vector3 input)
{
// Server validates and updates
serverPosition += input * Time.deltaTime;
UpdatePositionClientRpc(serverPosition);
}
[ClientRpc]
void UpdatePositionClientRpc(Vector3 serverPos)
{
if (IsOwner)
{
// Reconcile: adjust if server position differs
float error = Vector3.Distance(predictedPosition, serverPos);
if (error > 0.1f)
{
// Snap to server position and replay inputs
predictedPosition = serverPos;
ReplayInputs();
}
}
else
{
// Remote players just use server position
transform.position = serverPos;
}
}
void ReplayInputs()
{
// Replay recent inputs from corrected position
foreach (var inputState in inputHistory)
{
predictedPosition += inputState.input * Time.fixedDeltaTime;
}
}
}
Server Rewind (Hit Detection):
using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
public class ServerRewind : NetworkBehaviour
{
private Dictionary<float, Vector3> positionHistory = new Dictionary<float, Vector3>();
void Update()
{
if (IsServer)
{
// Store position history
positionHistory[Time.time] = transform.position;
// Clean old history (keep last 1 second)
float cutoff = Time.time - 1f;
var keysToRemove = new List<float>();
foreach (var key in positionHistory.Keys)
{
if (key < cutoff)
keysToRemove.Add(key);
}
foreach (var key in keysToRemove)
positionHistory.Remove(key);
}
}
[ServerRpc]
public void ShootServerRpc(Vector3 shootPosition, Vector3 direction, float clientTime)
{
// Rewind to when client shot
float serverTime = Time.time;
float latency = serverTime - clientTime;
float rewindTime = serverTime - latency;
// Find closest historical position
Vector3 rewindPosition = GetPositionAtTime(rewindTime);
// Perform hit detection at rewind position
RaycastHit hit;
if (Physics.Raycast(rewindPosition, direction, out hit))
{
// Hit detected, apply damage
if (hit.collider.CompareTag("Player"))
{
hit.collider.GetComponent<Health>().TakeDamage(10f);
}
}
}
Vector3 GetPositionAtTime(float time)
{
// Find closest time in history
float closestTime = 0f;
float minDiff = float.MaxValue;
foreach (var key in positionHistory.Keys)
{
float diff = Mathf.Abs(key - time);
if (diff < minDiff)
{
minDiff = diff;
closestTime = key;
}
}
return positionHistory.ContainsKey(closestTime)
? positionHistory[closestTime]
: transform.position;
}
}
Network Optimization
Bandwidth Optimization
Network bandwidth is limited. Optimize what you send and how often.
Optimization Techniques:
1. Reduce Update Frequency
// Update position every 0.1 seconds instead of every frame
private float lastUpdateTime = 0f;
private float updateInterval = 0.1f;
void Update()
{
if (Time.time - lastUpdateTime > updateInterval)
{
SendPositionUpdate();
lastUpdateTime = Time.time;
}
}
2. Only Send Changes
private Vector3 lastSentPosition;
private float positionThreshold = 0.1f;
void Update()
{
if (Vector3.Distance(transform.position, lastSentPosition) > positionThreshold)
{
SendPositionUpdate();
lastSentPosition = transform.position;
}
}
3. Compress Data
// Use half precision for positions (saves bandwidth)
public struct CompressedPosition
{
public ushort x, y, z; // 16-bit instead of 32-bit float
public Vector3 ToVector3()
{
return new Vector3(x / 100f, y / 100f, z / 100f);
}
public static CompressedPosition FromVector3(Vector3 pos)
{
return new CompressedPosition
{
x = (ushort)(pos.x * 100f),
y = (ushort)(pos.y * 100f),
z = (ushort)(pos.z * 100f)
};
}
}
Interpolation and Extrapolation
Smooth movement despite network delays requires interpolation and extrapolation.
Interpolation:
public class NetworkInterpolation : MonoBehaviour
{
private Vector3[] positionBuffer = new Vector3[10];
private float[] timeBuffer = new float[10];
private int bufferIndex = 0;
public void ReceivePositionUpdate(Vector3 position, float serverTime)
{
positionBuffer[bufferIndex] = position;
timeBuffer[bufferIndex] = serverTime;
bufferIndex = (bufferIndex + 1) % positionBuffer.Length;
}
void Update()
{
// Interpolate to position from 100ms ago (account for latency)
float targetTime = Time.time - 0.1f;
Vector3 interpolatedPos = InterpolatePosition(targetTime);
transform.position = interpolatedPos;
}
Vector3 InterpolatePosition(float targetTime)
{
// Find two positions to interpolate between
for (int i = 0; i < timeBuffer.Length - 1; i++)
{
if (timeBuffer[i] <= targetTime && timeBuffer[i + 1] >= targetTime)
{
float t = (targetTime - timeBuffer[i]) /
(timeBuffer[i + 1] - timeBuffer[i]);
return Vector3.Lerp(positionBuffer[i], positionBuffer[i + 1], t);
}
}
// Fallback: extrapolate from latest position
return ExtrapolatePosition(targetTime);
}
Vector3 ExtrapolatePosition(float targetTime)
{
// Estimate position based on velocity
if (positionBuffer.Length < 2)
return transform.position;
Vector3 velocity = (positionBuffer[1] - positionBuffer[0]) /
(timeBuffer[1] - timeBuffer[0]);
float timeDiff = targetTime - timeBuffer[0];
return positionBuffer[0] + velocity * timeDiff;
}
}
Common Networking Patterns
Authority and Ownership
Understanding who controls what is crucial for multiplayer games.
Ownership Patterns:
Server Authority:
- Server owns all game objects
- Clients send input, server decides
- Prevents cheating, standard for competitive games
Client Authority:
- Client owns their player object
- Server validates and broadcasts
- Lower latency, used in some action games
Hybrid Authority:
- Server owns important objects (players, projectiles)
- Clients own cosmetic objects (effects, particles)
- Balance between performance and security
Replication
Replication determines what data gets sent to which clients.
Replication Strategies:
1. Full Replication
- All clients receive all data
- Simple but bandwidth-intensive
- Good for small player counts
2. Relevance Filtering
- Only send data to nearby clients
- Reduces bandwidth significantly
- Essential for large-scale games
3. Interest Management
- Clients only receive relevant updates
- Based on distance, visibility, importance
- Most efficient for large games
Testing and Debugging
Network Testing Tools
Unity Netcode Testing:
using Unity.Netcode;
using UnityEngine;
public class NetworkTestManager : MonoBehaviour
{
void Start()
{
// Simulate network conditions
NetworkManager.Singleton.NetworkConfig.NetworkTransport =
new Unity.Netcode.Transports.UTP.UnityTransport();
// Add latency simulation
var transport = NetworkManager.Singleton.NetworkConfig.NetworkTransport
as Unity.Netcode.Transports.UTP.UnityTransport;
if (transport != null)
{
// Simulate 100ms latency
// Note: Actual implementation depends on transport
}
}
void OnGUI()
{
if (NetworkManager.Singleton != null)
{
GUILayout.Label($"Connected Clients: {NetworkManager.Singleton.ConnectedClients.Count}");
GUILayout.Label($"Is Server: {NetworkManager.Singleton.IsServer}");
GUILayout.Label($"Is Client: {NetworkManager.Singleton.IsClient}");
}
}
}
Common Networking Issues
Issue 1: Desynchronization
- Symptoms: Players see different game states
- Causes: Non-deterministic code, timing differences
- Solution: Use fixed timestep, deterministic physics, validate on server
Issue 2: Lag Spikes
- Symptoms: Sudden jumps in player positions
- Causes: Network congestion, packet loss
- Solution: Implement interpolation, buffer network updates, handle disconnections
Issue 3: Cheating
- Symptoms: Impossible player actions, modified game state
- Causes: Client authority without validation
- Solution: Server-side validation, authoritative server, anti-cheat systems
Best Practices
Performance Optimization
1. Minimize Network Calls
- Batch updates together
- Use delta compression
- Only send essential data
2. Optimize Update Frequency
- Adjust based on object importance
- Use adaptive update rates
- Reduce frequency for distant objects
3. Use Object Pooling
- Reuse network objects
- Reduce allocation overhead
- Improve garbage collection
Security Considerations
1. Server Validation
- Always validate on server
- Never trust client data
- Check for impossible actions
2. Encryption
- Encrypt sensitive data
- Use secure connections (TLS)
- Protect player information
3. Anti-Cheat
- Server-side validation
- Rate limiting
- Behavior analysis
Putting It All Together
Building multiplayer games requires understanding networking fundamentals, choosing the right architecture, and implementing proper synchronization and lag compensation. Start with local multiplayer to learn the basics, then move to online multiplayer with a client-server architecture.
The key is to start simple and iterate. Begin with basic state synchronization, add interpolation for smoothness, implement client-side prediction for responsiveness, and optimize for bandwidth as you scale.
Remember that networking is complex, but modern game engines provide excellent tools. Unity Netcode, Unreal's networking, and other frameworks handle much of the heavy lifting. Focus on game design and let the frameworks handle the networking details.
Next Steps
Ready to build your first multiplayer game? Start with local split-screen to understand multiplayer concepts, then move to online networking with Unity Netcode or your engine's networking solution.
For more networking tutorials, check out our complete guide to Unity game development or explore our game development resources for additional tools and learning materials.
Want to see networking in action? Try our AI Game Builder to experiment with different networking patterns and see how they impact gameplay.
Frequently Asked Questions
What's the difference between client-server and peer-to-peer?
Client-server uses a central authoritative server that all clients connect to. Peer-to-peer has players connect directly to each other. Client-server is more secure and scalable, while peer-to-peer is simpler for small groups.
How do I handle lag in multiplayer games?
Use client-side prediction for immediate feedback, server-side validation for accuracy, and interpolation/extrapolation for smooth movement. Lag compensation techniques like server rewind help with hit detection.
What's the best networking solution for Unity?
Unity Netcode for GameObjects (formerly MLAPI) is the official solution and works well for most games. Mirror Networking is a popular alternative with more features. Choose based on your specific needs.
How much bandwidth does multiplayer gaming require?
It depends on your game. Simple turn-based games might use 1-5 KB/s per player. Action games with many players can use 50-100 KB/s per player. Optimize by reducing update frequency and only sending essential data.
Can I make multiplayer games without a server?
Yes, for local multiplayer or small peer-to-peer games. For online multiplayer with many players, you'll need a server. Cloud services like Unity Gaming Services, Photon, or custom servers can host your game.