Skip to content

Commit 84b1aac

Browse files
update
Updating the door example to provide a more recommended way of using OnValueChanged.
1 parent 5a952c1 commit 84b1aac

1 file changed

Lines changed: 193 additions & 10 deletions

File tree

com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md

Lines changed: 193 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,43 +148,226 @@ The [synchronization and notification example](#synchronization-and-notification
148148
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.
149149

150150
```csharp
151+
/// <summary>
152+
/// A basic NetworkVariable driven door state example.
153+
/// </summary>
151154
public 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

Comments
 (0)