Skip to main content

๐Ÿ”„ 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 OnValueChanged callback
  • 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:

A NetworkVariable replicating from server to clients The server sets Health.Value to 80. Netcode replicates the new value to two clients, whose copies update to 80 and each fire an OnValueChanged callback that updates the health bar UI. Server writes Health.Value = 80; replicate (automatic, reliable) Client 1 Health.Value โ†’ 80 OnValueChanged(100, 80) โ†’ update bar Client 2 Health.Value โ†’ 80 OnValueChanged(100, 80) โ†’ update bar
Figure 1: The server writes once; Netcode replicates the value to every client and each fires 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:

NetworkVariableRPC (Lesson 6.4)
Forpersistent state ("what is true now")momentary events ("this just happened")
Late joinersget the current value automaticallymiss RPCs sent before they joined
Examplehealth, score, is-door-open"play explosion", "I pressed jump"
Directionserver โ†’ 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.

  1. Add the PlayerHealth NetworkBehaviour (server-write Health NetworkVariable) to your player prefab, with a world-space health bar.
  2. Subscribe to OnValueChanged in OnNetworkSpawn; update the bar; unsubscribe in OnNetworkDespawn.
  3. On the server, call TakeDamage(10) on a key press (guard with IsServer).
  4. 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.Server is the authoritative default.
  • The server sets .Value; clients react via OnValueChanged (subscribe in OnNetworkSpawn, unsubscribe in OnNetworkDespawn).
  • Supported types are serialisable value types, FixedString, and your own INetworkSerializable structs โ€” keep them small.
  • NetworkTransform syncs 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.