@@ -148,43 +148,226 @@ 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+ /// <summary >
152+ /// A basic NetworkVariable driven door state example.
153+ /// </summary >
151154public class Door : NetworkBehaviour
152155{
153- public NetworkVariable <bool > State = new NetworkVariable <bool >();
156+ /// <summary >
157+ /// Only for UI purposes.
158+ /// This provides an initial configuration state for the door.
159+ /// </summary >
160+ public enum DoorStates
161+ {
162+ Closed ,
163+ Open
164+ }
165+
166+ /// <summary >
167+ /// Initializes the door to a specific state (server side) when first spawned.
168+ /// </summary >
169+ [Tooltip (" Configures the door's initial state when 1st spawned." )]
170+ public DoorStates InitialState = DoorStates .Closed ;
171+
172+ /// <summary >
173+ /// A simple door state where the server has write permissions and everyone has read permissions.
174+ /// </summary >
175+ public NetworkVariable <bool > State = new NetworkVariable <bool >(default , NetworkVariableReadPermission .Everyone , NetworkVariableWritePermission .Server );
154176
177+ /// <summary >
178+ /// Invoked while the <see cref =" NetworkObject" /> is in the middle of
179+ /// being spawned.
180+ /// </summary >
155181 public override void OnNetworkSpawn ()
156182 {
157- State .OnValueChanged += OnStateChanged ;
183+ // The write authority (server) does not need to know about its
184+ // own changes (for this example) since it is the "single point
185+ // of truth" for the door instance.
186+ if (IsServer )
187+ {
188+ // Host/Server:
189+ // Applies the configurable state upon spawning.
190+ State .Value = InitialState == DoorStates .Open ;
191+ }
192+ else
193+ {
194+ // Clients:
195+ // Subscribe to changes in the door's state.
196+ State .OnValueChanged += OnStateChanged ;
197+ }
198+ }
199+
200+ /// <summary >
201+ /// Invoked once the door and all associated <see cref =" NetworkAnimator" />
202+ /// components have finished the spawn process.
203+ /// </summary >
204+ protected override void OnNetworkPostSpawn ()
205+ {
206+ // Everyone updates their door state when finished spawning the door.
207+ UpdateFromState ();
208+ base .OnNetworkPostSpawn ();
158209 }
159210
160- public override void OnNetworkDespawn ()
211+ /// <summary >
212+ /// Invoked just before this instance runs through its de-spawn
213+ /// sequence. A good time to unsubscribe from things.
214+ /// </summary >
215+ public override void OnNetworkPreDespawn ()
161216 {
162- State .OnValueChanged -= OnStateChanged ;
217+ if (! IsServer )
218+ {
219+ State .OnValueChanged -= OnStateChanged ;
220+ }
221+ base .OnNetworkPreDespawn ();
163222 }
164223
224+ /// <summary >
225+ /// Only clients invoke this.
226+ /// Server makes changes to the state.
227+ /// </summary >
228+ /// <remarks >
229+ /// When the previous state equals the current state, we are a client
230+ /// that is doing its 1st synchronization of this door instance.
231+ /// </remarks >
232+ /// <param name =" previous" >The previous state.</param >
233+ /// <param name =" current" >The current state.</param >
165234 public void OnStateChanged (bool previous , bool current )
166235 {
167- // note: `State.Value` will be equal to `current` here
236+ // Update to the current state while also providing a catch for
237+ // the first synchronization where previous == current.
238+ UpdateFromState (previous != current );
239+ }
240+
241+ /// <summary >
242+ /// Common method used to update the actual door asset based on its current state.
243+ /// </summary >
244+ /// <param name =" isFirstSynchronization" >only set upon first spawn by a client</param >
245+ private void UpdateFromState (bool isFirstSynchronization = false )
246+ {
168247 if (State .Value )
169248 {
170249 // door is open:
171250 // - rotate door transform
172251 // - play animations, sound etc.
252+ // if first sync, reset to open and don't play sound
173253 }
174254 else
175255 {
176256 // door is closed:
177257 // - rotate door transform
178258 // - play animations, sound etc.
259+ // if first sync, reset to closed and don't play sound
260+ }
261+
262+ // If 1st sync, don't log a message about a change in state
263+ // since previous == current (i.e. no change in state)
264+ if (! isFirstSynchronization )
265+ {
266+ var openClosed = State .Value ? " open" : " closed" ;
267+ Debug .Log ($" []The door is now {openClosed }." );
268+ }
269+ }
270+
271+ /// <summary >
272+ /// Override to apply specific checks (like a player having the right
273+ /// key to open the door) or make it a non-virtual class and add logic
274+ /// directly to this method.
275+ /// </summary >
276+ /// <param name =" player" >The player attempting to open the door.</param >
277+ /// <returns ></returns >
278+ protected virtual bool CanPlayerToggleState (NetworkObject player )
279+ {
280+ // For this example, the door can always be toggled.
281+ return true ;
282+ }
283+
284+ /// <summary >
285+ /// Invoked by either a Host or clients to interact with the door.
286+ /// </summary >
287+ public void Interact ()
288+ {
289+ // Optional:
290+ // This is only if you want clients to be able to
291+ // interact with doors. A dedicated server would not
292+ // be able to do this since it does not have a player.
293+ if (IsServer && ! IsHost )
294+ {
295+ // Optional to log a warning about this.
296+ return ;
297+ }
298+
299+ if (IsHost )
300+ {
301+ ToggleState (NetworkManager .LocalClientId );
302+ }
303+ else
304+ {
305+ // Clients send an RPC to server (write authority) who applies the
306+ // change in state that will be synchronized with all client observers.
307+ ToggleStateRpc ();
308+ }
309+ }
310+
311+ /// <summary >
312+ /// Invoked only server-side
313+ /// Primary method to handle toggling the door state.
314+ /// </summary >
315+ /// <param name =" clientId" >The client toggling the door state.</param >
316+ private void ToggleState (ulong clientId )
317+ {
318+ // Get the server-side client player instance
319+ var playerObject = NetworkManager .SpawnManager .GetPlayerNetworkObject (clientId );
320+ if (playerObject != null )
321+ {
322+ if (CanPlayerToggleState (playerObject ))
323+ {
324+ // Host toggles the state
325+ State .Value = ! State .Value ;
326+ UpdateFromState ();
327+ }
328+ else
329+ {
330+ ToggleStateFailRpc (RpcTarget .Single (clientId , RpcTargetUse .Temp ));
331+ }
332+ }
333+ else
334+ {
335+ // Optional as to how you handle this. Since ToggleState is only invoked by
336+ // sever-side only script, this could mean many things depending upon whether
337+ // or not a client could interact with something and not have a player object.
338+ // If that is the case, then don't even bother checking for a player object.
339+ // If that is not the case, then there could be a timing issue between when
340+ // something can be "interacted with" and when a player is about to be de-spawned.
341+ // For this example, we just log a warning as this example was built with
342+ // the requirement that a client has a spawned player object that is used for
343+ // reference to determine if the client's player can toggle the state of the
344+ // door or not.
345+ NetworkLog .LogWarningServer ($" Client-{clientId } has no spawned player object!" );
179346 }
180347 }
181348
182- [Rpc (SendTo .Server )]
183- public void ToggleStateRpc ()
349+ /// <summary >
350+ /// Invoked by clients.
351+ /// Re-directs to the common <see cref =" ToggleState(ulong)" /> method.
352+ /// </summary >
353+ /// <param name =" rpcParams" >includes <see cref =" RpcReceiveParams.SenderClientId" /> that is automatically populated for you.</param >
354+ [Rpc (SendTo .Server , InvokePermission = RpcInvokePermission .Everyone )]
355+ private void ToggleStateRpc (RpcParams rpcParams = default )
356+ {
357+ ToggleState (rpcParams .Receive .SenderClientId );
358+ }
359+
360+ /// <summary >
361+ /// Optional:
362+ /// Handling when a player cannot open a door.
363+ /// </summary >
364+ /// <param name =" rpcParams" >includes <see cref =" RpcReceiveParams.SenderClientId" /> that is automatically populated for you.</param >
365+ [Rpc (SendTo .SpecifiedInParams , InvokePermission = RpcInvokePermission .Server )]
366+ private void ToggleStateFailRpc (RpcParams rpcParams = default )
184367 {
185- // this will cause a replication over the network
186- // and ultimately invoke `OnValueChanged` on receivers
187- State . Value = ! State . Value ;
368+ // Provide player feedback that toggling failed.
369+ var openOrClose = State . Value ? " close " : " open " ;
370+ Debug . Log ( $" Failed to { openOrClose } the door! " ) ;
188371 }
189372}
190373```
0 commit comments