๐ Lesson 6.3: NetworkVariables & State Synchronization
Your objects exist on every machine, but they don't yet agree on anything โ one player's health, the shared score, a door's open state. A NetworkVariable is the clean answer: a value the server writes and Netcode automatically replicates to every client, firing a callback when it changes. This is how persistent shared state stays in sync.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Declare a
NetworkVariable<T>with read/write permissions - Write it on the server and have it replicate to all clients automatically
- React to changes with the
OnValueChangedcallback - Know which types a NetworkVariable supports
- Sync transforms with
NetworkTransform - Choose between a NetworkVariable and an RPC for a given need
Estimated Time: 60 minutes ยท Prerequisite: Lesson 6.2 (NetworkObjects & ownership)
In This Lesson
Keeping State in Agreement
Persistent shared state โ anything that "is true for a while" โ must read the same on every machine: a player's health, the match score, whose turn it is, whether a gate is open. You could send these manually with messages, but you'd have to handle initial sync (a client joining mid-match needs the current values), change detection, and reliability yourself.
A NetworkVariable handles all of that. Declare one on a NetworkBehaviour, write it on the server, and Netcode replicates the value to every client โ including late joiners, who receive the current value on spawn โ and tells each client whenever it changes. It's the workhorse of "server writes, clients read" from Lesson 6.1.
Declaring a NetworkVariable
A NetworkVariable is a field on a NetworkBehaviour, constructed with an initial value and two permissions โ who may read it and who may write it:
using Unity.Netcode;
using UnityEngine;
public class PlayerHealth : NetworkBehaviour
{
// Everyone can READ; only the SERVER can WRITE (the authoritative default).
public NetworkVariable<int> Health = new NetworkVariable<int>(
100,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
}
NetworkVariableReadPermission.Everyone lets all clients read the value (the usual choice โ clients need to display it). NetworkVariableWritePermission.Server means only the server may set it, enforcing authority at the type level. There's also Owner write permission for owner-authoritative designs, but server write is the safe default โ a client can't forge the value because it isn't allowed to write it.
Write, Replicate, React
The server writes .Value; every client's copy updates automatically. Here's the full picture:
OnValueChanged. No manual messaging, and late-joiners get the current value on spawn.// Server-side gameplay. Only the server may write, so guard it.
public void TakeDamage(int amount)
{
if (!IsServer) return;
Health.Value = Mathf.Max(0, Health.Value - amount); // replicates to all clients
}
OnValueChanged
Clients don't poll the value โ they subscribe to OnValueChanged, which fires with the previous and new value whenever it changes. Subscribe in OnNetworkSpawn and unsubscribe in OnNetworkDespawn (the Module 2 lesson on garbage-free code warned about leaked event subscriptions โ the same discipline applies here):
public override void OnNetworkSpawn()
{
Health.OnValueChanged += HandleHealthChanged;
HandleHealthChanged(0, Health.Value); // initialise UI to the current value
}
public override void OnNetworkDespawn()
{
Health.OnValueChanged -= HandleHealthChanged; // always unsubscribe
}
void HandleHealthChanged(int previous, int current)
{
healthBar.fillAmount = current / 100f;
if (current < previous) PlayHitFlash();
}
Because the callback carries both values, you can react to the delta โ flash red when health drops, play a heal effect when it rises. This is the clean, event-driven way to keep visuals in step with networked state, on every machine including the host.
Supported Types
A NetworkVariable can hold value types that Netcode knows how to serialise: the numeric primitives, bool, Vector3/Quaternion/Color and other Unity structs, enums, and FixedString types (since managed string isn't allowed). For your own structs, implement INetworkSerializable so Netcode can pack them:
public struct PlayerStats : INetworkSerializable
{
public int Kills;
public FixedString32Bytes Name; // not a managed string
public void NetworkSerialize<T>(BufferSerializer<T> s) where T : IReaderWriter
{
s.SerializeValue(ref Kills);
s.SerializeValue(ref Name);
}
}
// then: public NetworkVariable<PlayerStats> Stats = new();
Keep NetworkVariables small and change them only when needed โ every change is bandwidth. For a big, rarely-read blob, an RPC on demand (next lesson) is often better than a constantly-synced variable.
Syncing Transforms
You could put a position in a NetworkVariable and update it every frame, but movement is common enough that Netcode ships a component for it: NetworkTransform. Add it to a networked object and its position, rotation, and scale replicate automatically, with interpolation to smooth out network jitter.
By default NetworkTransform is server-authoritative โ the server's transform is the truth and clients follow it (so a client moves its avatar by requesting movement via an RPC, and the server applies it). There's also a ClientNetworkTransform variant for owner-authoritative movement, used when low latency matters more than strict anti-cheat. For this course's authoritative model, server-authoritative NetworkTransform plus movement RPCs is the clean default โ which sets up the next lesson perfectly.
โ Rule of thumb
Use NetworkTransform for position/rotation, a NetworkVariable for other persistent state (health, score, flags), and an RPC for one-off events and client requests. Reaching for the right one of the three is most of netcode design.
NetworkVariable vs RPC
The two ways to move data are complementary, and choosing correctly keeps your netcode clean:
| NetworkVariable | RPC (Lesson 6.4) | |
|---|---|---|
| For | persistent state ("what is true now") | momentary events ("this just happened") |
| Late joiners | get the current value automatically | miss RPCs sent before they joined |
| Example | health, score, is-door-open | "play explosion", "I pressed jump" |
| Direction | server โ clients (server writes) | either way, incl. client โ server requests |
The tell: if a client joining mid-game needs to know it, it's state โ NetworkVariable. If it's a fleeting notification that only matters at the moment, it's an event โ RPC.
Hands-on Challenge
๐๏ธ Exercise 1: A synced health bar
Objective: Replicate a value and react to it.
- Add the
PlayerHealthNetworkBehaviour (server-writeHealthNetworkVariable) to your player prefab, with a world-space health bar. - Subscribe to
OnValueChangedinOnNetworkSpawn; update the bar; unsubscribe inOnNetworkDespawn. - On the server, call
TakeDamage(10)on a key press (guard withIsServer). - Run host + client. Damage a player on the host and confirm both windows' bars drop in sync.
๐ก Hint
If only the host's bar updates, check the write permission is Server and that you're setting Health.Value on the server, not the client. If the client bar starts wrong, remember to initialise it to Health.Value in OnNetworkSpawn โ late joiners need the current value applied once.
๐๏ธ Exercise 2: State or event?
For each, choose NetworkVariable or RPC: (a) the current match score; (b) a one-shot "goal scored!" celebration effect; (c) whether a drawbridge is currently raised; (d) a client telling the server "I want to fire."
โ Answers
(a) NetworkVariable โ persistent state a late joiner needs. (b) RPC โ a momentary event. (c) NetworkVariable โ persistent state. (d) RPC โ a client โ server request/event. (a) and (c) must survive a mid-game join; (b) and (d) are fleeting.
๐ฏ Quick Quiz
Question 1: With NetworkVariableWritePermission.Server, who can set the value โ and why is that the safe default?
Question 2: How do clients learn a NetworkVariable changed?
Question 3: A player joins mid-match. What do they see for a server-written NetworkVariable score?
Summary
๐ Key Takeaways
- A
NetworkVariable<T>replicates persistent shared state from server to clients automatically, including to late joiners. - Construct it with read/write permissions;
ReadPermission.Everyone+WritePermission.Serveris the authoritative default. - The server sets
.Value; clients react viaOnValueChanged(subscribe inOnNetworkSpawn, unsubscribe inOnNetworkDespawn). - Supported types are serialisable value types,
FixedString, and your ownINetworkSerializablestructs โ keep them small. NetworkTransformsyncs position/rotation/scale (server-authoritative by default, with interpolation).- State โ NetworkVariable, event โ RPC: if a late joiner needs it, it's state.
๐ What's Next?
State flows server โ clients. But how does a client ask the server to do something โ fire, jump, pick up? And how does the server broadcast a one-off event? In Lesson 6.4: RPCs โ Remote Procedure Calls we complete the picture with the universal [Rpc] attribute.
๐ Everyone agrees now
Server writes, clients read, callbacks fire. Persistent state stays in sync across every machine with a single field โ the backbone of a networked game's shared world.