Skip to content

Commit 2da54f0

Browse files
RikuTheFuffsPaolo Abela
andauthored
feat: Added "OnServerStopped" event [MPDSE-10] (#2420)
*feat: added "OnServerStopped" event that will trigger only on the server (or host player) to notify that the server is no longer active. * feat(documentation): added entry to changelog.md * feat: 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 * documentation: updated documentation * fix: fixed OnServerStarted and OnClientStarted being called at different times in Host vs Client-only / Server-only mode --------- Co-authored-by: Paolo Abela <[email protected]>
1 parent 849ca84 commit 2da54f0

File tree

4 files changed

+307
-4
lines changed

4 files changed

+307
-4
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
2525
- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
2626
- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
2727
- 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)
28+
- 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)
29+
- 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)
2830

2931
### Changed
3032

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,28 @@ public IReadOnlyList<ulong> ConnectedClientsIds
409409
public event Action<ulong> OnClientDisconnectCallback = null;
410410

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

416+
/// <summary>
417+
/// The callback to invoke once the local client is ready
418+
/// </summary>
419+
public event Action OnClientStarted = null;
420+
421+
/// <summary>
422+
/// This callback is invoked once the local server is stopped.
423+
/// </summary>
424+
/// <param name="arg1">The first parameter of this event will be set to <see cref="true"/> when stopping a host instance and <see cref="false"/> when stopping a server instance.</param>
425+
public event Action<bool> OnServerStopped = null;
426+
427+
/// <summary>
428+
/// The callback to invoke once the local client stops
429+
/// </summary>
430+
/// <remarks>The parameter states whether the client was running in host mode</remarks>
431+
/// <param name="arg1">The first parameter of this event will be set to <see cref="true"/> when stopping the host client and <see cref="false"/> when stopping a standard client instance.</param>
432+
public event Action<bool> OnClientStopped = null;
433+
416434
/// <summary>
417435
/// The callback to invoke if the <see cref="NetworkTransport"/> fails.
418436
/// </summary>
@@ -868,6 +886,7 @@ public bool StartClient()
868886
IsClient = true;
869887
IsListening = true;
870888

889+
OnClientStarted?.Invoke();
871890
return true;
872891
}
873892

@@ -949,13 +968,14 @@ public bool StartHost()
949968

950969
SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
951970

971+
OnServerStarted?.Invoke();
972+
OnClientStarted?.Invoke();
973+
952974
// This assures that any in-scene placed NetworkObject is spawned and
953975
// any associated NetworkBehaviours' netcode related properties are
954976
// set prior to invoking OnClientConnected.
955977
InvokeOnClientConnectedCallback(LocalClientId);
956978

957-
OnServerStarted?.Invoke();
958-
959979
return true;
960980
}
961981

@@ -1155,7 +1175,9 @@ internal void ShutdownInternal()
11551175
NetworkLog.LogInfo(nameof(ShutdownInternal));
11561176
}
11571177

