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) 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..aa59d87d89 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,33 @@ public class ConnectionNotificationManager : MonoBehaviour } } ``` + +## Additional NetworkManager notifications + +### Instantiation and destroying + +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`](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 + + +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: + +- [`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`_. + +> [!NOTE] +> 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. 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); } 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