Post

Unity Netcode - Synchronization Deep Dive: RPCs vs. NetworkVariables

Unity Netcode - Synchronization Deep Dive: RPCs vs. NetworkVariables
Visitors


Network Synchronization

Network synchronization is broadly divided into two methods: RPC and NetworkVariables.

  • NetworkVariables are most commonly used to synchronize state with clients who join the game late (Late Join).

  • On the other hand, if game logic relies solely on RPCs, reliability issues may arise for late-joining clients due to missed data packets.



RPC (Remote Procedure Calls)


  • RPCs are a way to handle direct communication between Server and Client, or Client and NetworkBehaviour, in addition to sending messages and event notifications.

  • A client can invoke a Server RPC on a NetworkObject. The RPC is placed in a local queue, sent to the server, and executed on the server’s version of the same NetworkObject.

  • When a client invokes an RPC, the SDK records the object, component, method, and parameters of that RPC and transmits this information over the network.



  • How Server RPC works


Desktop View



  • How Client RPC works


Desktop View



How to Use RPCs

  • Generally, you declare an RPC by adding an attribute to the method you want to call.

  • Important: The method name must end with Rpc, ServerRpc, or ClientRpc.


Legacy RPC
// Suffix is mandatory in method name
// Separate attributes for ServerRpc / ClientRpc

[ServerRpc]
public void PingServerRpc(int pingCount)
{
    // Executed on Server
}

[ClientRpc]
public void PongClientRpc(
    int pingCount,
    string message)
{
    // Executed on all Clients
}
Universal RPC (Recommended)
// Method name only needs 'Rpc' suffix
// Target specified via SendTo (Flexible)

[Rpc(SendTo.Server)]
public void PingRpc(int pingCount)
{
    // Executed on Server
}

[Rpc(SendTo.NotServer)]
void PongRpc(
    int pingCount,
    string message)
{
    // Executed on everyone except Server
}



RPC Attribute Target Table

  • There are many targets, but Server, NotServer, and Everyone are the most commonly used.


Desktop View
Desktop View


  • Parameters often used with attributes:

Desktop View

1
2
3
[Rpc(SendTo.Everyone, RequireOwnership = false)]
 
[ServerRpc(RequireOwnership = false)]


  • Legacy RPCs might feel more intuitive, but since they are being replaced by Universal RPC attributes, it’s better to get used to the Universal form.
  • Both are currently usable, so further research on their differences might be needed.


Practical Example 1

Desktop View



Practical Example 2

  • Each client executes the client-only method TryAddIngredient.
  • Then, it calls the ServerRpc AddIngredientServerRpc.
  • ServerRpc runs only on the server, but inside it calls AddIngredientClientRpc, which executes on all connected clients to reflect the change.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public bool TryAddIngredient(KitchenObjectSO kitchenObjectSO)
{
    AddIngredientServerRpc(GetKitchenObjectSOIndex(kitchenObjectSO))
}
 
[ServerRpc(RequireOwnership = false)]
private void AddIngredientServerRpc(int kitchenObjectSOIndex)
{
    AddIngredientClientRpc(kitchenObjectSOIndex);
}
 
[ClientRpc]
private void AddIngredientClientRpc(int kitchenObjectSOIndex)
{
    KitchenObjectSO kitchenObjectSO =
        KitchenGameMultiplayer.Instance.GetKitchenObjectSOFromIndex(kitchenObjectSOIndex);
     
    kitchenObjectSOList.Add(kitchenObjectSO);
     
    OnIngredientAdded?.Invoke(this, new OnIngredientAddedEventArgs
    {
        kitchenObjectSO = kitchenObjectSO
    });
}






NetworkVariables Synchronization

  • Unlike RPCs, NetworkVariables provide continuous synchronization of properties between server and client.

  • Unlike RPCs and messages, they are not one-off communications at a specific point in time, meaning they persist for clients connecting later.


  • NetworkVariable is a wrapper for a specified value type T. To access the synchronized value, you must use the NetworkVariable.Value property.

  • Note: Type T must be a Value Type (int, bool, float, FixedString, etc.). Reference types are not allowed.


