Skip to content

Commit 1495955

Browse files
fix: networkmanager cannot be started within OnClientStopped or OnServerStopped callbacks (#3908)
* fix Fixing the issue where users might try to start NetworkManager within OnClientStopped or OnServerStopped which would result in a failed start. * test Adding test that verifies a NetworkManager can be started when OnClientStopped is invoked and that OnServerStopped is not invoked if the NetworkManager is started again during an OnClientStopped invocation (i.e. host). * fix Removing model: rtx2080 from our yamato vm definition file. * update - docs & changelog Adding additional information about some events we have added but do not cover in NetworkManager. Added additional information about starting NetworkManager within OnClientStopped or OnServerStopped. Adding changelog entry. * Apply suggestions from code review Co-authored-by: Amy Reeve <amy.reeve@unity3d.com> --------- Co-authored-by: Amy Reeve <amy.reeve@unity3d.com>
1 parent 47d7029 commit 1495955

File tree

7 files changed

+243
-19
lines changed

7 files changed

+243
-19
lines changed

.yamato/project.metafile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# smaller_flavor --> An override for flavor that determines the VM size/resources for lighter weight jobs that can run on a smaller vm
1313
# larger_flavor --> An override for flavor that determines the VM size/resources for heavier weight jobs that can need a bigger vm
1414
# standalone --> Specifies the build target platform (e.g., StandaloneLinux64, Android, IOS)
15-
# model --> Defines specific hardware model requirements (e.g., rtx2080, iPhone model 13). Notice that trunk currently (19.08.2025) has 13.0 as minimal iOS version which devices below this are not supporting
15+
# model --> Defines specific hardware model requirements (e.g., iPhone model 13). Notice that trunk currently (19.08.2025) has 13.0 as minimal iOS version which devices below this are not supporting
1616
# base --> Indicates the base operating system for build operations (e.g., win, mac)
1717
# architecture --> Specifies the target CPU architecture (e.g., armv7, arm64)
1818

@@ -50,15 +50,13 @@ test_platforms:
5050
smaller_flavor: b1.medium
5151
larger_flavor: b1.xlarge
5252
standalone: StandaloneLinux64
53-
model: rtx2080
5453
- name: win
5554
type: Unity::VM
5655
image: package-ci/win10:v4
5756
flavor: b1.large
5857
smaller_flavor: b1.medium
5958
larger_flavor: b1.xlarge
6059
standalone: StandaloneWindows64
61-
model: rtx2080
6260
- name: mac
6361
type: Unity::VM::osx
6462
image: package-ci/macos-13-arm64:v4 # ARM64 to support M1 model (below)

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
4141

4242
### Fixed
4343

44+
- Fixed issue where starting the NetworkManager within `OnClientStopped` or `OnServerStopped` resulted in a broken `NetworkManager` state. (#3908)
4445
- Fixed issue where an attachable could log an error upon being de-spawned during shutdown. (#3895)
4546
- NestedNetworkVariables initialized with no value no longer throw an error. (#3891)
4647
- Fixed `NetworkShow` behavior when it is called twice. (#3867)

com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,6 @@ Subscribe to the `NetworkManager.OnClientDisconnectCallback` event to receive no
169169
- On the client-side, the client identifier parameter is the identifier assigned to the client.
170170
- _The exception to this is when a client is disconnected before its connection is approved._
171171

172-
You can also use the `NetworkManager.OnServerStopped` and `NetworkManager.OnClientStopped` callbacks to get local notifications when the server or client stops respectively.
173-
174172
### Connection notification manager example
175173

176174
Below is one example of how you can provide client connect and disconnect notifications to any type of NetworkBehaviour or MonoBehaviour derived component.
@@ -256,3 +254,33 @@ public class ConnectionNotificationManager : MonoBehaviour
256254
}
257255
}
258256
```
257+
258+
## Additional NetworkManager notifications
259+
260+
### Instantiation and destroying
261+
262+
There are two static NetworkManager events you can use to be notified when a NetworkManager is instantiated or is about to be destroyed:
263+
264+
- [`NetworkManager.OnInstantiated`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnInstantiated): This is invoked when a NetworkManager is instantiated.
265+
- [`NetworkManager.OnDestroying`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnDestroying): This is invoked when a NetworkManager is about to be destroyed.
266+
267+
### When a NetworkManager is stopped
268+
269+
270+
Knowing when a NetworkManager has stopped is useful for establishing when it's safe to transition back to a main menu scene, or other similar tasks. There are two events you can use to be notified that the NetworkManager has finished shutting down:
271+
272+
- [`NetworkManager.OnClientStopped`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnClientStopped): This is invoked on a host or client when the NetworkManager has completely shut down and is ready to be restarted.
273+
- [`NetworkManager.OnServerStopped`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnServerStopped): This is invoked on a host or server when the NetworkManager has completely shut down and is ready to be restarted.
274+
275+
Since a host is both a client and a server, the event invocation order is:
276+
277+
- `OnClientStopped`
278+
- `OnServerStopped`
279+
- _Only if the NetworkManager instance is not restarted during `OnClientStopped`_.
280+
281+
> [!NOTE]
282+
> If you restart the NetworkManager during `NetworkManager.OnClientStopped`, then it will skip the invocation of `OnServerStopped`.
283+
284+
If you need to save the state of spawned objects before they're destroyed when the NetworkManager shuts down, you can use the following event notification:
285+
286+
- [`NetworkManager.OnPreShutdown`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnPreShutdown): This is invoked prior to finalizing the NetworkManager shut down process. Any remaining spawned objects will still be instantiated and spawned when this event is invoked.

com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,19 +1674,8 @@ internal void ShutdownInternal()
16741674
ConnectionManager.InvokeOnClientDisconnectCallback(LocalClientId);
16751675
}
16761676

1677-
if (ConnectionManager.LocalClient.IsClient)
1678-
{
1679-
// If we were a client, we want to know if we were a host
1680-
// client or not. (why we pass in "IsServer")
1681-
OnClientStopped?.Invoke(ConnectionManager.LocalClient.IsServer);
1682-
}
1683-
1684-
if (ConnectionManager.LocalClient.IsServer)
1685-
{
1686-
// If we were a server, we want to know if we were a host
1687-
// or not. (why we pass in "IsClient")
1688-
OnServerStopped?.Invoke(ConnectionManager.LocalClient.IsClient);
1689-
}
1677+
// Save off the last local client settings
1678+
var localClient = ConnectionManager.LocalClient;
16901679

16911680
// In the event shutdown is invoked within OnClientStopped or OnServerStopped, set it to false again
16921681
m_ShuttingDown = false;
@@ -1706,6 +1695,21 @@ internal void ShutdownInternal()
17061695
// can unsubscribe from tick updates and such.
17071696
NetworkTimeSystem?.Shutdown();
17081697
NetworkTickSystem = null;
1698+
1699+
1700+
if (localClient.IsClient)
1701+
{
1702+
// If we were a client, we want to know if we were a host
1703+
// client or not. (why we pass in "IsServer")
1704+
OnClientStopped?.Invoke(localClient.IsServer);
1705+
}
1706+
1707+
if (localClient.IsServer)
1708+
{
1709+
// If we were a server, we want to know if we were a host
1710+
// or not. (why we pass in "IsClient")
1711+
OnServerStopped?.Invoke(localClient.IsClient);
1712+
}
17091713
}
17101714

17111715
// Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application.

com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1269,7 +1269,7 @@ internal unsafe void UpdateNetworkObjectSceneChanges()
12691269
foreach (var entry in NetworkObjectsToSynchronizeSceneChanges)
12701270
{
12711271
// If it fails the first update then don't add for updates
1272-
if (!entry.Value.UpdateForSceneChanges())
1272+
if (entry.Value != null && !entry.Value.UpdateForSceneChanges())
12731273
{
12741274
CleanUpDisposedObjects.Push(entry.Key);
12751275
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using NUnit.Framework;
6+
using Unity.Netcode.TestHelpers.Runtime;
7+
using UnityEngine.TestTools;
8+
9+
namespace Unity.Netcode.RuntimeTests
10+
{
11+
[TestFixture(NetworkTopologyTypes.ClientServer)]
12+
internal class NetworkManagerStartStopTests : NetcodeIntegrationTest
13+
{
14+
private const int k_NumberOfSessions = 5;
15+
protected override int NumberOfClients => 2;
16+
private OnClientStoppedHandler m_StoppedHandler;
17+
private int m_ExpectedNumberOfClients = 0;
18+
public NetworkManagerStartStopTests(NetworkTopologyTypes networkTopologyType) : base(networkTopologyType, HostOrServer.Host) { }
19+
20+
/// <summary>
21+
/// This test will not work with the CMB service since it requires the service
22+
/// to remain active after all clients have disconnected.
23+
/// </summary>
24+
protected override bool UseCMBService()
25+
{
26+
return false;
27+
}
28+
29+
private void ShutdownIfListening()
30+
{
31+
var networkManager = m_StoppedHandler.NetworkManager;
32+
if (networkManager.IsListening)
33+
{
34+
m_StoppedHandler.NetworkManager.Shutdown();
35+
}
36+
}
37+
38+
private bool NetworkManagerCompletedSessionCount(StringBuilder errorLog)
39+
{
40+
// Once the session count is decremented to zero the condition has been met.
41+
if (m_StoppedHandler.SessionCount != 0)
42+
{
43+
// If we are a host, then only shutdown once all clients have reconnected
44+
if (m_StoppedHandler.IsSessionAuthority && m_StoppedHandler.NetworkManager.ConnectedClientsIds.Count != m_ExpectedNumberOfClients)
45+
{
46+
errorLog.Append($"[{m_StoppedHandler.NetworkManager.name}] Waiting for {m_ExpectedNumberOfClients} clients to connect but there are only {m_StoppedHandler.NetworkManager.ConnectedClientsIds.Count} connected!");
47+
return false;
48+
}
49+
ShutdownIfListening();
50+
errorLog.Append($"[{m_StoppedHandler.NetworkManager.name}] Still has a session count of {m_StoppedHandler.SessionCount}!");
51+
}
52+
return errorLog.Length == 0;
53+
}
54+
55+
[UnityTest]
56+
public IEnumerator StartFromWithinOnClientStopped()
57+
{
58+
var authority = GetAuthorityNetworkManager();
59+
m_ExpectedNumberOfClients = authority.ConnectedClientsIds.Count;
60+
61+
// Validate a client can disconnect and immediately reconnect from within OnClientStopped
62+
m_StoppedHandler = new OnClientStoppedHandler(k_NumberOfSessions, GetNonAuthorityNetworkManager());
63+
ShutdownIfListening();
64+
yield return WaitForConditionOrTimeOut(NetworkManagerCompletedSessionCount);
65+
AssertOnTimeout($"Not all {nameof(NetworkManager)} instances finished their sessions!");
66+
67+
// Validate a host can disconnect and immediately reconnect from within OnClientStopped
68+
m_StoppedHandler = new OnHostStoppedHandler(k_NumberOfSessions, authority, m_NetworkManagers.ToList());
69+
ShutdownIfListening();
70+
yield return WaitForConditionOrTimeOut(NetworkManagerCompletedSessionCount);
71+
AssertOnTimeout($"Not all {nameof(NetworkManager)} instances finished their sessions!");
72+
73+
// Verify OnServerStopped is not invoked if NetworkManager is started again within OnClientStopped (it should not invoke if it is listening).
74+
Assert.False((m_StoppedHandler as OnHostStoppedHandler).OnServerStoppedInvoked, $"{nameof(NetworkManager.OnServerStopped)} was invoked when it should not have been invoked!");
75+
}
76+
}
77+
78+
internal class OnHostStoppedHandler : OnClientStoppedHandler
79+
{
80+
public bool OnServerStoppedInvoked = false;
81+
82+
private List<NetworkManager> m_Clients = new List<NetworkManager>();
83+
84+
private Networking.Transport.NetworkEndpoint m_Endpoint;
85+
86+
protected override void OnClientStopped(bool wasHost)
87+
{
88+
m_Endpoint.Port++;
89+
var unityTransport = (Transports.UTP.UnityTransport)NetworkManager.NetworkConfig.NetworkTransport;
90+
unityTransport.SetConnectionData(m_Endpoint);
91+
// Make sure all clients are shutdown or shutting down
92+
foreach (var networkManager in m_Clients)
93+
{
94+
if (networkManager.IsListening && !networkManager.ShutdownInProgress)
95+
{
96+
networkManager.Shutdown();
97+
}
98+
}
99+
100+
base.OnClientStopped(wasHost);
101+
if (SessionCount != 0)
102+
{
103+
NetworkManager.StartCoroutine(StartClients());
104+
}
105+
106+
}
107+
108+
private IEnumerator StartClients()
109+
{
110+
var nextPhase = false;
111+
var timeout = UnityEngine.Time.realtimeSinceStartup + 5.0f;
112+
while (!nextPhase)
113+
{
114+
if (!nextPhase && timeout < UnityEngine.Time.realtimeSinceStartup)
115+
{
116+
Assert.Fail($"Timed out waiting for all {nameof(NetworkManager)} instances to shutdown!");
117+
yield break;
118+
}
119+
120+
nextPhase = true;
121+
foreach (var networkManager in m_Clients)
122+
{
123+
if (networkManager.ShutdownInProgress || networkManager.IsListening)
124+
{
125+
nextPhase = false;
126+
}
127+
}
128+
yield return null;
129+
}
130+
131+
// Now, start all of the clients and have them connect again
132+
foreach (var networkManager in m_Clients)
133+
{
134+
var unityTransport = (Transports.UTP.UnityTransport)networkManager.NetworkConfig.NetworkTransport;
135+
unityTransport.SetConnectionData(m_Endpoint);
136+
networkManager.StartClient();
137+
}
138+
}
139+
140+
public OnHostStoppedHandler(int numberOfSessions, NetworkManager authority, List<NetworkManager> networkManagers) : base(numberOfSessions, authority)
141+
{
142+
m_Endpoint = ((Transports.UTP.UnityTransport)authority.NetworkConfig.NetworkTransport).GetLocalEndpoint();
143+
networkManagers.Remove(authority);
144+
m_Clients = networkManagers;
145+
authority.OnServerStopped += OnServerStopped;
146+
}
147+
148+
private void OnServerStopped(bool wasHost)
149+
{
150+
OnServerStoppedInvoked = SessionCount != 0;
151+
}
152+
}
153+
154+
internal class OnClientStoppedHandler
155+
{
156+
public NetworkManager NetworkManager { get; private set; }
157+
158+
public int SessionCount { get; private set; }
159+
public bool IsSessionAuthority { get; private set; }
160+
161+
protected virtual void OnClientStopped(bool wasHost)
162+
{
163+
SessionCount--;
164+
if (SessionCount <= 0)
165+
{
166+
NetworkManager.OnClientStopped -= OnClientStopped;
167+
return;
168+
}
169+
170+
if (wasHost)
171+
{
172+
NetworkManager.StartHost();
173+
}
174+
else
175+
{
176+
NetworkManager.StartClient();
177+
}
178+
}
179+
180+
public OnClientStoppedHandler(int sessionCount, NetworkManager networkManager)
181+
{
182+
NetworkManager = networkManager;
183+
NetworkManager.OnClientStopped += OnClientStopped;
184+
SessionCount = sessionCount;
185+
IsSessionAuthority = networkManager.IsServer || networkManager.LocalClient.IsSessionOwner;
186+
}
187+
188+
public OnClientStoppedHandler() { }
189+
190+
}
191+
}

com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)