diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md
index 0ea6c445e8..4fa32270d0 100644
--- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md
+++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md
@@ -148,43 +148,277 @@ 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
-public class Door : NetworkBehaviour
+using System.Runtime.CompilerServices;
+using Unity.Netcode;
+using UnityEngine;
+
+///
+/// Example of using a to drive changes
+/// in state.
+///
+///
+/// This is a simple state driven door example.
+/// This script was written with recommended usages patterns in mind.
+///
+public class Door : NetworkBehaviour, INetworkUpdateSystem
{
- public NetworkVariable State = new NetworkVariable();
+ ///
+ /// The two door states.
+ ///
+ 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;
+
+ ///
+ /// 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.
+ ///
+ 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
+ /// 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.
+ m_State.Value = InitialState;
+ }
+ else
+ {
+ // Clients:
+ // Subscribe to changes in the door's state.
+ m_State.OnValueChanged += OnStateChanged;
+ }
}
- public override void OnNetworkDespawn()
+ ///
+ /// 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
+ // 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.
+ ///
+ public override void OnNetworkPreDespawn()
+ {
+ if (!IsServer)
+ {
+ m_State.OnValueChanged -= OnStateChanged;
+ }
+
+ // Stop updating this NetworkBehaviour instance prior to running
+ // through the de-spawn process.
+ NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update);
+ base.OnNetworkPreDespawn();
+ }
+
+ ///
+ /// 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(DoorStates previous, DoorStates current)
+ {
+ UpdateFromState();
+ }
+
+ ///
+ /// Invoke when the state is updated in order to apply the change
+ /// in door state to the door asset itself.
+ ///
+ private void UpdateFromState()
+ {
+ switch(m_State.Value)
+ {
+ case DoorStates.Closed:
+ {
+ // door is open:
+ // - rotate door transform
+ // - play animations, sound etc.
+ ///
+ /// 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)
{
- State.OnValueChanged -= OnStateChanged;
+ // 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;
}
- public void OnStateChanged(bool previous, bool current)
+ ///
+ /// Invoked by either a Host or clients to interact with the door.
+ ///
+ public void Interact()
{
- // note: `State.Value` will be equal to `current` here
- if (State.Value)
+ // 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)
{
- // door is open:
- // - rotate door transform
- // - play animations, sound etc.
+ // Optional to log a warning about this.
+ return;
+ }
+
+ if (IsHost)
+ {
+ ToggleState(NetworkManager.LocalClientId);
}
else
{
- // door is closed:
- // - rotate door transform
- // - play animations, sound etc.
+ // Clients send an RPC to server (write authority) who applies the
+ // change in state that will be synchronized with all client observers.
+ ToggleStateRpc();
}
}
- [Rpc(SendTo.Server)]
- public void ToggleStateRpc()
+ [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.
+ ///
+ /// 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)
+ {
+ var nextToggleState = NextToggleState();
+ if (CanPlayerToggleState(playerObject))
+ {
+ // Host toggles the state
+ m_State.Value = nextToggleState;
+ UpdateFromState();
+ }
+ else
+ {
+ ToggleStateFailRpc(nextToggleState, 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!");
+ }
+ }
+
+ ///
+ /// 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(DoorStates doorState, 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.
+ Debug.Log($"Failed to {doorState} the door!");
}
}
```