From 84b1aac360e11d49997834299cf77df8ab62b206 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 27 Apr 2026 15:23:12 -0500 Subject: [PATCH 1/4] update Updating the door example to provide a more recommended way of using OnValueChanged. --- .../Documentation~/basics/networkvariable.md | 203 +++++++++++++++++- 1 file changed, 193 insertions(+), 10 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index 0ea6c445e8..0c3048196f 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -148,43 +148,226 @@ The [synchronization and notification example](#synchronization-and-notification The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (open or closed) using an RPC that's sent to the server. Each time the door is used by a client, the `Door.ToggleStateRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. ```csharp +/// +/// A basic NetworkVariable driven door state example. +/// public class Door : NetworkBehaviour { - public NetworkVariable State = new NetworkVariable(); + /// + /// Only for UI purposes. + /// This provides an initial configuration state for the door. + /// + public enum DoorStates + { + Closed, + Open + } + + /// + /// Initializes the door to a specific state (server side) when first spawned. + /// + [Tooltip("Configures the door's initial state when 1st spawned.")] + public DoorStates InitialState = DoorStates.Closed; + + /// + /// A simple door state where the server has write permissions and everyone has read permissions. + /// + public NetworkVariable State = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + /// + /// Invoked while the is in the middle of + /// being spawned. + /// public override void OnNetworkSpawn() { - State.OnValueChanged += OnStateChanged; + // The write authority (server) does not need to know about its + // own changes (for this example) since it is the "single point + // of truth" for the door instance. + if (IsServer) + { + // Host/Server: + // Applies the configurable state upon spawning. + State.Value = InitialState == DoorStates.Open; + } + else + { + // Clients: + // Subscribe to changes in the door's state. + State.OnValueChanged += OnStateChanged; + } + } + + /// + /// Invoked once the door and all associated + /// components have finished the spawn process. + /// + protected override void OnNetworkPostSpawn() + { + // Everyone updates their door state when finished spawning the door. + UpdateFromState(); + base.OnNetworkPostSpawn(); } - public override void OnNetworkDespawn() + /// + /// Invoked just before this instance runs through its de-spawn + /// sequence. A good time to unsubscribe from things. + /// + public override void OnNetworkPreDespawn() { - State.OnValueChanged -= OnStateChanged; + if (!IsServer) + { + State.OnValueChanged -= OnStateChanged; + } + base.OnNetworkPreDespawn(); } + /// + /// Only clients invoke this. + /// Server makes changes to the state. + /// + /// + /// When the previous state equals the current state, we are a client + /// that is doing its 1st synchronization of this door instance. + /// + /// The previous state. + /// The current state. public void OnStateChanged(bool previous, bool current) { - // note: `State.Value` will be equal to `current` here + // Update to the current state while also providing a catch for + // the first synchronization where previous == current. + UpdateFromState(previous != current); + } + + /// + /// Common method used to update the actual door asset based on its current state. + /// + /// only set upon first spawn by a client + private void UpdateFromState(bool isFirstSynchronization = false) + { if (State.Value) { // door is open: // - rotate door transform // - play animations, sound etc. + // if first sync, reset to open and don't play sound } else { // door is closed: // - rotate door transform // - play animations, sound etc. + // if first sync, reset to closed and don't play sound + } + + // If 1st sync, don't log a message about a change in state + // since previous == current (i.e. no change in state) + if (!isFirstSynchronization) + { + var openClosed = State.Value ? "open" : "closed"; + Debug.Log($"[]The door is now {openClosed}."); + } + } + + /// + /// Override to apply specific checks (like a player having the right + /// key to open the door) or make it a non-virtual class and add logic + /// directly to this method. + /// + /// The player attempting to open the door. + /// + protected virtual bool CanPlayerToggleState(NetworkObject player) + { + // For this example, the door can always be toggled. + return true; + } + + /// + /// Invoked by either a Host or clients to interact with the door. + /// + public void Interact() + { + // Optional: + // This is only if you want clients to be able to + // interact with doors. A dedicated server would not + // be able to do this since it does not have a player. + if (IsServer && !IsHost) + { + // Optional to log a warning about this. + return; + } + + if (IsHost) + { + ToggleState(NetworkManager.LocalClientId); + } + else + { + // Clients send an RPC to server (write authority) who applies the + // change in state that will be synchronized with all client observers. + ToggleStateRpc(); + } + } + + /// + /// Invoked only server-side + /// Primary method to handle toggling the door state. + /// + /// The client toggling the door state. + private void ToggleState(ulong clientId) + { + // Get the server-side client player instance + var playerObject = NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId); + if (playerObject != null) + { + if (CanPlayerToggleState(playerObject)) + { + // Host toggles the state + State.Value = !State.Value; + UpdateFromState(); + } + else + { + ToggleStateFailRpc(RpcTarget.Single(clientId, RpcTargetUse.Temp)); + } + } + else + { + // Optional as to how you handle this. Since ToggleState is only invoked by + // sever-side only script, this could mean many things depending upon whether + // or not a client could interact with something and not have a player object. + // If that is the case, then don't even bother checking for a player object. + // If that is not the case, then there could be a timing issue between when + // something can be "interacted with" and when a player is about to be de-spawned. + // For this example, we just log a warning as this example was built with + // the requirement that a client has a spawned player object that is used for + // reference to determine if the client's player can toggle the state of the + // door or not. + NetworkLog.LogWarningServer($"Client-{clientId} has no spawned player object!"); } } - [Rpc(SendTo.Server)] - public void ToggleStateRpc() + /// + /// Invoked by clients. + /// Re-directs to the common method. + /// + /// includes that is automatically populated for you. + [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] + private void ToggleStateRpc(RpcParams rpcParams = default) + { + ToggleState(rpcParams.Receive.SenderClientId); + } + + /// + /// Optional: + /// Handling when a player cannot open a door. + /// + /// includes that is automatically populated for you. + [Rpc(SendTo.SpecifiedInParams, InvokePermission = RpcInvokePermission.Server)] + private void ToggleStateFailRpc(RpcParams rpcParams = default) { - // this will cause a replication over the network - // and ultimately invoke `OnValueChanged` on receivers - State.Value = !State.Value; + // Provide player feedback that toggling failed. + var openOrClose = State.Value ? "close" : "open"; + Debug.Log($"Failed to {openOrClose} the door!"); } } ``` From 8fb61f14017acc9ed9530000ad2a116970fd4885 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 1 May 2026 13:50:43 -0500 Subject: [PATCH 2/4] style removing trailing spaces. --- .../Documentation~/basics/networkvariable.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index 0c3048196f..3eab65ab91 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -269,8 +269,8 @@ public class Door : NetworkBehaviour } /// - /// Override to apply specific checks (like a player having the right - /// key to open the door) or make it a non-virtual class and add logic + /// Override to apply specific checks (like a player having the right + /// key to open the door) or make it a non-virtual class and add logic /// directly to this method. /// /// The player attempting to open the door. @@ -295,7 +295,7 @@ public class Door : NetworkBehaviour // Optional to log a warning about this. return; } - + if (IsHost) { ToggleState(NetworkManager.LocalClientId); @@ -321,7 +321,7 @@ public class Door : NetworkBehaviour { if (CanPlayerToggleState(playerObject)) { - // Host toggles the state + // Host toggles the state State.Value = !State.Value; UpdateFromState(); } @@ -336,9 +336,9 @@ public class Door : NetworkBehaviour // sever-side only script, this could mean many things depending upon whether // or not a client could interact with something and not have a player object. // If that is the case, then don't even bother checking for a player object. - // If that is not the case, then there could be a timing issue between when + // If that is not the case, then there could be a timing issue between when // something can be "interacted with" and when a player is about to be de-spawned. - // For this example, we just log a warning as this example was built with + // For this example, we just log a warning as this example was built with // the requirement that a client has a spawned player object that is used for // reference to determine if the client's player can toggle the state of the // door or not. @@ -356,7 +356,7 @@ public class Door : NetworkBehaviour { ToggleState(rpcParams.Receive.SenderClientId); } - + /// /// Optional: /// Handling when a player cannot open a door. From ecd19a0c97c005f0e29352ed35f618f6146117e7 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 1 May 2026 15:34:08 -0500 Subject: [PATCH 3/4] Update networkvariable.md Fixes to the example script --- .../Documentation~/basics/networkvariable.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index 3eab65ab91..3ec16cd38c 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -204,7 +204,7 @@ public class Door : NetworkBehaviour protected override void OnNetworkPostSpawn() { // Everyone updates their door state when finished spawning the door. - UpdateFromState(); + UpdateFromState(true); base.OnNetworkPostSpawn(); } @@ -235,7 +235,7 @@ public class Door : NetworkBehaviour { // Update to the current state while also providing a catch for // the first synchronization where previous == current. - UpdateFromState(previous != current); + UpdateFromState(); } /// @@ -259,8 +259,8 @@ public class Door : NetworkBehaviour // if first sync, reset to closed and don't play sound } - // If 1st sync, don't log a message about a change in state - // since previous == current (i.e. no change in state) + // Don't log a message about a change in state when first + // synchronizing. if (!isFirstSynchronization) { var openClosed = State.Value ? "open" : "closed"; From 902f3cf55eb9a666bb9065825a340d3193ff716a Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 1 May 2026 16:40:51 -0500 Subject: [PATCH 4/4] Update networkvariable.md Did another pass over this script with some improvements. --- .../Documentation~/basics/networkvariable.md | 147 ++++++++++++------ 1 file changed, 99 insertions(+), 48 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index 3ec16cd38c..4fa32270d0 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -148,14 +148,22 @@ The [synchronization and notification example](#synchronization-and-notification The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (open or closed) using an RPC that's sent to the server. Each time the door is used by a client, the `Door.ToggleStateRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. ```csharp +using System.Runtime.CompilerServices; +using Unity.Netcode; +using UnityEngine; + /// -/// A basic NetworkVariable driven door state example. +/// Example of using a to drive changes +/// in state. /// -public class Door : NetworkBehaviour +/// +/// This is a simple state driven door example. +/// This script was written with recommended usages patterns in mind. +/// +public class Door : NetworkBehaviour, INetworkUpdateSystem { /// - /// Only for UI purposes. - /// This provides an initial configuration state for the door. + /// The two door states. /// public enum DoorStates { @@ -169,10 +177,22 @@ public class Door : NetworkBehaviour [Tooltip("Configures the door's initial state when 1st spawned.")] public DoorStates InitialState = DoorStates.Closed; + /// + /// Used for example purposes. + /// When true, only the server can open and close the door. + /// Clients will receive a console log saying they could not open the door. + /// + public bool IsLocked; + /// /// A simple door state where the server has write permissions and everyone has read permissions. /// - public NetworkVariable State = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + private NetworkVariable m_State = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + + /// + /// The current state of the door. + /// + public DoorStates CurrentState => m_State.Value; /// /// Invoked while the is in the middle of @@ -187,27 +207,52 @@ public class Door : NetworkBehaviour { // Host/Server: // Applies the configurable state upon spawning. - State.Value = InitialState == DoorStates.Open; + m_State.Value = InitialState; } else { // Clients: // Subscribe to changes in the door's state. - State.OnValueChanged += OnStateChanged; + m_State.OnValueChanged += OnStateChanged; } } /// - /// Invoked once the door and all associated - /// components have finished the spawn process. + /// Invoked once the door and all associated components + /// have finished the spawn process. /// protected override void OnNetworkPostSpawn() { - // Everyone updates their door state when finished spawning the door. - UpdateFromState(true); + // Everyone updates their door state when finished spawning the door + // in order to assure the door reflects (visually) its current state. + UpdateFromState(); + + // Begin to start updating this NetworkBehaviour instance once all + // netcode related components have finished the spawn process. + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); base.OnNetworkPostSpawn(); } + /// + /// Example of using the usage pattern + /// where it only updates while spawned. + /// + /// The current update stage being invoked. + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + switch (updateStage) + { + case NetworkUpdateStage.Update: + { + if (Input.GetKeyDown(KeyCode.Space)) + { + Interact(); + } + break; + } + } + } + /// /// Invoked just before this instance runs through its de-spawn /// sequence. A good time to unsubscribe from things. @@ -216,56 +261,55 @@ public class Door : NetworkBehaviour { if (!IsServer) { - State.OnValueChanged -= OnStateChanged; + m_State.OnValueChanged -= OnStateChanged; } + + // Stop updating this NetworkBehaviour instance prior to running + // through the de-spawn process. + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); base.OnNetworkPreDespawn(); } /// - /// Only clients invoke this. /// Server makes changes to the state. + /// Clients receive the changes in state. /// /// /// When the previous state equals the current state, we are a client /// that is doing its 1st synchronization of this door instance. /// - /// The previous state. - /// The current state. - public void OnStateChanged(bool previous, bool current) + /// The previous state. + /// The current state. + public void OnStateChanged(DoorStates previous, DoorStates current) { - // Update to the current state while also providing a catch for - // the first synchronization where previous == current. UpdateFromState(); } /// - /// Common method used to update the actual door asset based on its current state. + /// Invoke when the state is updated in order to apply the change + /// in door state to the door asset itself. /// - /// only set upon first spawn by a client - private void UpdateFromState(bool isFirstSynchronization = false) + private void UpdateFromState() { - if (State.Value) - { - // door is open: - // - rotate door transform - // - play animations, sound etc. - // if first sync, reset to open and don't play sound - } - else + switch(m_State.Value) { - // door is closed: - // - rotate door transform - // - play animations, sound etc. - // if first sync, reset to closed and don't play sound - } - - // Don't log a message about a change in state when first - // synchronizing. - if (!isFirstSynchronization) - { - var openClosed = State.Value ? "open" : "closed"; - Debug.Log($"[]The door is now {openClosed}."); + case DoorStates.Closed: + { + // door is open: + // - rotate door transform + // - play animations, sound etc. + /// @@ -277,8 +321,9 @@ public class Door : NetworkBehaviour /// protected virtual bool CanPlayerToggleState(NetworkObject player) { - // For this example, the door can always be toggled. - return true; + // For this example, if the door "is locked" then clients will + // not be able to open the door but the host-client's player can. + return !IsLocked || player.IsOwnedByServer; } /// @@ -308,6 +353,12 @@ public class Door : NetworkBehaviour } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private DoorStates NextToggleState() + { + return m_State.Value == DoorStates.Open ? DoorStates.Closed : DoorStates.Open; + } + /// /// Invoked only server-side /// Primary method to handle toggling the door state. @@ -319,15 +370,16 @@ public class Door : NetworkBehaviour var playerObject = NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId); if (playerObject != null) { + var nextToggleState = NextToggleState(); if (CanPlayerToggleState(playerObject)) { // Host toggles the state - State.Value = !State.Value; + m_State.Value = nextToggleState; UpdateFromState(); } else { - ToggleStateFailRpc(RpcTarget.Single(clientId, RpcTargetUse.Temp)); + ToggleStateFailRpc(nextToggleState, RpcTarget.Single(clientId, RpcTargetUse.Temp)); } } else @@ -363,11 +415,10 @@ public class Door : NetworkBehaviour /// /// includes that is automatically populated for you. [Rpc(SendTo.SpecifiedInParams, InvokePermission = RpcInvokePermission.Server)] - private void ToggleStateFailRpc(RpcParams rpcParams = default) + private void ToggleStateFailRpc(DoorStates doorState, RpcParams rpcParams = default) { // Provide player feedback that toggling failed. - var openOrClose = State.Value ? "close" : "open"; - Debug.Log($"Failed to {openOrClose} the door!"); + Debug.Log($"Failed to {doorState} the door!"); } } ```