๐ Lesson 6.5: Mini-Project โ A Small Authoritative Multiplayer Prototype
Everything Module 6 taught, in one playable game. Two or more players share an arena, move their avatars, and race to collect coins โ with a synced score. Every rule runs on the authoritative server: clients request movement, the server owns positions and pickups, and scores replicate to all. It's small, but it's correct netcode, end to end.
๐ฏ What You'll Build
- Server-authoritative movement โ clients send input via RPC, the server moves and replicates
- Server-spawned coins as NetworkObjects
- Server-validated pickups that award a synced score NetworkVariable
- A scoreboard driven by
OnValueChanged, playable host + client
Estimated Time: 120 minutes ยท Prerequisite: Lessons 6.1โ6.4 (all of Module 6)
In This Lesson
The Architecture
Every piece maps onto the "server writes, clients request" model. Here's the whole loop:
Step 1: Scene & Prefabs
- A scene with a ground plane, a NetworkManager (Unity Transport), and the
ConnectionMenufrom Lesson 6.1. - Player prefab: a capsule +
NetworkObject+ aNetworkTransform(leave it server-authoritative) + a trigger collider (Is Trigger on) + the scripts below. Assign it as the NetworkManager's Player Prefab. - Coin prefab: a small sphere +
NetworkObject+NetworkTransform+ theCoinscript + a (non-trigger) collider. Register it in the NetworkManager's network prefabs list. - An empty CoinSpawner object with a
NetworkObjectand the spawner script.
Step 2: Authoritative Movement
The owner reads input and requests movement; the server applies it, and the server-authoritative NetworkTransform replicates the new position to everyone. The client never moves its own transform directly:
using Unity.Netcode;
using UnityEngine;
public class PlayerMovement : NetworkBehaviour
{
[SerializeField] float speed = 5f;
void Update()
{
if (!IsOwner) return; // only drive the avatar I own
var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical"));
if (input.sqrMagnitude > 0.001f)
MoveRpc(input.normalized); // request โ runs on the server
}
[Rpc(SendTo.Server)]
void MoveRpc(Vector3 direction)
{
// Executes on the SERVER. It owns the transform; NetworkTransform replicates it.
transform.position += direction * speed * Time.deltaTime;
}
}
โ ๏ธ Per-frame RPCs are a prototype convenience
Sending a move RPC every frame is fine for learning but chatty for a real game โ production code sends input at a fixed tick rate (or uses client prediction). Keep that in mind, but for this prototype the clarity is worth it: you can see the request โ authoritative move โ replicate flow directly.
Step 3: Coins & Pickups
The coin is a trivial NetworkBehaviour carrying a value. The server spawns a field of them on start:
using Unity.Netcode;
using UnityEngine;
public class Coin : NetworkBehaviour
{
public int Value = 10;
}
public class CoinSpawner : NetworkBehaviour
{
[SerializeField] GameObject coinPrefab;
[SerializeField] int count = 20;
[SerializeField] float range = 8f;
public override void OnNetworkSpawn()
{
if (!IsServer) return; // ONLY the server spawns
for (int i = 0; i < count; i++)
{
var pos = new Vector3(Random.Range(-range, range), 0.5f, Random.Range(-range, range));
var go = Instantiate(coinPrefab, pos, Quaternion.identity);
go.GetComponent<NetworkObject>().Spawn(); // replicate to all clients
}
}
}
Pickup detection runs on the server via the player's trigger. Because movement is server-authoritative, the physics overlap happens on the server โ so the trigger fires there, and we guard with IsServer to be safe:
using Unity.Netcode;
using UnityEngine;
public class PlayerScore : NetworkBehaviour
{
// server writes, everyone reads
public NetworkVariable<int> Score = new NetworkVariable<int>(
0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
void OnTriggerEnter(Collider other)
{
if (!IsServer) return; // authoritative pickup only on the server
if (other.TryGetComponent(out Coin coin) && coin.NetworkObject.IsSpawned)
{
Score.Value += coin.Value; // NetworkVariable โ replicates to all
coin.NetworkObject.Despawn(); // remove the coin for everyone
}
}
}
Notice there's no client-supplied number anywhere โ the server reads the coin's own value, checks it's still spawned, awards points, and despawns it. A cheat-client can't fake a pickup because it can't run this code; it only ever moves (a validated request), and the server decides the rest.
Step 4: Synced Score & UI
The score is already a NetworkVariable, so the UI just subscribes to changes โ no polling, and correct on every machine including late joiners:
using Unity.Netcode;
using UnityEngine;
using TMPro; // or UnityEngine.UI.Text
public class ScoreLabel : NetworkBehaviour
{
[SerializeField] TMP_Text label;
PlayerScore score;
public override void OnNetworkSpawn()
{
score = GetComponent<PlayerScore>();
score.Score.OnValueChanged += OnScoreChanged;
OnScoreChanged(0, score.Score.Value); // init to current (late joiners)
}
public override void OnNetworkDespawn()
{
if (score != null) score.Score.OnValueChanged -= OnScoreChanged;
}
void OnScoreChanged(int previous, int current) =>
label.text = $"{OwnerClientId}: {current}";
}
Each player object shows its own score above its head, updated the instant the server changes it โ and every client sees every player's score, because the NetworkVariable replicates to all.
Step 5: Play It
- Build the project. Run one instance, click Host; run a second, click Client (connects to
127.0.0.1). - Move each avatar with WASD. Each window controls only its own player, but both avatars move in both windows โ server-authoritative movement replicating.
- Drive over coins. They vanish for everyone, and the collector's score ticks up on every scoreboard.
- Confirm authority: nothing a client does changes shared state except through the server. That's a correct multiplayer game.
โ ๏ธ Testing tip
The Multiplayer Play Mode package spins up multiple virtual players inside one Editor, so you can test host + client without making a build each time. Highly recommended for iterating on netcode.
What You Built & Extending
โ Every Module 6 concept, working together
Sessions and roles (6.1), NetworkObjects + auto-spawned players + ownership (6.2), a synced Score NetworkVariable (6.3), and a movement-request RPC with server validation (6.4) โ assembled into a playable, cheat-resistant game. The authority discipline is the throughline: server writes, clients request.
- Win condition. Add a match timer and a server-side check that ends the round and broadcasts the winner with a
SendTo.ClientsAndHostRPC. - Respawning coins. Have the server spawn a new coin whenever one is collected, keeping the field full.
- Smoother movement. Send input at a fixed tick rate instead of every frame, or explore client prediction (a specialisation โ see the finale's "where to go next").
- Player names. A
FixedString32BytesNetworkVariable set on spawn, shown on the scoreboard.
Quick Quiz
Question 1: How does a client move its avatar in this prototype?
Question 2: Where does coin-pickup logic run, and why?
Question 3: Why is the score a NetworkVariable rather than sent by RPC?
Summary
๐ Key Takeaways
- A correct multiplayer game is the four pieces working together: session/roles, spawning/ownership, NetworkVariables, and RPCs.
- Movement: owner requests via
[Rpc(SendTo.Server)]; the server moves; server-authoritativeNetworkTransformreplicates. - Spawning: only the server instantiates +
Spawn()s coins; clients receive them by replication. - Pickups: validated on the server (trigger +
IsServerguard); award a server-written Score NetworkVariable and despawn the coin for all. - UI: driven by
OnValueChanged, correct on every machine including late joiners. - The throughline is authority: clients request, the server decides, results replicate.
๐ What's Next?
That completes the Netcode pillar. The final pillar is about your productivity and shipping. Module 7: Editor Tooling & Addressables begins with Lesson 7.1: Custom Inspectors & Property Drawers โ bending the Editor itself to your workflow.
๐ A real multiplayer game
Players move, coins collect, scores sync โ all server-authoritative and cheat-resistant. You've built genuine netcode from foundations to a finished loop.