-
Notifications
You must be signed in to change notification settings - Fork 461
fix: Improved OnValueChanged door example #3959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop-2.0.0
Are you sure you want to change the base?
Changes from all commits
84b1aac
de3f9f5
ab995fd
8fb61f1
3b1ad0b
6658dc1
ecd19a0
902f3cf
c39b29c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Example of using a <see cref="NetworkVariable{T}"/> to drive changes | ||||||
| /// in state. | ||||||
| /// </summary> | ||||||
| /// <remarks> | ||||||
| /// This is a simple state driven door example. | ||||||
| /// This script was written with recommended usages patterns in mind. | ||||||
| /// </remarks> | ||||||
| public class Door : NetworkBehaviour, INetworkUpdateSystem | ||||||
| { | ||||||
| public NetworkVariable<bool> State = new NetworkVariable<bool>(); | ||||||
| /// <summary> | ||||||
| /// The two door states. | ||||||
| /// </summary> | ||||||
| public enum DoorStates | ||||||
| { | ||||||
| Closed, | ||||||
| Open | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Initializes the door to a specific state (server side) when first spawned. | ||||||
| /// </summary> | ||||||
| [Tooltip("Configures the door's initial state when 1st spawned.")] | ||||||
| public DoorStates InitialState = DoorStates.Closed; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Used for <see cref="CanPlayerToggleState"/> 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. | ||||||
| /// </summary> | ||||||
| public bool IsLocked; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// A simple door state where the server has write permissions and everyone has read permissions. | ||||||
| /// </summary> | ||||||
| private NetworkVariable<DoorStates> m_State = new NetworkVariable<DoorStates>(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// The current state of the door. | ||||||
| /// </summary> | ||||||
| public DoorStates CurrentState => m_State.Value; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Invoked while the <see cref="NetworkObject"/> is in the middle of | ||||||
| /// being spawned. | ||||||
| /// </summary> | ||||||
| public override void OnNetworkSpawn() | ||||||
| { | ||||||
| State.OnValueChanged += OnStateChanged; | ||||||
| // The write authority (server) does not need to know about its | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // own changes (for this example) since it is the "single point | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // 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() | ||||||
| /// <summary> | ||||||
| /// Invoked once the door and all associated components | ||||||
| /// have finished the spawn process. | ||||||
| /// </summary> | ||||||
| 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. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| UpdateFromState(); | ||||||
|
|
||||||
| // Begin to start updating this NetworkBehaviour instance once all | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // netcode related components have finished the spawn process. | ||||||
| NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); | ||||||
| base.OnNetworkPostSpawn(); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Example of using the <see cref="INetworkUpdateSystem"/> usage pattern | ||||||
| /// where it only updates while spawned. | ||||||
| /// </summary> | ||||||
| /// <param name="updateStage">The current update stage being invoked.</param> | ||||||
| public void NetworkUpdate(NetworkUpdateStage updateStage) | ||||||
| { | ||||||
| switch (updateStage) | ||||||
| { | ||||||
| case NetworkUpdateStage.Update: | ||||||
| { | ||||||
| if (Input.GetKeyDown(KeyCode.Space)) | ||||||
| { | ||||||
| Interact(); | ||||||
| } | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Invoked just before this instance runs through its de-spawn | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// sequence. A good time to unsubscribe from things. | ||||||
| /// </summary> | ||||||
| public override void OnNetworkPreDespawn() | ||||||
| { | ||||||
| if (!IsServer) | ||||||
| { | ||||||
| m_State.OnValueChanged -= OnStateChanged; | ||||||
| } | ||||||
|
|
||||||
| // Stop updating this NetworkBehaviour instance prior to running | ||||||
| // through the de-spawn process. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); | ||||||
| base.OnNetworkPreDespawn(); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Server makes changes to the state. | ||||||
| /// Clients receive the changes in state. | ||||||
| /// </summary> | ||||||
| /// <remarks> | ||||||
| /// When the previous state equals the current state, we are a client | ||||||
| /// that is doing its 1st synchronization of this door instance. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// </remarks> | ||||||
| /// <param name="previous">The previous <see cref="DoorStates"/> state.</param> | ||||||
| /// <param name="current">The current <see cref="DoorStates"/> state.</param> | ||||||
| public void OnStateChanged(DoorStates previous, DoorStates current) | ||||||
| { | ||||||
| UpdateFromState(); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Invoke when the state is updated in order to apply the change | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// in door state to the door asset itself. | ||||||
| /// </summary> | ||||||
| private void UpdateFromState() | ||||||
| { | ||||||
| switch(m_State.Value) | ||||||
| { | ||||||
| case DoorStates.Closed: | ||||||
| { | ||||||
| // door is open: | ||||||
| // - rotate door transform | ||||||
| // - play animations, sound etc. | ||||||
| /// <see cref="Netcode.Components.Helpers.ComponentCont" | ||||||
| break; | ||||||
| } | ||||||
| case DoorStates.Open: | ||||||
| { | ||||||
| // door is closed: | ||||||
| // - rotate door transform | ||||||
| // - play animations, sound etc. | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
| Debug.Log($"[{name}] Door is currently {m_State.Value}."); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 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. | ||||||
| /// </summary> | ||||||
| /// <param name="player">The player attempting to open the door.</param> | ||||||
| /// <returns></returns> | ||||||
| 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) | ||||||
| /// <summary> | ||||||
| /// Invoked by either a Host or clients to interact with the door. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// </summary> | ||||||
| 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; | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Invoked only server-side | ||||||
| /// Primary method to handle toggling the door state. | ||||||
| /// </summary> | ||||||
| /// <param name="clientId">The client toggling the door state.</param> | ||||||
| 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!"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Invoked by clients. | ||||||
| /// Re-directs to the common <see cref="ToggleState(ulong)"/> method. | ||||||
| /// </summary> | ||||||
| /// <param name="rpcParams">includes <see cref="RpcReceiveParams.SenderClientId"/> that is automatically populated for you.</param> | ||||||
| [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] | ||||||
| private void ToggleStateRpc(RpcParams rpcParams = default) | ||||||
| { | ||||||
| ToggleState(rpcParams.Receive.SenderClientId); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Optional: | ||||||
| /// Handling when a player cannot open a door. | ||||||
| /// </summary> | ||||||
| /// <param name="rpcParams">includes <see cref="RpcReceiveParams.SenderClientId"/> that is automatically populated for you.</param> | ||||||
| [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!"); | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.