1
2
3
4
5
6
7
8
9
private NetworkVariable<int> testValue = new NetworkVariable<int>();
private const int initValue = 1111;
 
public override void OnNetworkSpawn()
{
       testValue.Value = initValue;
 
       ...
}



How NetworkVariables Synchronize

1. New Client Joins Game (Late Join)

  • When a NetworkObject with NetworkVariable properties is spawned on a NetworkBehaviour, the current state (Value) of the NetworkVariable is automatically synchronized to the client.


2. Connected Clients

  • When a NetworkVariable value changes, all connected clients subscribed to NetworkVariable.OnValueChanged are notified before the value is updated locally.
  • The OnValueChanged callback takes two parameters: previous and current.


Practical Example 1

  • Create a NetworkVariable named state and register State_OnValueChanged to OnValueChanged.
  • Whenever state changes, invoke all subscribers of the OnStateChanged event.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// State is an enum type T
[SerializeField] private NetworkVariable<State> state = new NetworkVariable<State>(State.WaitingToStart);
 
public event EventHandler OnStateChanged;
 
public override void OnNetworkSpawn()
{
    state.OnValueChanged += State_OnValueChanged;
    isGamePaused.OnValueChanged += IsGamePaused_OnValueChanged;
 
    if (IsServer)
    {
        NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback;
        NetworkManager.Singleton.SceneManager.OnLoadEventCompleted += SceneManager_OnLoadEventCompleted;
    }
}
 
private void State_OnValueChanged(State previousValue, State newValue)
{
    OnStateChanged?.Invoke(this, EventArgs.Empty);
}


Practical Example 2

  • Each client calls a ServerRpc when using a Door, toggling the Door’s state on the server.
  • Once Door.State.Value changes, all connected clients sync to the new current value (bool here), and OnStateChanged is called on each client.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Door : NetworkBehaviour
{
    public NetworkVariable<bool> State = new NetworkVariable<bool>();
 
    public override void OnNetworkSpawn()
    {
        State.OnValueChanged += OnStateChanged;
    }
 
    public override void OnNetworkDespawn()
    {
        State.OnValueChanged -= OnStateChanged;
    }
 
    public void OnStateChanged(bool previous, bool current)
    {
        // note: `State.Value` will be equal to `current` here
        if (State.Value)
        {
            // door is open:
            //  - rotate door transform
            //  - play animations, sound etc.
        }
        else
        {
            // door is closed:
            //  - rotate door transform
            //  - play animations, sound etc.
        }
    }
 
    [Rpc(SendTo.Server)]
    public void ToggleServerRpc()
    {
        // this will cause a replication over the network
        // and ultimately invoke `OnValueChanged` on receivers
        State.Value = !State.Value;
    }
}






Custom NetworkVariables

  • It is possible to synchronize custom “packet-like” data structures using NetworkVariable.
  • Custom NetworkVariable types must implement serialization, typically via generic types.


NetworkVariable Example

  • When declaring a custom NetworkVariable, you can specify initialization values as well as NetworkVariableReadPermission and NetworkVariableWritePermission.
  • We used an arbitrary struct MyCustomData for serialization.
  • This struct must implement the INetworkSerializable interface.
  • Inside, define the Value types you want to sync.


1
NetworkSerialize<T>


  • Implement serialization of variables using BufferSerializer inside this method.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private NetworkVariable<MyCustomData> randomNumber = new NetworkVariable<MyCustomData>(
    new MyCustomData
    {
        _int = 1,
        _bool = true,
        _message = "nan"
    }, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
 
public struct MyCustomData : INetworkSerializable
{
    public int _int;
    public bool _bool;
    public string _message;
     
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref _int);
        serializer.SerializeValue(ref _bool);
        serializer.SerializeValue(ref _message);
    }
}


  • Subscribe to randomNumber via OnValueChanged with a lambda to log the output.
  • Pressing “T” sets a new MyCustomData value. Note who has ownership of the NetworkVariable.
  • In the Door example, state was changed via ServerRpc (Server-owned). Here, randomNumber is Client-owned.
  • Summary: Roles are divided based on ownership: Server-managed (FSM states, game logic) vs. Client-managed (Player stats).


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public override void OnNetworkSpawn()
{
    randomNumber.OnValueChanged += (MyCustomData previousValue, MyCustomData newValue) =>
    {
        Debug.Log(OwnerClientId + ";  " + newValue._int + ";  " + newValue._bool + ";  " + newValue._message);
    };
}

