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