@@ -148,43 +148,277 @@ The [synchronization and notification example](#synchronization-and-notification
148148The ` 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.
149149
150150``` csharp
151- public class Door : NetworkBehaviour
151+ using System .Runtime .CompilerServices ;
152+ using Unity .Netcode ;
153+ using UnityEngine ;
154+
155+ /// <summary >
156+ /// Example of using a <see cref =" NetworkVariable{T}" /> to drive changes
157+ /// in state.
158+ /// </summary >
159+ /// <remarks >
160+ /// This is a simple state driven door example.
161+ /// This script was written with recommended usages patterns in mind.
162+ /// </remarks >
163+ public class Door : NetworkBehaviour , INetworkUpdateSystem
152164{
153- public NetworkVariable <bool > State = new NetworkVariable <bool >();
165+ /// <summary >
166+ /// The two door states.
167+ /// </summary >
168+ public enum DoorStates
169+ {
170+ Closed ,
171+ Open
172+ }
173+
174+ /// <summary >
175+ /// Initializes the door to a specific state (server side) when first spawned.
176+ /// </summary >
177+ [Tooltip (" Configures the door's initial state when 1st spawned." )]
178+ public DoorStates InitialState = DoorStates .Closed ;
179+
180+ /// <summary >
181+ /// Used for <see cref =" CanPlayerToggleState" /> example purposes.
182+ /// When true, only the server can open and close the door.
183+ /// Clients will receive a console log saying they could not open the door.
184+ /// </summary >
185+ public bool IsLocked ;
186+
187+ /// <summary >
188+ /// A simple door state where the server has write permissions and everyone has read permissions.
189+ /// </summary >
190+ private NetworkVariable <DoorStates > m_State = new NetworkVariable <DoorStates >(default , NetworkVariableReadPermission .Everyone , NetworkVariableWritePermission .Server );
191+
192+ /// <summary >
193+ /// The current state of the door.
194+ /// </summary >
195+ public DoorStates CurrentState => m_State .Value ;
154196
197+ /// <summary >
198+ /// Invoked while the <see cref =" NetworkObject" /> is in the process of
199+ /// being spawned.
200+ /// </summary >
155201 public override void OnNetworkSpawn ()
156202 {
157- State .OnValueChanged += OnStateChanged ;
203+ // The write authority (server) doesn't need to know about its
204+ // own changes (for this example) since it's the "single point
205+ // of truth" for the door instance.
206+ if (IsServer )
207+ {
208+ // Host/Server:
209+ // Applies the configurable state upon spawning.
210+ m_State .Value = InitialState ;
211+ }
212+ else
213+ {
214+ // Clients:
215+ // Subscribe to changes in the door's state.
216+ m_State .OnValueChanged += OnStateChanged ;
217+ }
158218 }
159219
160- public override void OnNetworkDespawn ()
220+ /// <summary >
221+ /// Invoked once the door and all associated components
222+ /// have finished the spawn process.
223+ /// </summary >
224+ protected override void OnNetworkPostSpawn ()
225+ {
226+ // Everyone updates their door state when finished spawning the door
227+ // to ensure the door reflects (visually) its current state.
228+ UpdateFromState ();
229+
230+ // Begin updating this NetworkBehaviour instance once all
231+ // netcode related components have finished the spawn process.
232+ NetworkUpdateLoop .RegisterNetworkUpdate (this , NetworkUpdateStage .Update );
233+ base .OnNetworkPostSpawn ();
234+ }
235+
236+ /// <summary >
237+ /// Example of using the <see cref =" INetworkUpdateSystem" /> usage pattern
238+ /// where it only updates while spawned.
239+ /// </summary >
240+ /// <param name =" updateStage" >The current update stage being invoked.</param >
241+ public void NetworkUpdate (NetworkUpdateStage updateStage )
242+ {
243+ switch (updateStage )
244+ {
245+ case NetworkUpdateStage .Update :
246+ {
247+ if (Input .GetKeyDown (KeyCode .Space ))
248+ {
249+ Interact ();
250+ }
251+ break ;
252+ }
253+ }
254+ }
255+
256+ /// <summary >
257+ /// Invoked just before this instance runs through its despawn
258+ /// sequence. A good time to unsubscribe from things.
259+ /// </summary >
260+ public override void OnNetworkPreDespawn ()
261+ {
262+ if (! IsServer )
263+ {
264+ m_State .OnValueChanged -= OnStateChanged ;
265+ }
266+
267+ // Stop updating this NetworkBehaviour instance prior to running
268+ // through the despawn process.
269+ NetworkUpdateLoop .RegisterNetworkUpdate (this , NetworkUpdateStage .Update );
270+ base .OnNetworkPreDespawn ();
271+ }
272+
273+ /// <summary >
274+ /// Server makes changes to the state.
275+ /// Clients receive the changes in state.
276+ /// </summary >
277+ /// <remarks >
278+ /// When the previous state equals the current state, we are a client
279+ /// that is doing its first synchronization of this door instance.
280+ /// </remarks >
281+ /// <param name =" previous" >The previous <see cref =" DoorStates" /> state.</param >
282+ /// <param name =" current" >The current <see cref =" DoorStates" /> state.</param >
283+ public void OnStateChanged (DoorStates previous , DoorStates current )
284+ {
285+ UpdateFromState ();
286+ }
287+
288+ /// <summary >
289+ /// Invoke when the state is updated to apply the change
290+ /// in door state to the door asset itself.
291+ /// </summary >
292+ private void UpdateFromState ()
293+ {
294+ switch (m_State .Value )
295+ {
296+ case DoorStates .Closed :
297+ {
298+ // door is open:
299+ // - rotate door transform
300+ // - play animations, sound etc.
301+ // / <see cref="Netcode.Components.Helpers.ComponentCont"
302+ break ;
303+ }
304+ case DoorStates .Open :
305+ {
306+ // door is closed:
307+ // - rotate door transform
308+ // - play animations, sound etc.
309+ break ;
310+ }
311+ }
312+ Debug .Log ($" [{name }] Door is currently {m_State .Value }." );
313+ }
314+
315+ /// <summary >
316+ /// Override to apply specific checks (like a player having the right
317+ /// key to open the door) or make it a non-virtual class and add logic
318+ /// directly to this method.
319+ /// </summary >
320+ /// <param name =" player" >The player attempting to open the door.</param >
321+ /// <returns ></returns >
322+ protected virtual bool CanPlayerToggleState (NetworkObject player )
161323 {
162- State .OnValueChanged -= OnStateChanged ;
324+ // For this example, if the door "is locked" then clients will
325+ // not be able to open the door but the host-client's player can.
326+ return ! IsLocked || player .IsOwnedByServer ;
163327 }
164328
165- public void OnStateChanged (bool previous , bool current )
329+ /// <summary >
330+ /// Invoked by either a host or clients to interact with the door.
331+ /// </summary >
332+ public void Interact ()
166333 {
167- // note: `State.Value` will be equal to `current` here
168- if (State .Value )
334+ // Optional:
335+ // This is only if you want clients to be able to
336+ // interact with doors. A dedicated server would not
337+ // be able to do this since it does not have a player.
338+ if (IsServer && ! IsHost )
169339 {
170- // door is open:
171- // - rotate door transform
172- // - play animations, sound etc.
340+ // Optional to log a warning about this.
341+ return ;
342+ }
343+
344+ if (IsHost )
345+ {
346+ ToggleState (NetworkManager .LocalClientId );
173347 }
174348 else
175349 {
176- // door is closed:
177- // - rotate door transform
178- // - play animations, sound etc.
350+ // Clients send an RPC to server (write authority) who applies the
351+ // change in state that will be synchronized with all client observers.
352+ ToggleStateRpc ();
179353 }
180354 }
181355
182- [Rpc (SendTo .Server )]
183- public void ToggleStateRpc ()
356+ [MethodImpl (MethodImplOptions .AggressiveInlining )]
357+ private DoorStates NextToggleState ()
358+ {
359+ return m_State .Value == DoorStates .Open ? DoorStates .Closed : DoorStates .Open ;
360+ }
361+
362+ /// <summary >
363+ /// Invoked only server-side
364+ /// Primary method to handle toggling the door state.
365+ /// </summary >
366+ /// <param name =" clientId" >The client toggling the door state.</param >
367+ private void ToggleState (ulong clientId )
368+ {
369+ // Get the server-side client player instance
370+ var playerObject = NetworkManager .SpawnManager .GetPlayerNetworkObject (clientId );
371+ if (playerObject != null )
372+ {
373+ var nextToggleState = NextToggleState ();
374+ if (CanPlayerToggleState (playerObject ))
375+ {
376+ // Host toggles the state
377+ m_State .Value = nextToggleState ;
378+ UpdateFromState ();
379+ }
380+ else
381+ {
382+ ToggleStateFailRpc (nextToggleState , RpcTarget .Single (clientId , RpcTargetUse .Temp ));
383+ }
384+ }
385+ else
386+ {
387+ // Optional as to how you handle this. Since ToggleState is only invoked by
388+ // sever-side only script, this could mean many things depending upon whether
389+ // or not a client could interact with something and not have a player object.
390+ // If that is the case, then don't even bother checking for a player object.
391+ // If that is not the case, then there could be a timing issue between when
392+ // something can be "interacted with" and when a player is about to be de-spawned.
393+ // For this example, we just log a warning as this example was built with
394+ // the requirement that a client has a spawned player object that is used for
395+ // reference to determine if the client's player can toggle the state of the
396+ // door or not.
397+ NetworkLog .LogWarningServer ($" Client-{clientId } has no spawned player object!" );
398+ }
399+ }
400+
401+ /// <summary >
402+ /// Invoked by clients.
403+ /// Re-directs to the common <see cref =" ToggleState(ulong)" /> method.
404+ /// </summary >
405+ /// <param name =" rpcParams" >includes <see cref =" RpcReceiveParams.SenderClientId" /> that is automatically populated for you.</param >
406+ [Rpc (SendTo .Server , InvokePermission = RpcInvokePermission .Everyone )]
407+ private void ToggleStateRpc (RpcParams rpcParams = default )
408+ {
409+ ToggleState (rpcParams .Receive .SenderClientId );
410+ }
411+
412+ /// <summary >
413+ /// Optional:
414+ /// Handling when a player cannot open a door.
415+ /// </summary >
416+ /// <param name =" rpcParams" >includes <see cref =" RpcReceiveParams.SenderClientId" /> that is automatically populated for you.</param >
417+ [Rpc (SendTo .SpecifiedInParams , InvokePermission = RpcInvokePermission .Server )]
418+ private void ToggleStateFailRpc (DoorStates doorState , RpcParams rpcParams = default )
184419 {
185- // this will cause a replication over the network
186- // and ultimately invoke `OnValueChanged` on receivers
187- State .Value = ! State .Value ;
420+ // Provide player feedback that toggling failed.
421+ Debug .Log ($" Failed to {doorState } the door!" );
188422 }
189423}
190424```
0 commit comments