Skip to content
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 33 additions & 2 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,10 +409,27 @@ public IReadOnlyList<ulong> ConnectedClientsIds
public event Action<ulong> OnClientDisconnectCallback = null;

/// <summary>
/// The callback to invoke once the server is ready
/// This callback is invoked when the local server is started and listening for incoming connections.
/// </summary>
public event Action OnServerStarted = null;

/// <summary>
/// The callback to invoke once the local client is ready
Comment thread
RikuTheFuffs marked this conversation as resolved.
/// </summary>
Comment thread
RikuTheFuffs marked this conversation as resolved.
public event Action OnClientStarted = null;

/// <summary>
/// This callback is invoked once the local server is stopped.
/// </summary>
/// <remarks>The parameter states whether the server was running in host mode</remarks>
Comment thread
RikuTheFuffs marked this conversation as resolved.
Outdated
public event Action<bool> OnServerStopped = null;

/// <summary>
/// The callback to invoke once the local client stops
/// </summary>
/// <remarks>The parameter states whether the client was running in host mode</remarks>
Comment thread
RikuTheFuffs marked this conversation as resolved.
public event Action<bool> OnClientStopped = null;

/// <summary>
/// The callback to invoke if the <see cref="NetworkTransport"/> fails.
/// </summary>
Expand Down Expand Up @@ -868,6 +885,7 @@ public bool StartClient()
IsClient = true;
IsListening = true;

OnClientStarted?.Invoke();
return true;
}

Expand Down Expand Up @@ -955,6 +973,7 @@ public bool StartHost()
InvokeOnClientConnectedCallback(LocalClientId);

OnServerStarted?.Invoke();
OnClientStarted?.Invoke();

return true;
}
Expand Down Expand Up @@ -1155,7 +1174,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)
Expand Down Expand Up @@ -1288,6 +1309,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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NetworkManager>();

// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };

Action<bool> 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<bool> 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<NetworkManager>();

// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };

int callbacksInvoked = 0;
Action<bool> onClientStopped = (bool wasAlsoServer) =>
{
callbacksInvoked++;
Assert.IsTrue(wasAlsoServer);
if (m_ServerManager.IsClient)
{
Assert.Fail("onClientStopped called when the client is still active");
}
};

Action<bool> 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<NetworkManager>();

// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
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<NetworkManager>();

// Set dummy transport that does nothing
var transport = gameObject.AddComponent<DummyTransport>();
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;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.