Skip to content

Commit acd0b6d

Browse files
NoelStephensUnitynetcode-ci-service
authored andcommitted
fix: SceneEventProgress was triggering twice when running a host and no clients were connected (Unity-Technologies#2292)
* fix Exclude the host-client from being processed as one of the clients to test for when the scene event has completed. GetClientsWithStatus needed to add the host-client to the list of clients that completed once the scene event was completed. Also fixed a bug in GetClientsWithStatus where, when checking for clients that did not complete, it would include the clients that completed and the clients that did not complete in the list of client identifiers it returned. Fixing issue where integration tests could lag by more than 1 network tick and the AsyncOperation could not yet be assigned. * test Added HostReceivesOneLoadEventCompletedNotification integration test to validate the fix for this PR. Updated NetcodeIntegrationTest virtual methods to be able to gain access to a newly created client (via NetcodeIntegrationTest.CreateAndStartNewClient) when it is first created, started, and when it has been connected. The updates to SceneEventProgress exposed an issue with ClientSynchronizationValidationTest. It was written before many NetcodeIntegrationTest helpers, so there are adjustments here to leverage from those which greatly simplifies the test. Switched the scene that was not validated by the client-side to one that did not contain an in-scene placed NetworkObject. Co-authored-by: Unity Netcode CI <74025435+netcode-ci-service@users.noreply.github.com>
1 parent 3906346 commit acd0b6d

File tree

5 files changed

+134
-67
lines changed

5 files changed

+134
-67
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1414

1515
### Fixed
1616

17+
- Fixed issue where the host would receive more than one event completed notification when loading or unloading a scene only when no clients were connected. (#2292)
1718
- Fixed issue where in-scene placed `NetworkObjects` were not honoring the `AutoObjectParentSync` property. (#2281)
1819
- Fixed the issue where `NetworkManager.OnClientConnectedCallback` was being invoked before in-scene placed `NetworkObject`s had been spawned when starting `NetworkManager` as a host. (#2277)
1920
- Creating a `FastBufferReader` with `Allocator.None` will not result in extra memory being allocated for the buffer (since it's owned externally in that scenario). (#2265)

com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,37 @@ internal bool HasTimedOut()
108108
internal List<ulong> GetClientsWithStatus(bool completedSceneEvent)
109109
{
110110
var clients = new List<ulong>();
111-
foreach (var clientStatus in ClientsProcessingSceneEvent)
111+
if (completedSceneEvent)
112112
{
113-
if (clientStatus.Value == completedSceneEvent)
113+
// If we are the host, then add the host-client to the list
114+
// of clients that completed if the AsyncOperation is done.
115+
if (m_NetworkManager.IsHost && m_AsyncOperation.isDone)
114116
{
115-
clients.Add(clientStatus.Key);
117+
clients.Add(m_NetworkManager.LocalClientId);
116118
}
117-
}
118119

119-
// If we are getting the list of clients that have not completed the
120-
// scene event, then add any clients that disconnected during this
121-
// scene event.
122-
if (!completedSceneEvent)
120+
// Add all clients that completed the scene event
121+
foreach (var clientStatus in ClientsProcessingSceneEvent)
122+
{
123+
if (clientStatus.Value == completedSceneEvent)
124+
{
125+
clients.Add(clientStatus.Key);
126+
}
127+
}
128+
}
129+
else
123130
{
131+
// If we are the host, then add the host-client to the list
132+
// of clients that did not complete if the AsyncOperation is
133+
// not done.
134+
if (m_NetworkManager.IsHost && !m_AsyncOperation.isDone)
135+
{
136+
clients.Add(m_NetworkManager.LocalClientId);
137+
}
138+
139+
// If we are getting the list of clients that have not completed the
140+
// scene event, then add any clients that disconnected during this
141+
// scene event.
124142
clients.AddRange(ClientsThatDisconnected);
125143
}
126144
return clients;
@@ -138,6 +156,11 @@ internal SceneEventProgress(NetworkManager networkManager, SceneEventProgressSta
138156
// Track the clients that were connected when we started this event
139157
foreach (var connectedClientId in networkManager.ConnectedClientsIds)
140158
{
159+
// Ignore the host client
160+
if (NetworkManager.ServerClientId == connectedClientId)
161+
{
162+
continue;
163+
}
141164
ClientsProcessingSceneEvent.Add(connectedClientId, false);
142165
}
143166

@@ -218,7 +241,10 @@ private bool HasFinished()
218241
}
219242

220243
// Return the local scene event's AsyncOperation status
221-
return m_AsyncOperation.isDone;
244+
// Note: Integration tests process scene loading through a queue
245+
// and the AsyncOperation could not be assigned for several
246+
// network tick periods. Return false if that is the case.
247+
return m_AsyncOperation == null ? false : m_AsyncOperation.isDone;
222248
}
223249

224250
/// <summary>

com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,6 @@ protected void CreateServerAndClients()
274274
CreateServerAndClients(NumberOfClients);
275275
}
276276

277-
protected virtual void OnNewClientCreated(NetworkManager networkManager)
278-
{
279-
280-
}
281-
282-
protected virtual void OnNewClientStartedAndConnected(NetworkManager networkManager)
283-
{
284-
285-
}
286-
287277
private void AddRemoveNetworkManager(NetworkManager networkManager, bool addNetworkManager)
288278
{
289279
var clientNetworkManagersList = new List<NetworkManager>(m_ClientNetworkManagers);
@@ -299,6 +289,37 @@ private void AddRemoveNetworkManager(NetworkManager networkManager, bool addNetw
299289
m_NumberOfClients = clientNetworkManagersList.Count;
300290
}
301291

292+
/// <summary>
293+
/// CreateAndStartNewClient Only
294+
/// Invoked when the newly created client has been created
295+
/// </summary>
296+
protected virtual void OnNewClientCreated(NetworkManager networkManager)
297+
{
298+
299+
}
300+
301+
/// <summary>
302+
/// CreateAndStartNewClient Only
303+
/// Invoked when the newly created client has been created and started
304+
/// </summary>
305+
protected virtual void OnNewClientStarted(NetworkManager networkManager)
306+
{
307+
}
308+
309+
/// <summary>
310+
/// CreateAndStartNewClient Only
311+
/// Invoked when the newly created client has been created, started, and connected
312+
/// to the server-host.
313+
/// </summary>
314+
protected virtual void OnNewClientStartedAndConnected(NetworkManager networkManager)
315+
{
316+
317+
}
318+
319+
/// <summary>
320+
/// This will create, start, and connect a new client while in the middle of an
321+
/// integration test.
322+
/// </summary>
302323
protected IEnumerator CreateAndStartNewClient()
303324
{
304325
var networkManager = NetcodeIntegrationTestHelpers.CreateNewClient(m_ClientNetworkManagers.Length);
@@ -309,9 +330,15 @@ protected IEnumerator CreateAndStartNewClient()
309330
OnNewClientCreated(networkManager);
310331

311332
NetcodeIntegrationTestHelpers.StartOneClient(networkManager);
333+
312334
AddRemoveNetworkManager(networkManager, true);
335+
336+
OnNewClientStarted(networkManager);
337+
313338
// Wait for the new client to connect
314339
yield return WaitForClientsConnectedOrTimeOut();
340+
341+
OnNewClientStartedAndConnected(networkManager);
315342
if (s_GlobalTimeoutHelper.TimedOut)
316343
{
317344
AddRemoveNetworkManager(networkManager, false);
@@ -322,6 +349,9 @@ protected IEnumerator CreateAndStartNewClient()
322349
VerboseDebug($"[{networkManager.name}] Created and connected!");
323350
}
324351

352+
/// <summary>
353+
/// This will stop a client while in the middle of an integration test
354+
/// </summary>
325355
protected IEnumerator StopOneClient(NetworkManager networkManager, bool destroy = false)
326356
{
327357
NetcodeIntegrationTestHelpers.StopOneClient(networkManager, destroy);

testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationValidationTest.cs

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Collections;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using NUnit.Framework;
54
using UnityEngine;
65
using UnityEngine.SceneManagement;
@@ -12,48 +11,23 @@ namespace TestProject.RuntimeTests
1211
{
1312
public class ClientSynchronizationValidationTest : NetcodeIntegrationTest
1413
{
15-
protected override int NumberOfClients => 1;
14+
protected override int NumberOfClients => 0;
1615
private const string k_FirstSceneToLoad = "UnitTestBaseScene";
17-
private const string k_SecondSceneToSkip = "InSceneNetworkObject";
18-
private const string k_ThirdSceneToLoad = "EmptyScene";
19-
private bool m_CanStartServerAndClients;
20-
private List<ClientSceneVerificationHandler> m_ClientSceneVerifiers = new List<ClientSceneVerificationHandler>();
16+
private const string k_SecondSceneToLoad = "InSceneNetworkObject";
17+
private const string k_ThirdSceneToSkip = "EmptyScene";
2118

22-
protected override bool CanStartServerAndClients()
23-
{
24-
return m_CanStartServerAndClients;
25-
}
19+
private List<ClientSceneVerificationHandler> m_ClientSceneVerifiers = new List<ClientSceneVerificationHandler>();
2620

27-
protected override IEnumerator OnStartedServerAndClients()
21+
protected override void OnNewClientStarted(NetworkManager networkManager)
2822
{
29-
// Create ClientSceneVerificationHandlers for each client
30-
foreach (var client in m_ClientNetworkManagers)
31-
{
32-
m_ClientSceneVerifiers.Add(new ClientSceneVerificationHandler(client));
33-
}
34-
return base.OnStartedServerAndClients();
23+
m_ClientSceneVerifiers.Add(new ClientSceneVerificationHandler(networkManager));
24+
base.OnNewClientStarted(networkManager);
3525
}
3626

3727
[UnityTest]
3828
public IEnumerator ClientVerifySceneBeforeLoading()
3929
{
40-
// Because despawning a client will cause it to shutdown and clean everything in the
41-
// scene hierarchy, we have to prevent one of the clients from spawning initially before
42-
// we test synchronizing late joining clients.
43-
// So, we prevent the automatic starting of the server and clients, remove the client we
44-
// will be targeting to join late from the m_ClientNetworkManagers array, start the server
45-
// and the remaining client, despawn the in-scene NetworkObject, and then start and synchronize
46-
// the clientToTest.
47-
var clientToTest = m_ClientNetworkManagers[0];
48-
var clients = m_ClientNetworkManagers.ToList();
49-
clients.Remove(clientToTest);
50-
m_ClientNetworkManagers = clients.ToArray();
51-
m_CanStartServerAndClients = true;
52-
yield return StartServerAndClients();
53-
clients.Add(clientToTest);
54-
m_ClientNetworkManagers = clients.ToArray();
55-
56-
var scenesToLoad = new List<string>() { k_FirstSceneToLoad, k_SecondSceneToSkip, k_ThirdSceneToLoad };
30+
var scenesToLoad = new List<string>() { k_FirstSceneToLoad, k_SecondSceneToLoad, k_ThirdSceneToSkip };
5731
m_ServerNetworkManager.SceneManager.OnLoadComplete += OnLoadComplete;
5832
foreach (var sceneToLoad in scenesToLoad)
5933
{
@@ -65,15 +39,10 @@ public IEnumerator ClientVerifySceneBeforeLoading()
6539
AssertOnTimeout($"Timed out waiting for scene {m_SceneBeingLoaded} to finish loading!");
6640
}
6741

68-
// Now late join a client to make sure the client synchronizes to 2 of the 3 scenes loaded
69-
NetcodeIntegrationTestHelpers.StartOneClient(clientToTest);
70-
yield return WaitForConditionOrTimeOut(() => (clientToTest.IsConnectedClient && clientToTest.IsListening));
71-
AssertOnTimeout($"Timed out waiting for {clientToTest.name} to reconnect!");
72-
73-
yield return s_DefaultWaitForTick;
42+
yield return CreateAndStartNewClient();
7443

75-
// Update the newly joined client information
76-
ClientNetworkManagerPostStartInit();
44+
yield return WaitForConditionOrTimeOut(m_ClientSceneVerifiers[0].HasLoadedExpectedScenes);
45+
AssertOnTimeout($"Timed out waiting for the client to have loaded the expected scenes");
7746

7847
// Check to make sure only the two scenes were loaded and one
7948
// completely skipped.
@@ -110,15 +79,20 @@ public ClientSceneVerificationHandler(NetworkManager networkManager)
11079
m_NetworkManager.SceneManager.OnLoad += ClientSceneManager_OnLoad;
11180
m_NetworkManager.SceneManager.OnLoadComplete += ClientSceneManager_OnLoadComplete;
11281
m_ValidSceneEventCount.Add(k_FirstSceneToLoad, 0);
113-
m_ValidSceneEventCount.Add(k_SecondSceneToSkip, 0);
114-
m_ValidSceneEventCount.Add(k_ThirdSceneToLoad, 0);
82+
m_ValidSceneEventCount.Add(k_SecondSceneToLoad, 0);
83+
m_ValidSceneEventCount.Add(k_ThirdSceneToSkip, 0);
84+
}
85+
86+
public bool HasLoadedExpectedScenes()
87+
{
88+
return m_ValidSceneEventCount[k_FirstSceneToLoad] == 2 && m_ValidSceneEventCount[k_SecondSceneToLoad] == 2;
11589
}
11690

11791
public void ValidateScenesLoaded()
11892
{
119-
Assert.IsFalse(m_ValidSceneEventCount[k_SecondSceneToSkip] > 0, $"Client still loaded the invalidated scene {k_SecondSceneToSkip}!");
120-
Assert.IsTrue(m_ValidSceneEventCount[k_FirstSceneToLoad] == 1, $"Client did not load and process the validated scene {k_FirstSceneToLoad}! Expected (1) but was ({m_ValidSceneEventCount[k_FirstSceneToLoad]})");
121-
Assert.IsTrue(m_ValidSceneEventCount[k_ThirdSceneToLoad] == 1, $"Client did not load and process the validated scene {k_ThirdSceneToLoad}! Expected (1) but was ({m_ValidSceneEventCount[k_ThirdSceneToLoad]})");
93+
Assert.IsTrue(m_ValidSceneEventCount[k_ThirdSceneToSkip] == 0, $"Client still loaded the invalidated scene {k_ThirdSceneToSkip}!");
94+
Assert.IsTrue(m_ValidSceneEventCount[k_FirstSceneToLoad] == 2, $"Client did not load and process the validated scene {k_FirstSceneToLoad}! Expected (1) but was ({m_ValidSceneEventCount[k_FirstSceneToLoad]})");
95+
Assert.IsTrue(m_ValidSceneEventCount[k_SecondSceneToLoad] == 2, $"Client did not load and process the validated scene {k_SecondSceneToLoad}! Expected (1) but was ({m_ValidSceneEventCount[k_SecondSceneToLoad]})");
12296
}
12397

12498
private void ClientSceneManager_OnLoadComplete(ulong clientId, string sceneName, LoadSceneMode loadSceneMode)
@@ -139,7 +113,7 @@ private void ClientSceneManager_OnLoad(ulong clientId, string sceneName, LoadSce
139113

140114
private bool VerifySceneBeforeLoading(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
141115
{
142-
if (sceneName == k_SecondSceneToSkip)
116+
if (sceneName == k_ThirdSceneToSkip)
143117
{
144118
return false;
145119
}

testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerUsageTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,41 @@ private void ClientSceneManager_OnLoadComplete(ulong clientId, string sceneName,
134134
m_ClientLoadedScene = true;
135135
}
136136
}
137+
138+
private int m_LoadEventCompletedInvocationCount;
139+
140+
/// <summary>
141+
/// This test validates that a host only receives one OnLoadEventCompleted callback per scene loading event when
142+
/// no clients are connected.
143+
/// Note: the fix was within SceneEventProgress but is associated with NetworkSceneManager
144+
/// </summary>
145+
[UnityTest]
146+
public IEnumerator HostReceivesOneLoadEventCompletedNotification()
147+
{
148+
// Host only test
149+
if (!m_UseHost)
150+
{
151+
yield break;
152+
}
153+
154+
yield return StopOneClient(m_ClientNetworkManagers[0]);
155+
m_LoadEventCompletedInvocationCount = 0;
156+
m_ServerNetworkManager.SceneManager.OnLoadEventCompleted += SceneManager_OnLoadEventCompleted;
157+
158+
var retStatus = m_ServerNetworkManager.SceneManager.LoadScene(k_AdditiveScene1, LoadSceneMode.Additive);
159+
Assert.AreEqual(retStatus, SceneEventProgressStatus.Started);
160+
yield return WaitForConditionOrTimeOut(() => m_LoadEventCompletedInvocationCount > 0);
161+
AssertOnTimeout($"Host timed out loading scene {k_AdditiveScene1} additively!");
162+
163+
// Wait one tick to make sure any other notifications are not triggered.
164+
yield return s_DefaultWaitForTick;
165+
166+
Assert.IsTrue(m_LoadEventCompletedInvocationCount == 1, $"Expected OnLoadEventCompleted to be triggered once but was triggered {m_LoadEventCompletedInvocationCount} times!");
167+
}
168+
169+
private void SceneManager_OnLoadEventCompleted(string sceneName, LoadSceneMode loadSceneMode, System.Collections.Generic.List<ulong> clientsCompleted, System.Collections.Generic.List<ulong> clientsTimedOut)
170+
{
171+
m_LoadEventCompletedInvocationCount++;
172+
}
137173
}
138174
}

0 commit comments

Comments
 (0)