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!");
}
}
```