Skip to content

Commit 4cbec9b

Browse files
committed
Merge branch 'develop-2.0.0' into chore/fast-enter-playmode
2 parents 638855f + 916b47b commit 4cbec9b

9 files changed

Lines changed: 393 additions & 15 deletions

File tree

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
using System.Runtime.CompilerServices;
2+
using Unity.Netcode;
3+
using UnityEngine;
4+
5+
/// <summary>
6+
/// Example of using a <see cref="NetworkVariable{T}"/> to drive changes
7+
/// in state.
8+
/// </summary>
9+
/// <remarks>
10+
/// This is a simple state driven door example.
11+
/// This script was written with recommended usages patterns in mind.
12+
/// </remarks>
13+
public class Door : NetworkBehaviour, INetworkUpdateSystem
14+
{
15+
/// <summary>
16+
/// The two door states.
17+
/// </summary>
18+
public enum DoorStates
19+
{
20+
Closed,
21+
Open
22+
}
23+
24+
/// <summary>
25+
/// Initializes the door to a specific state (server side) when first spawned.
26+
/// </summary>
27+
[Tooltip("Configures the door's initial state when 1st spawned.")]
28+
public DoorStates InitialState = DoorStates.Closed;
29+
30+
/// <summary>
31+
/// Used for <see cref="CanPlayerToggleState"/> example purposes.
32+
/// When true, only the server can open and close the door.
33+
/// Clients will receive a console log saying they could not open the door.
34+
/// </summary>
35+
public bool IsLocked;
36+
37+
/// <summary>
38+
/// A simple door state where the server has write permissions and everyone has read permissions.
39+
/// </summary>
40+
private NetworkVariable<DoorStates> m_State = new NetworkVariable<DoorStates>(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
41+
42+
/// <summary>
43+
/// The current state of the door.
44+
/// </summary>
45+
public DoorStates CurrentState => m_State.Value;
46+
47+
/// <summary>
48+
/// Invoked while the <see cref="NetworkObject"/> is in the middle of
49+
/// being spawned.
50+
/// </summary>
51+
public override void OnNetworkSpawn()
52+
{
53+
// The write authority (server) does not need to know about its
54+
// own changes (for this example) since it is the "single point
55+
// of truth" for the door instance.
56+
if (IsServer)
57+
{
58+
// Host/Server:
59+
// Applies the configurable state upon spawning.
60+
m_State.Value = InitialState;
61+
}
62+
else
63+
{
64+
// Clients:
65+
// Subscribe to changes in the door's state.
66+
m_State.OnValueChanged += OnStateChanged;
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Invoked once the door and all associated components
72+
/// have finished the spawn process.
73+
/// </summary>
74+
protected override void OnNetworkPostSpawn()
75+
{
76+
// Everyone updates their door state when finished spawning the door
77+
// in order to assure the door reflects (visually) its current state.
78+
UpdateFromState();
79+
80+
// Begin to start updating this NetworkBehaviour instance once all
81+
// netcode related components have finished the spawn process.
82+
NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update);
83+
base.OnNetworkPostSpawn();
84+
}
85+
86+
/// <summary>
87+
/// Example of using the <see cref="INetworkUpdateSystem"/> usage pattern
88+
/// where it only updates while spawned.
89+
/// </summary>
90+
/// <param name="updateStage">The current update stage being invoked.</param>
91+
public void NetworkUpdate(NetworkUpdateStage updateStage)
92+
{
93+
switch (updateStage)
94+
{
95+
case NetworkUpdateStage.Update:
96+
{
97+
if (Input.GetKeyDown(KeyCode.Space))
98+
{
99+
Interact();
100+
}
101+
break;
102+
}
103+
}
104+
}
105+
106+
/// <summary>
107+
/// Invoked just before this instance runs through its de-spawn
108+
/// sequence. A good time to unsubscribe from things.
109+
/// </summary>
110+
public override void OnNetworkPreDespawn()
111+
{
112+
if (!IsServer)
113+
{
114+
m_State.OnValueChanged -= OnStateChanged;
115+
}
116+
117+
// Stop updating this NetworkBehaviour instance prior to running
118+
// through the de-spawn process.
119+
NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update);
120+
base.OnNetworkPreDespawn();
121+
}
122+
123+
/// <summary>
124+
/// Server makes changes to the state.
125+
/// Clients receive the changes in state.
126+
/// </summary>
127+
/// <remarks>
128+
/// When the previous state equals the current state, we are a client
129+
/// that is doing its 1st synchronization of this door instance.
130+
/// </remarks>
131+
/// <param name="previous">The previous <see cref="DoorStates"/> state.</param>
132+
/// <param name="current">The current <see cref="DoorStates"/> state.</param>
133+
public void OnStateChanged(DoorStates previous, DoorStates current)
134+
{
135+
UpdateFromState();
136+
}
137+
138+
/// <summary>
139+
/// Invoke when the state is updated in order to apply the change
140+
/// in door state to the door asset itself.
141+
/// </summary>
142+
private void UpdateFromState()
143+
{
144+
switch(m_State.Value)
145+
{
146+
case DoorStates.Closed:
147+
{
148+
// door is open:
149+
// - rotate door transform
150+
// - play animations, sound etc.
151+
/// <see cref="Netcode.Components.Helpers.ComponentCont"
152+
break;
153+
}
154+
case DoorStates.Open:
155+
{
156+
// door is closed:
157+
// - rotate door transform
158+
// - play animations, sound etc.
159+
break;
160+
}
161+
}
162+
Debug.Log($"[{name}] Door is currently {m_State.Value}.");
163+
}
164+
165+
/// <summary>
166+
/// Override to apply specific checks (like a player having the right
167+
/// key to open the door) or make it a non-virtual class and add logic
168+
/// directly to this method.
169+
/// </summary>
170+
/// <param name="player">The player attempting to open the door.</param>
171+
/// <returns></returns>
172+
protected virtual bool CanPlayerToggleState(NetworkObject player)
173+
{
174+
// For this example, if the door "is locked" then clients will
175+
// not be able to open the door but the host-client's player can.
176+
return !IsLocked || player.IsOwnedByServer;
177+
}
178+
179+
/// <summary>
180+
/// Invoked by either a Host or clients to interact with the door.
181+
/// </summary>
182+
public void Interact()
183+
{
184+
// Optional:
185+
// This is only if you want clients to be able to
186+
// interact with doors. A dedicated server would not
187+
// be able to do this since it does not have a player.
188+
if (IsServer && !IsHost)
189+
{
190+
// Optional to log a warning about this.
191+
return;
192+
}
193+
194+
if (IsHost)
195+
{
196+
ToggleState(NetworkManager.LocalClientId);
197+
}
198+
else
199+
{
200+
// Clients send an RPC to server (write authority) who applies the
201+
// change in state that will be synchronized with all client observers.
202+
ToggleStateRpc();
203+
}
204+
}
205+
206+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
207+
private DoorStates NextToggleState()
208+
{
209+
return m_State.Value == DoorStates.Open ? DoorStates.Closed : DoorStates.Open;
210+
}
211+
212+
/// <summary>
213+
/// Invoked only server-side
214+
/// Primary method to handle toggling the door state.
215+
/// </summary>
216+
/// <param name="clientId">The client toggling the door state.</param>
217+
private void ToggleState(ulong clientId)
218+
{
219+
// Get the server-side client player instance
220+
var playerObject = NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId);
221+
if (playerObject != null)
222+
{
223+
var nextToggleState = NextToggleState();
224+
if (CanPlayerToggleState(playerObject))
225+
{
226+
// Host toggles the state
227+
m_State.Value = nextToggleState;
228+
UpdateFromState();
229+
}
230+
else
231+
{
232+
ToggleStateFailRpc(nextToggleState, RpcTarget.Single(clientId, RpcTargetUse.Temp));
233+
}
234+
}
235+
else
236+
{
237+
// Optional as to how you handle this. Since ToggleState is only invoked by
238+
// sever-side only script, this could mean many things depending upon whether
239+
// or not a client could interact with something and not have a player object.
240+
// If that is the case, then don't even bother checking for a player object.
241+
// If that is not the case, then there could be a timing issue between when
242+
// something can be "interacted with" and when a player is about to be de-spawned.
243+
// For this example, we just log a warning as this example was built with
244+
// the requirement that a client has a spawned player object that is used for
245+
// reference to determine if the client's player can toggle the state of the
246+
// door or not.
247+
NetworkLog.LogWarningServer($"Client-{clientId} has no spawned player object!");
248+
}
249+
}
250+
251+
/// <summary>
252+
/// Invoked by clients.
253+
/// Re-directs to the common <see cref="ToggleState(ulong)"/> method.
254+
/// </summary>
255+
/// <param name="rpcParams">includes <see cref="RpcReceiveParams.SenderClientId"/> that is automatically populated for you.</param>
256+
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
257+
private void ToggleStateRpc(RpcParams rpcParams = default)
258+
{
259+
ToggleState(rpcParams.Receive.SenderClientId);
260+
}
261+
262+
/// <summary>
263+
/// Optional:
264+
/// Handling when a player cannot open a door.
265+
/// </summary>
266+
/// <param name="rpcParams">includes <see cref="RpcReceiveParams.SenderClientId"/> that is automatically populated for you.</param>
267+
[Rpc(SendTo.SpecifiedInParams, InvokePermission = RpcInvokePermission.Server)]
268+
private void ToggleStateFailRpc(DoorStates doorState, RpcParams rpcParams = default)
269+
{
270+
// Provide player feedback that toggling failed.
271+
Debug.Log($"Failed to {doorState} the door!");
272+
}
273+
}

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ Additional documentation and release notes are available at [Multiplayer Documen
2929
### Obsolete
3030

3131

32+
### [2.11.2] - 2026-05-01
33+
34+
### Fixed
35+
36+
- Fixed issue where if the `NetworkManager` player prefab is not assigned an exception is thrown upon starting a host and/or when a client joins. (#3965)
37+
3238
## [2.11.1] - 2026-04-26
3339

3440
### Changed

com.unity.netcode.gameobjects/Documentation~/advanced-topics/network-update-loop-system/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ After injection, the player loops follows these stages. The player loop executes
2323

2424
In all `NetworkUpdateStages`, it iterates over an array and calls the `NetworkUpdate` method over `INetworkUpdateSystem` interface, and the pattern is repeated.
2525

26-
<Mermaid chart={`
26+
```mermaid
2727
graph LR;
2828
A(Initialization)
2929
B(EarlyUpdate)
@@ -33,7 +33,7 @@ In all `NetworkUpdateStages`, it iterates over an array and calls the `NetworkUp
3333
F(PreLateUpdate)
3434
G(PostLateUpdate)
3535
A --> B --> C --> D --> E --> F --> G
36-
`}/>
36+
```
3737

3838
| Stage | Method |
3939
| -- | -- |

com.unity.netcode.gameobjects/Documentation~/advanced-topics/networktime-ticks.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ Netcode for GameObjects (Netcode) uses a star topology. That means all communica
99
- `LocalTime` on a client is ahead of the server. If a server RPC is sent at `LocalTime` from a client it will roughly arrive at `ServerTime` on the server.
1010
- `ServerTime` on clients is behind the server. If a client RPC is sent at `ServerTime` from the server to clients it will roughly arrive at `ServerTime` on the clients.
1111

12-
13-
14-
<Mermaid chart={`
12+
```mermaid
1513
sequenceDiagram
1614
participant Owner as Client LocalTime
1715
participant Server as Server ServerTime & LocalTime
@@ -23,10 +21,7 @@ sequenceDiagram
2321
Note over Server: Send message to clients at LocalTime.
2422
Server->>Receiver: Delay when sending message
2523
Note over Receiver: Message arrives at ServerTime.
26-
`}/>
27-
28-
29-
24+
```
3025

3126
`LocalTime`
3227
- Use for player objects with client authority.
@@ -115,7 +110,7 @@ public class SyncedEventExample : NetworkBehaviour
115110
}
116111
```
117112

118-
<Mermaid chart={`
113+
```mermaid
119114
sequenceDiagram
120115
participant Owner as Owner
121116
participant Server as Server
@@ -133,7 +128,7 @@ sequenceDiagram
133128
Note over Receiver: StartCoroutine(WaitAndSpawnSyncedEffect(0.07))
134129
Receiver->>Receiver: WaitForSeconds(0.07);
135130
Note over Receiver: Instantiate effect at ServerTime = 10.0
136-
`}/>
131+
```
137132

138133
> [!NOTE]
139134
> Some components such as NetworkTransform add additional buffering. When trying to align an RPC event like in this example, an additional delay would need to be added.

com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,37 @@ internal void ProcessClientsToDisconnect()
946946
m_ClientsToDisconnect.Clear();
947947
}
948948

949+
/// <summary>
950+
/// The checks to find the right GlobalObjectIdHash value
951+
/// are complex enough to deserve a method that includes
952+
/// an easy to follow logical flow.
953+
/// This also makes it a quick check to determine if there
954+
/// even is a player prefab to spawn (it is valid to not
955+
/// have any player spawned upon connection).
956+
/// </summary>
957+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
958+
private (bool IsValid, uint GlobalObjectIdHash) GetPlayerPrefabHash(uint? playerPrefabHash)
959+
{
960+
if (playerPrefabHash != null && playerPrefabHash.HasValue)
961+
{
962+
return (true, playerPrefabHash.Value);
963+
}
964+
else
965+
if (NetworkManager.NetworkConfig.PlayerPrefab != null)
966+
{
967+
var networkObject = NetworkManager.NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>();
968+
if (networkObject != null)
969+
{
970+
return (true, networkObject.GlobalObjectIdHash);
971+
}
972+
else
973+
{
974+
NetworkManager.Log.Error(new Logging.Context(LogLevel.Error, $"Player prefab {NetworkManager.NetworkConfig.PlayerPrefab.name} has no {nameof(NetworkObject)}!"));
975+
}
976+
}
977+
return (false, 0);
978+
}
979+
949980
/// <summary>
950981
/// Server Side: Handles the approval of a client
951982
/// </summary>
@@ -972,10 +1003,10 @@ internal void HandleConnectionApproval(ulong ownerClientId, bool createPlayerObj
9721003
}
9731004

9741005
// Server-side spawning (only if there is a prefab hash or player prefab provided)
975-
var idHashToSpawn = playerPrefabHash ?? NetworkManager.NetworkConfig.PlayerPrefab?.GetComponent<NetworkObject>()?.GlobalObjectIdHash;
976-
if (!NetworkManager.DistributedAuthorityMode && createPlayerObject && idHashToSpawn.HasValue)
1006+
var idHashToSpawn = GetPlayerPrefabHash(playerPrefabHash);
1007+
if (!NetworkManager.DistributedAuthorityMode && createPlayerObject && idHashToSpawn.IsValid)
9771008
{
978-
var playerObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(idHashToSpawn.Value, ownerClientId, playerPosition, playerRotation);
1009+
var playerObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(idHashToSpawn.GlobalObjectIdHash, ownerClientId, playerPosition, playerRotation);
9791010

9801011
if (playerObject == null)
9811012
{

0 commit comments

Comments
 (0)