Server‐authoritative networking - Development-Illustrated/the-game GitHub Wiki
What makes a Server-Authoritative game?
The core principle of our multiplayer architecture is that the server is the ultimate authority on everything that happens in the game. This means:
- Clients (players) only send their intentions (inputs, actions they want to take)
- The server validates these inputs and decides what actually happens
- The server broadcasts the results back to all clients
- Clients update their local view based on the server's decisions
This approach prevents cheating and ensures all players see a consistent game state, even if it sometimes means a slight delay between input and visible result.
Overview
Our game will be made up of several interconnected systems that span both the server and client. Here's the 10,000 foot view of how everything fits together:
- Player Input: Captured on the client, sent to the server for validation
- Server Logic: Validates input, runs physics, calculates damage, manages game state
- Network Synchronisation: Sends authoritative-data from server to clients
- Client Rendering: Updates visuals, plays effects, shows UI based on server data
- Client Prediction: Makes movement feel responsive despite network latency
The beauty of Netcode for GameObjects is that it abstracts away much of the networking complexity while still letting us maintain server authority.
Core Components and Their Responsibilities
Server-Side Components
These components run on the server and contain the authoritative game logic:
GameNetworkManager is our central connection point. It handles player connections, spawns player objects, and initialises our core systems. There's only one instance of this in a scene, and it's the "boss" of our network setup. It doesn't do much gameplay logic itself - it's more of a coordinator or orchestrator.
GameStateSystem tracks match-level information like scores, kill counts, and game status. It's the authority on who's winning and when the game should end. When a player reaches X kills, this system will trigger the match end sequence.
SpawnManager handles player respawning after death. It selects spawn points, manages respawn timers, and makes sure players don't spawn on top of each other.
PickupManager controls weapon spawns throughout the level, it decides when and where weapons appear, and manages their collection by players.
Network-Synchronised Components
These components exist on both server and client, but the server version is always authoritative.
PlayerNetworkController is attached to each player and handles basic movement. On the server, it validates movement requests to prevent cheating (moving through walls or faster than allowed, etc). On clients, it synchronises position and rotation.
PlayerCombatController manages weapons, shooting and damage. The server version does hit detection, damage calculation, and confirms kills. The client version shows weapon effects and sends shooting requests.
HealthManager tracks player health and handles death. The server version is the source of truth for health values, while the client version updates health bars and triggers damage effects.
ProjectileController handles bullets or other projectiles. The server calculates true trajectories and hit detection, while clients visualise them.
Client-Only Components
These components only run on the local client machine and handle user experience.
InputManager captures and processes player inputs from keyboard, mouse, or controller. It translates these inputs into network messages that get sent to the server.
LocalPlayerController handles camera control and local player-specific behaviours. It makes the game feel responsive by working with client prediction.
ClientEffectsManager handles visual and audio effects that don't require network synchronisations - not sure what needs to be in there
UIController manages all UI elements including health displays, ammo counters, kill feed, and chat. It takes information from network components and updates what the player sees.
ClientNetworkPredictor implements client-side prediction and reconciliation for smoother gameplay despite network latency. It predicts where the player will be based on inputs, then adjusts if the server disagrees.
Data Flow
Let's walk through some common gameplay scenarios and see how the data flows through:
Player Movement
- Client: InputManager detects WASD/Controller input
- Client: Immediately moves character locally (prediction)
- Client: Sends movement input to server via
MovePlayerServerRpc()
- Server: Validates movement (checking for collisions, etc)
- Server: Updates authoritative position
- Server: Broadcasts position via
NetworkTransform
- Client: Receives authoritative position
- Client: Reconciles any differences between predicted and actual position
This cycle will happen multiple times per second, the client prediction is what makes movement feel responsive despite network delay.
Combat and Damage
- Client: Player clicks to fire weapon
- Client: Shows any visual effects immediately (purely visual)
- Client: Sends fire request to server via
FireWeaponServerRpc()
- Server: Validates shot (weapon cooldowns, ammo, etc)
- Server: Performs hit detection using raycasts or projectile physics
- Server: If hit, calculates damage and updates targe's health
- Server: Broadcasts hit confirmation via
HitConfirmedClientRpc()
- Server: Updates health
NetworkVariable
(sync automatically, like aNetworkTransform
) - Client: Plays hit effects based on server confirmation
- Client: Updates health bar UI based on
NetworkVariable
change
Note that all important decisions (did the shot hit, how much damage, etc) happen on the server.
Player Death and Respawning
- Server: HealthManager detects health <= 0
- Server: Triggers player death sequence
- Server: Updates GameStateSystem with kill information
- Server: Broadcasts death via
PlayerKilledClientRpc()
- Client: Shows death effects and UI
- Server: SpawnManager starts respawn timer
- Server: After timer, selects spawn point and respawns player
- Server: Updates kill feed via
UpdateKillFeedClientRpc()
- Client: Updates kill feed UI
Game End
- Server: GameStateSystem detects player reached X kills
- Server: Sets game state to
ended
- Server: Broadcasts game end via
GameEndClientRpc()
- Client: Shows game end UI with results
- Server: After cooldown, GameNetworkManager loads next level or returns to a lobby or whatever
Communication Methods: ServerRpc and ClientRpc
Netcode for GameObjects gives us two main ways to communicate between client and server:
ServerRpc functions are called by clients but execute on the server. They're used when clients want to do something that needs validation:
- Player movement
- Weapon firing
- Using items
- Chat messages
Most ServerRpc
functions return void - the result comes back later via NetworkVariables
or ClientRpc
.
ClientRpc functions are called by the server and execute on clients. They're used to broadcast information or trigger effects:
- Hit confirmation
- Kill feed updates
- Game events
- Chat message distribution
NetworkVariables
automatically sync whenever their value changes. They're perfect for:
- Health values
- Ammo counts
- Player scores
- Game state
Events vs Direct References
For communication between components, we'll use a mix of events and direct references.
When to use events
- UI updates from network components
- Visual/audio effects
- One-to-many notifications
- When you want loose coupling between systems
When to use direct references
- Core gameplay functions called frequently
- Performance-critical code paths
- When the relationship is direct and clear
For client-side to network component communication, create a ClientGameManager
that servers as a bridge.