From 85964c345e4d7a2c37019c670e28f11e5f3cc4dc Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 18 Mar 2026 19:01:03 -0500 Subject: [PATCH 1/5] fix Fixing the issue where users might try to start NetworkManager within OnClientStopped or OnServerStopped which would result in a failed start. --- .../Runtime/Core/NetworkManager.cs | 30 +++++++++++-------- .../Runtime/Spawning/NetworkSpawnManager.cs | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 67acb6b7a8..b5f037c4ac 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -1674,19 +1674,8 @@ internal void ShutdownInternal() ConnectionManager.InvokeOnClientDisconnectCallback(LocalClientId); } - if (ConnectionManager.LocalClient.IsClient) - { - // If we were a client, we want to know if we were a host - // client or not. (why we pass in "IsServer") - OnClientStopped?.Invoke(ConnectionManager.LocalClient.IsServer); - } - - if (ConnectionManager.LocalClient.IsServer) - { - // If we were a server, we want to know if we were a host - // or not. (why we pass in "IsClient") - OnServerStopped?.Invoke(ConnectionManager.LocalClient.IsClient); - } + // Save off the last local client settings + var localClient = ConnectionManager.LocalClient; // In the event shutdown is invoked within OnClientStopped or OnServerStopped, set it to false again m_ShuttingDown = false; @@ -1706,6 +1695,21 @@ internal void ShutdownInternal() // can unsubscribe from tick updates and such. NetworkTimeSystem?.Shutdown(); NetworkTickSystem = null; + + + if (localClient.IsClient) + { + // If we were a client, we want to know if we were a host + // client or not. (why we pass in "IsServer") + OnClientStopped?.Invoke(localClient.IsServer); + } + + if (localClient.IsServer) + { + // If we were a server, we want to know if we were a host + // or not. (why we pass in "IsClient") + OnServerStopped?.Invoke(localClient.IsClient); + } } // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application. diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index d4768692ec..bf4eacbc1b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1269,7 +1269,7 @@ internal unsafe void UpdateNetworkObjectSceneChanges() foreach (var entry in NetworkObjectsToSynchronizeSceneChanges) { // If it fails the first update then don't add for updates - if (!entry.Value.UpdateForSceneChanges()) + if (entry.Value != null && !entry.Value.UpdateForSceneChanges()) { CleanUpDisposedObjects.Push(entry.Key); } From bd5c75784050892ca89648ced03ade085d632da4 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 18 Mar 2026 19:09:18 -0500 Subject: [PATCH 2/5] 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). --- .../Runtime/NetworkManagerStartStopTests.cs | 191 ++++++++++++++++++ .../NetworkManagerStartStopTests.cs.meta | 2 + 2 files changed, 193 insertions(+) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs new file mode 100644 index 0000000000..32f9e42856 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs @@ -0,0 +1,191 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(NetworkTopologyTypes.ClientServer)] + internal class NetworkManagerStartStopTests : NetcodeIntegrationTest + { + private const int k_NumberOfSessions = 5; + protected override int NumberOfClients => 2; + private OnClientStoppedHandler m_StoppedHandler; + private int m_ExpectedNumberOfClients = 0; + public NetworkManagerStartStopTests(NetworkTopologyTypes networkTopologyType) : base(networkTopologyType, HostOrServer.Host) { } + + /// + /// This test will not work with the CMB service since it requires the service + /// to remain active after all clients have disconnected. + /// + protected override bool UseCMBService() + { + return false; + } + + private void ShutdownIfListening() + { + var networkManager = m_StoppedHandler.NetworkManager; + if (networkManager.IsListening) + { + m_StoppedHandler.NetworkManager.Shutdown(); + } + } + + private bool NetworkManagerCompletedSessionCount(StringBuilder errorLog) + { + // Once the session count is decremented to zero the condition has been met. + if (m_StoppedHandler.SessionCount != 0) + { + // If we are a host, then only shutdown once all clients have reconnected + if (m_StoppedHandler.IsSessionAuthority && m_StoppedHandler.NetworkManager.ConnectedClientsIds.Count != m_ExpectedNumberOfClients) + { + errorLog.Append($"[{m_StoppedHandler.NetworkManager.name}] Waiting for {m_ExpectedNumberOfClients} clients to connect but there are only {m_StoppedHandler.NetworkManager.ConnectedClientsIds.Count} connected!"); + return false; + } + ShutdownIfListening(); + errorLog.Append($"[{m_StoppedHandler.NetworkManager.name}] Still has a session count of {m_StoppedHandler.SessionCount}!"); + } + return errorLog.Length == 0; + } + + [UnityTest] + public IEnumerator StartFromWithinOnClientStopped() + { + var authority = GetAuthorityNetworkManager(); + m_ExpectedNumberOfClients = authority.ConnectedClientsIds.Count; + + // Validate a client can disconnect and immediately reconnect from within OnClientStopped + m_StoppedHandler = new OnClientStoppedHandler(k_NumberOfSessions, GetNonAuthorityNetworkManager()); + ShutdownIfListening(); + yield return WaitForConditionOrTimeOut(NetworkManagerCompletedSessionCount); + AssertOnTimeout($"Not all {nameof(NetworkManager)} instances finished their sessions!"); + + // Validate a host can disconnect and immediately reconnect from within OnClientStopped + m_StoppedHandler = new OnHostStoppedHandler(k_NumberOfSessions, authority, m_NetworkManagers.ToList()); + ShutdownIfListening(); + yield return WaitForConditionOrTimeOut(NetworkManagerCompletedSessionCount); + AssertOnTimeout($"Not all {nameof(NetworkManager)} instances finished their sessions!"); + + // Verify OnServerStopped is not invoked if NetworkManager is started again within OnClientStopped (it should not invoke if it is listening). + Assert.False((m_StoppedHandler as OnHostStoppedHandler).OnServerStoppedInvoked, $"{nameof(NetworkManager.OnServerStopped)} was invoked when it should not have been invoked!"); + } + } + + internal class OnHostStoppedHandler : OnClientStoppedHandler + { + public bool OnServerStoppedInvoked = false; + + private List m_Clients = new List(); + + private Networking.Transport.NetworkEndpoint m_Endpoint; + + protected override void OnClientStopped(bool wasHost) + { + m_Endpoint.Port++; + var unityTransport = (Transports.UTP.UnityTransport)NetworkManager.NetworkConfig.NetworkTransport; + unityTransport.SetConnectionData(m_Endpoint); + // Make sure all clients are shutdown or shutting down + foreach (var networkManager in m_Clients) + { + if (networkManager.IsListening && !networkManager.ShutdownInProgress) + { + networkManager.Shutdown(); + } + } + + base.OnClientStopped(wasHost); + if (SessionCount != 0) + { + NetworkManager.StartCoroutine(StartClients()); + } + + } + + private IEnumerator StartClients() + { + var nextPhase = false; + var timeout = UnityEngine.Time.realtimeSinceStartup + 5.0f; + while (!nextPhase) + { + if (!nextPhase && timeout < UnityEngine.Time.realtimeSinceStartup) + { + Assert.Fail($"Timed out waiting for all {nameof(NetworkManager)} instances to shutdown!"); + yield break; + } + + nextPhase = true; + foreach (var networkManager in m_Clients) + { + if (networkManager.ShutdownInProgress || networkManager.IsListening) + { + nextPhase = false; + } + } + yield return null; + } + + // Now, start all of the clients and have them connect again + foreach (var networkManager in m_Clients) + { + var unityTransport = (Transports.UTP.UnityTransport)networkManager.NetworkConfig.NetworkTransport; + unityTransport.SetConnectionData(m_Endpoint); + networkManager.StartClient(); + } + } + + public OnHostStoppedHandler(int numberOfSessions, NetworkManager authority, List networkManagers) : base(numberOfSessions, authority) + { + m_Endpoint = ((Transports.UTP.UnityTransport)authority.NetworkConfig.NetworkTransport).GetLocalEndpoint(); + networkManagers.Remove(authority); + m_Clients = networkManagers; + authority.OnServerStopped += OnServerStopped; + } + + private void OnServerStopped(bool wasHost) + { + OnServerStoppedInvoked = SessionCount != 0; + } + } + + internal class OnClientStoppedHandler + { + public NetworkManager NetworkManager { get; private set; } + + public int SessionCount { get; private set; } + public bool IsSessionAuthority { get; private set; } + + protected virtual void OnClientStopped(bool wasHost) + { + SessionCount--; + if (SessionCount <= 0) + { + NetworkManager.OnClientStopped -= OnClientStopped; + return; + } + + if (wasHost) + { + NetworkManager.StartHost(); + } + else + { + NetworkManager.StartClient(); + } + } + + public OnClientStoppedHandler(int sessionCount, NetworkManager networkManager) + { + NetworkManager = networkManager; + NetworkManager.OnClientStopped += OnClientStopped; + SessionCount = sessionCount; + IsSessionAuthority = networkManager.IsServer || networkManager.LocalClient.IsSessionOwner; + } + + public OnClientStoppedHandler() { } + + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs.meta new file mode 100644 index 0000000000..8192a7454e --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerStartStopTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8ec27192eb899144e82f7a5016ce5cfa \ No newline at end of file From 1b2c6528165289c45e29c05192c48de7f89c9e6b Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Thu, 19 Mar 2026 10:51:01 -0500 Subject: [PATCH 3/5] fix Removing model: rtx2080 from our yamato vm definition file. --- .yamato/project.metafile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.yamato/project.metafile b/.yamato/project.metafile index 941a17f984..938bbf2d74 100644 --- a/.yamato/project.metafile +++ b/.yamato/project.metafile @@ -12,7 +12,7 @@ # smaller_flavor --> An override for flavor that determines the VM size/resources for lighter weight jobs that can run on a smaller vm # larger_flavor --> An override for flavor that determines the VM size/resources for heavier weight jobs that can need a bigger vm # standalone --> Specifies the build target platform (e.g., StandaloneLinux64, Android, IOS) - # 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 + # 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 # base --> Indicates the base operating system for build operations (e.g., win, mac) # architecture --> Specifies the target CPU architecture (e.g., armv7, arm64) @@ -50,7 +50,6 @@ test_platforms: smaller_flavor: b1.medium larger_flavor: b1.xlarge standalone: StandaloneLinux64 - model: rtx2080 - name: win type: Unity::VM image: package-ci/win10:v4 @@ -58,7 +57,6 @@ test_platforms: smaller_flavor: b1.medium larger_flavor: b1.xlarge standalone: StandaloneWindows64 - model: rtx2080 - name: mac type: Unity::VM::osx image: package-ci/macos-13-arm64:v4 # ARM64 to support M1 model (below) From 03d543a70ad9fcbe0caa160a7b4f13e8340e644c Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Thu, 19 Mar 2026 14:05:42 -0500 Subject: [PATCH 4/5] 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. --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../components/core/networkmanager.md | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 798064c1ad..7f561d4349 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -41,6 +41,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where starting the NetworkManager within `OnClientStopped` or `OnServerStopped` resulted in a broken `NetworkManager` state. (#3908) - Fixed issue where an attachable could log an error upon being de-spawned during shutdown. (#3895) - NestedNetworkVariables initialized with no value no longer throw an error. (#3891) - Fixed `NetworkShow` behavior when it is called twice. (#3867) diff --git a/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md b/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md index a36ec63e95..787702c1ce 100644 --- a/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md +++ b/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md @@ -169,8 +169,6 @@ Subscribe to the `NetworkManager.OnClientDisconnectCallback` event to receive no - On the client-side, the client identifier parameter is the identifier assigned to the client. - _The exception to this is when a client is disconnected before its connection is approved._ -You can also use the `NetworkManager.OnServerStopped` and `NetworkManager.OnClientStopped` callbacks to get local notifications when the server or client stops respectively. - ### Connection notification manager example 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,32 @@ public class ConnectionNotificationManager : MonoBehaviour } } ``` + +## Additional NetworkManager notifications + +### Instantiation and destroying + +There are times when it could be useful to be notified when a NetworkManager has been instantiated or is about to be destroyed. There are two static NetworkManager events you can use for this: + +- NetworkManager.OnInstantiated: This is invoked when a NetworkManager is instantiated. +- NetworkManager.OnDestroying: This is invoked when a NetworkManager is about to be destroyed. + +### When a NetworkManager is stopped + +You will almost always want to know when a NetworkManager has finished shutting down ("stopped"). This can be useful to know when it is safe to transition back to a main menu scene or you might want to perform other similar types of tasks. However, knowing when a NetworkManager has been stopped does not help if you want to save off any state that might exist on spawned objects because at that point everything will have been de-spawned and destroyed. If you run into this scenario then you can use the following event notification: + +- [NetworkManager.OnPreShutdown](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.10/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnPreShutdown): This is invoked prior to finalizing the NetworkManager shutdown process. Any remaining spawned objects will still be instantiated and spawned when this event is invoked. + +Similar to the started events, there are two stopped events you can subscribe to that lets you know the NetworkManager is completely shutdown ("stopped"): + +- [NetworkManager.OnClientStopped](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.10/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnClientStopped): This is invoked on a Host or client when NetworkManager has completely shutdown and is ready to be restarted. +- [NetworkManager.OnServerStopped](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.10/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnServerStopped): This is invoked on a Host or Server when NetworkManager has completely shutdown and is ready to be restarted. + +Since a host is both a client and a server, the event invocation order is: + +- OnClientStopped +- OnServerStopped + - _Only if the NetworkManager instance is not restarted during `OnClientStopped`_. + +> [!NOTE] +> If you start the NetworkManager during `NetworkManager.OnClientStopped`, then upon the NetworkManager having restarted successfully it will skip the invocation of `OnServerStopped` since it is no longer shutdown. From 4722d7e9e34e6ff5e01cb81bd02e2a7f727853ed Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 20 Mar 2026 09:44:24 -0500 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Amy Reeve --- .../components/core/networkmanager.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md b/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md index 787702c1ce..aa59d87d89 100644 --- a/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md +++ b/com.unity.netcode.gameobjects/Documentation~/components/core/networkmanager.md @@ -259,27 +259,28 @@ public class ConnectionNotificationManager : MonoBehaviour ### Instantiation and destroying -There are times when it could be useful to be notified when a NetworkManager has been instantiated or is about to be destroyed. There are two static NetworkManager events you can use for this: +There are two static NetworkManager events you can use to be notified when a NetworkManager is instantiated or is about to be destroyed: -- NetworkManager.OnInstantiated: This is invoked when a NetworkManager is instantiated. -- NetworkManager.OnDestroying: This is invoked when a NetworkManager is about to be destroyed. +- [`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. +- [`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. ### When a NetworkManager is stopped -You will almost always want to know when a NetworkManager has finished shutting down ("stopped"). This can be useful to know when it is safe to transition back to a main menu scene or you might want to perform other similar types of tasks. However, knowing when a NetworkManager has been stopped does not help if you want to save off any state that might exist on spawned objects because at that point everything will have been de-spawned and destroyed. If you run into this scenario then you can use the following event notification: -- [NetworkManager.OnPreShutdown](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.10/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnPreShutdown): This is invoked prior to finalizing the NetworkManager shutdown process. Any remaining spawned objects will still be instantiated and spawned when this event is invoked. +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: -Similar to the started events, there are two stopped events you can subscribe to that lets you know the NetworkManager is completely shutdown ("stopped"): - -- [NetworkManager.OnClientStopped](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.10/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnClientStopped): This is invoked on a Host or client when NetworkManager has completely shutdown and is ready to be restarted. -- [NetworkManager.OnServerStopped](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.10/api/Unity.Netcode.NetworkManager.html#Unity_Netcode_NetworkManager_OnServerStopped): This is invoked on a Host or Server when NetworkManager has completely shutdown and is ready to be restarted. +- [`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. +- [`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. Since a host is both a client and a server, the event invocation order is: -- OnClientStopped -- OnServerStopped - - _Only if the NetworkManager instance is not restarted during `OnClientStopped`_. +- `OnClientStopped` +- `OnServerStopped` + - _Only if the NetworkManager instance is not restarted during `OnClientStopped`_. > [!NOTE] -> If you start the NetworkManager during `NetworkManager.OnClientStopped`, then upon the NetworkManager having restarted successfully it will skip the invocation of `OnServerStopped` since it is no longer shutdown. +> If you restart the NetworkManager during `NetworkManager.OnClientStopped`, then it will skip the invocation of `OnServerStopped`. + +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: + +- [`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.