Skip to content

Commit 2ccaa37

Browse files
authored
Merge branch 'develop-2.0.0' into test-and-ci-check
2 parents ee7402a + cfa5789 commit 2ccaa37

1 file changed

Lines changed: 253 additions & 19 deletions

File tree

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

Lines changed: 253 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -148,43 +148,277 @@ 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-
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

Comments
 (0)