1158-
if (IsServer)
1178+
bool wasServer = IsServer;
1179+
bool wasClient = IsClient;
1180+
if (wasServer)
11591181
{
11601182
// make sure all messages are flushed before transport disconnect clients
11611183
if (MessagingSystem != null)
@@ -1288,6 +1310,16 @@ internal void ShutdownInternal()
12881310
m_StopProcessingMessages = false;
12891311

12901312
ClearClients();
1313+
1314+
if (wasClient)
1315+
{
1316+
OnClientStopped?.Invoke(wasServer);
1317+
}
1318+
if (wasServer)
1319+
{
1320+
OnServerStopped?.Invoke(wasClient);
1321+
}
1322+
12911323
// This cleans up the internal prefabs list
12921324
NetworkConfig?.Prefabs.Shutdown();
12931325
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
using System;
2+
using System.Collections;
3+
using UnityEngine;
4+
using UnityEngine.TestTools;
5+
using NUnit.Framework;
6+
using Unity.Netcode.TestHelpers.Runtime;
7+
8+
namespace Unity.Netcode.RuntimeTests
9+
{
10+
public class NetworkManagerEventsTests
11+
{
12+
private NetworkManager m_ClientManager;
13+
private NetworkManager m_ServerManager;
14+
15+
[UnityTest]
16+
public IEnumerator OnServerStoppedCalledWhenServerStops()
17+
{
18+
bool callbackInvoked = false;
19+
var gameObject = new GameObject(nameof(OnServerStoppedCalledWhenServerStops));
20+
m_ServerManager = gameObject.AddComponent<NetworkManager>();
21+
22+
// Set dummy transport that does nothing
23+
var transport = gameObject.AddComponent<DummyTransport>();
24+
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
25+
26+
Action<bool> onServerStopped = (bool wasAlsoClient) =>
27+
{
28+
callbackInvoked = true;
29+
Assert.IsFalse(wasAlsoClient);
30+
if (m_ServerManager.IsServer)
31+
{
32+
Assert.Fail("OnServerStopped called when the server is still active");
33+
}
34+
};
35+
36+
// Start server to cause initialization process
37+
Assert.True(m_ServerManager.StartServer());
38+
Assert.True(m_ServerManager.IsListening);
39+
40+
m_ServerManager.OnServerStopped += onServerStopped;
41+
m_ServerManager.Shutdown();
42+
UnityEngine.Object.DestroyImmediate(gameObject);
43+
44+
yield return WaitUntilManagerShutsdown();
45+
46+
Assert.False(m_ServerManager.IsListening);
47+
Assert.True(callbackInvoked, "OnServerStopped wasn't invoked");
48+
}
49+
50+
[UnityTest]
51+
public IEnumerator OnClientStoppedCalledWhenClientStops()
52+
{
53+
yield return InitializeServerAndAClient();
54+
55+
bool callbackInvoked = false;
56+
Action<bool> onClientStopped = (bool wasAlsoServer) =>
57+
{
58+
callbackInvoked = true;
59+
Assert.IsFalse(wasAlsoServer);
60+
if (m_ClientManager.IsClient)
61+
{
62+
Assert.Fail("onClientStopped called when the client is still active");
63+
}
64+
};
65+
66+
m_ClientManager.OnClientStopped += onClientStopped;
67+
m_ClientManager.Shutdown();
68+
yield return WaitUntilManagerShutsdown();
69+
70+
Assert.True(callbackInvoked, "OnClientStopped wasn't invoked");
71+
}
72+
73+
[UnityTest]
74+
public IEnumerator OnClientAndServerStoppedCalledWhenHostStops()
75+
{
76+
var gameObject = new GameObject(nameof(OnClientAndServerStoppedCalledWhenHostStops));
77+
m_ServerManager = gameObject.AddComponent<NetworkManager>();
78+
79+
// Set dummy transport that does nothing
80+
var transport = gameObject.AddComponent<DummyTransport>();
81+
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
82+
83+
int callbacksInvoked = 0;
84+
Action<bool> onClientStopped = (bool wasAlsoServer) =>
85+
{
86+
callbacksInvoked++;
87+
Assert.IsTrue(wasAlsoServer);
88+
if (m_ServerManager.IsClient)
89+
{
90+
Assert.Fail("onClientStopped called when the client is still active");
91+
}
92+
};
93+
94+
Action<bool> onServerStopped = (bool wasAlsoClient) =>
95+
{
96+
callbacksInvoked++;
97+
Assert.IsTrue(wasAlsoClient);
98+
if (m_ServerManager.IsServer)
99+
{
100+
Assert.Fail("OnServerStopped called when the server is still active");
101+
}
102+
};
103+
104+
// Start server to cause initialization process
105+
Assert.True(m_ServerManager.StartHost());
106+
Assert.True(m_ServerManager.IsListening);
107+
108+
m_ServerManager.OnServerStopped += onServerStopped;
109+
m_ServerManager.OnClientStopped += onClientStopped;
110+
m_ServerManager.Shutdown();
111+
UnityEngine.Object.DestroyImmediate(gameObject);
112+
113+
yield return WaitUntilManagerShutsdown();
114+
115+
Assert.False(m_ServerManager.IsListening);
116+
Assert.AreEqual(2, callbacksInvoked, "either OnServerStopped or OnClientStopped wasn't invoked");
117+
}
118+
119+
[UnityTest]
120+
public IEnumerator OnServerStartedCalledWhenServerStarts()
121+
{
122+
var gameObject = new GameObject(nameof(OnServerStartedCalledWhenServerStarts));
123+
m_ServerManager = gameObject.AddComponent<NetworkManager>();
124+
125+
// Set dummy transport that does nothing
126+
var transport = gameObject.AddComponent<DummyTransport>();
127+
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
128+
129+
bool callbackInvoked = false;
130+
Action onServerStarted = () =>
131+
{
132+
callbackInvoked = true;
133+
if (!m_ServerManager.IsServer)
134+
{
135+
Assert.Fail("OnServerStarted called when the server is not active yet");
136+
}
137+
};
138+
139+
// Start server to cause initialization process
140+
m_ServerManager.OnServerStarted += onServerStarted;
141+
142+
Assert.True(m_ServerManager.StartServer());
143+
Assert.True(m_ServerManager.IsListening);
144+
145+
yield return WaitUntilServerBufferingIsReady();
146+
147+
Assert.True(callbackInvoked, "OnServerStarted wasn't invoked");
148+
}
149+
150+
[UnityTest]
151+
public IEnumerator OnClientStartedCalledWhenClientStarts()
152+
{
153+
bool callbackInvoked = false;
154+
Action onClientStarted = () =>
155+
{
156+
callbackInvoked = true;
157+
if (!m_ClientManager.IsClient)
158+
{
159+
Assert.Fail("onClientStarted called when the client is not active yet");
160+
}
161+
};
162+
163+
yield return InitializeServerAndAClient(onClientStarted);
164+
165+
Assert.True(callbackInvoked, "OnClientStarted wasn't invoked");
166+
}
167+
168+
[UnityTest]
169+
public IEnumerator OnClientAndServerStartedCalledWhenHostStarts()
170+
{
171+
var gameObject = new GameObject(nameof(OnClientAndServerStartedCalledWhenHostStarts));
172+
m_ServerManager = gameObject.AddComponent<NetworkManager>();
173+
174+
// Set dummy transport that does nothing
175+
var transport = gameObject.AddComponent<DummyTransport>();
176+
m_ServerManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport };
177+
178+
int callbacksInvoked = 0;
179+
Action onClientStarted = () =>
180+
{
181+
callbacksInvoked++;
182+
if (!m_ServerManager.IsClient)
183+
{
184+
Assert.Fail("OnClientStarted called when the client is not active yet");
185+
}
186+
};
187+
188+
Action onServerStarted = () =>
189+
{
190+
callbacksInvoked++;
191+
if (!m_ServerManager.IsServer)
192+
{
193+
Assert.Fail("OnServerStarted called when the server is not active yet");
194+
}
195+
};
196+
197+
m_ServerManager.OnServerStarted += onServerStarted;
198+
m_ServerManager.OnClientStarted += onClientStarted;
199+
200+
// Start server to cause initialization process
201+
Assert.True(m_ServerManager.StartHost());
202+
Assert.True(m_ServerManager.IsListening);
203+
204+
yield return WaitUntilServerBufferingIsReady();
205+
Assert.AreEqual(2, callbacksInvoked, "either OnServerStarted or OnClientStarted wasn't invoked");
206+
}
207+
208+
private IEnumerator WaitUntilManagerShutsdown()
209+
{
210+
/* Need two updates to actually shut down. First one to see the transport failing, which
211+
marks the NetworkManager as shutting down. Second one where actual shutdown occurs. */
212+
yield return null;
213+
yield return null;
214+
}
215+
216+
private IEnumerator InitializeServerAndAClient(Action onClientStarted = null)
217+
{
218+
// Create multiple NetworkManager instances
219+
if (!NetcodeIntegrationTestHelpers.Create(1, out m_ServerManager, out NetworkManager[] clients, 30))
220+
{
221+
Debug.LogError("Failed to create instances");
222+
Assert.Fail("Failed to create instances");
223+
}
224+
225+
// passing no clients on purpose to start them manually later
226+
NetcodeIntegrationTestHelpers.Start(false, m_ServerManager, new NetworkManager[] { });
227+
228+
yield return WaitUntilServerBufferingIsReady();
229+
m_ClientManager = clients[0];
230+
231+
if (onClientStarted != null)
232+
{
233+
m_ClientManager.OnClientStarted += onClientStarted;
234+
}
235+
236+
Assert.True(m_ClientManager.StartClient());
237+
NetcodeIntegrationTestHelpers.RegisterHandlers(clients[0]);
238+
// Wait for connection on client side
239+
yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients);
240+
}
241+
242+
private IEnumerator WaitUntilServerBufferingIsReady()
243+
{
244+
/* wait until at least more than 2 server ticks have passed
245+
Note: Waiting for more than 2 ticks on the server is due
246+
to the time system applying buffering to the received time
247+
in NetworkTimeSystem.Sync */
248+
yield return new WaitUntil(() => m_ServerManager.NetworkTickSystem.ServerTime.Tick > 2);
249+
}
250+
251+
[UnityTearDown]
252+
public virtual IEnumerator Teardown()
253+
{
254+
NetcodeIntegrationTestHelpers.Destroy();
255+
yield return null;
256+
}
257+
}
258+
}

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

Lines changed: 11 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)