private void Update()
{
    if (!IsOwner) return;

    if (Input.GetKeyDown(KeyCode.T))
    {       
        randomNumber.Value = new MyCustomData
        {
            _int = Random.Range(0, 100),
            _bool = Random.Range(0, 1) == 0 ? true : false,
            _message = "Hello, World!",
        };
    }
}



NetworkList Example

  • NetworkList is a List version of NetworkVariable.
  • Note the addition of the IEquatable interface. This allows NetworkVariable or NetworkList structs to perform efficient equality checks.
  • It also helps reduce network traffic by detecting if data actually changed.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public struct PlayerData : IEquatable<PlayerData>, INetworkSerializable
{
    public ulong clientId;
    public int colorId;
    public FixedString64Bytes playerName;
    public FixedString64Bytes playerId;
     
    public bool Equals(PlayerData other)
    {
        return clientId == other.clientId &&
               colorId == other.colorId &&
               playerName == other.playerName &&
               playerId == other.playerId;
    }
 
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref clientId);
        serializer.SerializeValue(ref colorId);
        serializer.SerializeValue(ref playerName);
        serializer.SerializeValue(ref playerId);
    }
}


  • Since it’s a List type, the event arguments are not previous and current. Instead, NetworkListEvent contains:
  • Value and PreviousValue.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public event EventHandler OnPlayerDataNetworkListChanged;
 
private NetworkList<PlayerData> playerDataNetworkList;
 
private void Awake()
{
    playerDataNetworkList = new NetworkList<PlayerData>();
    playerDataNetworkList.OnListChanged += PlayerDataNetworkList_OnListChanged;
}
 
private void PlayerDataNetworkList_OnListChanged(NetworkListEvent<PlayerData> changeEvent)
{
    OnPlayerDataNetworkListChanged?.Invoke(this, EventArgs.Empty);
}






RPC vs NetworkVariable

  • Use RPC: For transient events or information transfer. Useful only at the moment the info is received.
  • Use NetworkVariables: Useful for managing persistent state information. “Persistent state” should be maintained in-game and consistently delivered to all players.

  • The quickest way to decide is to ask: “Does a player joining midway need to receive this information?”


Desktop View
NetworkVariable sends the current state, allowing late-joining clients to easily catch up.


Desktop View
If an RPC is sent to all clients, players joining after the RPC was sent will miss that info and see incorrect visuals.


  • You might think, “Then isn’t RPC useless? Can’t I just use NetworkVariables for everything?” But…



Why Not Use NetworkVariables for Everything?

  • RPCs are simpler.
  • You don’t need to declare a NetworkVariable for every transient event in the game (like explosions, object spawning).


  • RPCs are better for synchronized delivery of multiple values.
  • If you change NetworkVariables “a” and “b”, there is no guarantee that the client receives both changes in the same frame.


  • Due to latency, clients might receive updates at different times.


Desktop View
There is no guarantee that different NetworkVariables updated in the same tick are delivered to clients simultaneously.



  • On the other hand, if sent as two parameters of the same RPC, the client receives both values simultaneously.


Desktop View
To ensure multiple values sync at the exact same time, you can bundle them into a Client RPC.



Summary

  1. Use NetworkVariable for persistent state information.
  2. Persistent state means information that must be maintained in the game and consistently delivered to all players.
  3. This allows new players to immediately sync to the latest state upon connection, ensuring everyone shares the same state.
This post is licensed under CC BY 4.0 by the author.