diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 78c877a712..c9d3f0d9b1 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -25,6 +25,8 @@ Additional documentation and release notes are available at [Multiplayer Documen - Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383) - Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383) - Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383) +- Added `OnServerStarted` and `OnServerStopped` events that will trigger only on the server (or host player) to notify that the server just started or is no longer active (#2420) +- Added `OnClientStarted` and `OnClientStopped` events that will trigger only on the client (or host player) to notify that the client just started or is no longer active (#2420) ### Changed diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index b0173033b7..1e50da5a7d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -409,10 +409,28 @@ public IReadOnlyList ConnectedClientsIds public event Action OnClientDisconnectCallback = null; /// - /// The callback to invoke once the server is ready + /// This callback is invoked when the local server is started and listening for incoming connections. /// public event Action OnServerStarted = null; + /// + /// The callback to invoke once the local client is ready + /// + public event Action OnClientStarted = null; + + /// + /// This callback is invoked once the local server is stopped. + /// + /// The first parameter of this event will be set to when stopping a host instance and when stopping a server instance. + public event Action OnServerStopped = null; + + /// + /// The callback to invoke once the local client stops + /// + /// The parameter states whether the client was running in host mode + /// The first parameter of this event will be set to when stopping the host client and when stopping a standard client instance. + public event Action OnClientStopped = null; + /// /// The callback to invoke if the fails. /// @@ -868,6 +886,7 @@ public bool StartClient() IsClient = true; IsListening = true; + OnClientStarted?.Invoke(); return true; } @@ -949,13 +968,14 @@ public bool StartHost() SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); + OnServerStarted?.Invoke(); + OnClientStarted?.Invoke(); + // This assures that any in-scene placed NetworkObject is spawned and // any associated NetworkBehaviours' netcode related properties are // set prior to invoking OnClientConnected. InvokeOnClientConnectedCallback(LocalClientId); - OnServerStarted?.Invoke(); - return true; } @@ -1155,7 +1175,9 @@ internal void ShutdownInternal() NetworkLog.LogInfo(nameof(ShutdownInternal)); } - if (IsServer) + bool wasServer = IsServer; + bool wasClient = IsClient; + if (wasServer) { // make sure all messages are flushed before transport disconnect clients if (MessagingSystem != null) @@ -1288,6 +1310,16 @@ internal void ShutdownInternal() m_StopProcessingMessages = false; ClearClients(); + + if (wasClient) + { + OnClientStopped?.Invoke(wasServer); + } + if (wasServer) + { + OnServerStopped?.Invoke(wasClient); + } + // This cleans up the internal prefabs list NetworkConfig?.Prefabs.Shutdown(); } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerEventsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerEventsTests.cs new file mode 100644 index 0000000000..2544de9bc5 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerEventsTests.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkManagerEventsTests + { + private NetworkManager m_ClientManager; + private NetworkManager m_ServerManager; + + [UnityTest] + public IEnumerator OnServerStoppedCalledWhenServerStops() + { + bool callbackInvoked = false; + var gameObject = new GameObject(nameof(OnServerStoppedCalledWhenServerStops)); + m_ServerManager = gameObject.AddComponent(); + + // Set dummy transport that does nothing + var transport = gameObject.AddComponent(); + m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport }; + + Action onServerStopped = (bool wasAlsoClient) => + { + callbackInvoked = true; + Assert.IsFalse(wasAlsoClient); + if (m_ServerManager.IsServer) + { + Assert.Fail("OnServerStopped called when the server is still active"); + } + }; + + // Start server to cause initialization process + Assert.True(m_ServerManager.StartServer()); + Assert.True(m_ServerManager.IsListening); + + m_ServerManager.OnServerStopped += onServerStopped; + m_ServerManager.Shutdown(); + UnityEngine.Object.DestroyImmediate(gameObject); + + yield return WaitUntilManagerShutsdown(); + + Assert.False(m_ServerManager.IsListening); + Assert.True(callbackInvoked, "OnServerStopped wasn't invoked"); + } + + [UnityTest] + public IEnumerator OnClientStoppedCalledWhenClientStops() + { + yield return InitializeServerAndAClient(); + + bool callbackInvoked = false; + Action onClientStopped = (bool wasAlsoServer) => + { + callbackInvoked = true; + Assert.IsFalse(wasAlsoServer); + if (m_ClientManager.IsClient) + { + Assert.Fail("onClientStopped called when the client is still active"); + } + }; + + m_ClientManager.OnClientStopped += onClientStopped; + m_ClientManager.Shutdown(); + yield return WaitUntilManagerShutsdown(); + + Assert.True(callbackInvoked, "OnClientStopped wasn't invoked"); + } + + [UnityTest] + public IEnumerator OnClientAndServerStoppedCalledWhenHostStops() + { + var gameObject = new GameObject(nameof(OnClientAndServerStoppedCalledWhenHostStops)); + m_ServerManager = gameObject.AddComponent(); + + // Set dummy transport that does nothing + var transport = gameObject.AddComponent(); + m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport }; + + int callbacksInvoked = 0; + Action onClientStopped = (bool wasAlsoServer) => + { + callbacksInvoked++; + Assert.IsTrue(wasAlsoServer); + if (m_ServerManager.IsClient) + { + Assert.Fail("onClientStopped called when the client is still active"); + } + }; + + Action onServerStopped = (bool wasAlsoClient) => + { + callbacksInvoked++; + Assert.IsTrue(wasAlsoClient); + if (m_ServerManager.IsServer) + { + Assert.Fail("OnServerStopped called when the server is still active"); + } + }; + + // Start server to cause initialization process + Assert.True(m_ServerManager.StartHost()); + Assert.True(m_ServerManager.IsListening); + + m_ServerManager.OnServerStopped += onServerStopped; + m_ServerManager.OnClientStopped += onClientStopped; + m_ServerManager.Shutdown(); + UnityEngine.Object.DestroyImmediate(gameObject); + + yield return WaitUntilManagerShutsdown(); + + Assert.False(m_ServerManager.IsListening); + Assert.AreEqual(2, callbacksInvoked, "either OnServerStopped or OnClientStopped wasn't invoked"); + } + + [UnityTest] + public IEnumerator OnServerStartedCalledWhenServerStarts() + { + var gameObject = new GameObject(nameof(OnServerStartedCalledWhenServerStarts)); + m_ServerManager = gameObject.AddComponent(); + + // Set dummy transport that does nothing + var transport = gameObject.AddComponent(); + m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport }; + + bool callbackInvoked = false; + Action onServerStarted = () => + { + callbackInvoked = true; + if (!m_ServerManager.IsServer) + { + Assert.Fail("OnServerStarted called when the server is not active yet"); + } + }; + + // Start server to cause initialization process + m_ServerManager.OnServerStarted += onServerStarted; + + Assert.True(m_ServerManager.StartServer()); + Assert.True(m_ServerManager.IsListening); + + yield return WaitUntilServerBufferingIsReady(); + + Assert.True(callbackInvoked, "OnServerStarted wasn't invoked"); + } + + [UnityTest] + public IEnumerator OnClientStartedCalledWhenClientStarts() + { + bool callbackInvoked = false; + Action onClientStarted = () => + { + callbackInvoked = true; + if (!m_ClientManager.IsClient) + { + Assert.Fail("onClientStarted called when the client is not active yet"); + } + }; + + yield return InitializeServerAndAClient(onClientStarted); + + Assert.True(callbackInvoked, "OnClientStarted wasn't invoked"); + } + + [UnityTest] + public IEnumerator OnClientAndServerStartedCalledWhenHostStarts() + { + var gameObject = new GameObject(nameof(OnClientAndServerStartedCalledWhenHostStarts)); + m_ServerManager = gameObject.AddComponent(); + + // Set dummy transport that does nothing + var transport = gameObject.AddComponent(); + m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport }; + + int callbacksInvoked = 0; + Action onClientStarted = () => + { + callbacksInvoked++; + if (!m_ServerManager.IsClient) + { + Assert.Fail("OnClientStarted called when the client is not active yet"); + } + }; + + Action onServerStarted = () => + { + callbacksInvoked++; + if (!m_ServerManager.IsServer) + { + Assert.Fail("OnServerStarted called when the server is not active yet"); + } + }; + + m_ServerManager.OnServerStarted += onServerStarted; + m_ServerManager.OnClientStarted += onClientStarted; + + // Start server to cause initialization process + Assert.True(m_ServerManager.StartHost()); + Assert.True(m_ServerManager.IsListening); + + yield return WaitUntilServerBufferingIsReady(); + Assert.AreEqual(2, callbacksInvoked, "either OnServerStarted or OnClientStarted wasn't invoked"); + } + + private IEnumerator WaitUntilManagerShutsdown() + { + /* Need two updates to actually shut down. First one to see the transport failing, which + marks the NetworkManager as shutting down. Second one where actual shutdown occurs. */ + yield return null; + yield return null; + } + + private IEnumerator InitializeServerAndAClient(Action onClientStarted = null) + { + // Create multiple NetworkManager instances + if (!NetcodeIntegrationTestHelpers.Create(1, out m_ServerManager, out NetworkManager[] clients, 30)) + { + Debug.LogError("Failed to create instances"); + Assert.Fail("Failed to create instances"); + } + + // passing no clients on purpose to start them manually later + NetcodeIntegrationTestHelpers.Start(false, m_ServerManager, new NetworkManager[] { }); + + yield return WaitUntilServerBufferingIsReady(); + m_ClientManager = clients[0]; + + if (onClientStarted != null) + { + m_ClientManager.OnClientStarted += onClientStarted; + } + + Assert.True(m_ClientManager.StartClient()); + NetcodeIntegrationTestHelpers.RegisterHandlers(clients[0]); + // Wait for connection on client side + yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients); + } + + private IEnumerator WaitUntilServerBufferingIsReady() + { + /* wait until at least more than 2 server ticks have passed + Note: Waiting for more than 2 ticks on the server is due + to the time system applying buffering to the received time + in NetworkTimeSystem.Sync */ + yield return new WaitUntil(() => m_ServerManager.NetworkTickSystem.ServerTime.Tick > 2); + } + + [UnityTearDown] + public virtual IEnumerator Teardown() + { + NetcodeIntegrationTestHelpers.Destroy(); + yield return null; + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerEventsTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerEventsTests.cs.meta new file mode 100644 index 0000000000..996ba3bf46 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkManagerEventsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 238d8724ba5ce3947bc20f5d6c056b6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: