Skip to content

Commit 6c46dfd

Browse files
fix: network objects to synchronize scene changes edge cases (#3230)
* fix Clean up scene migration synchronization table entry when GameObject is destroyed when it should not have been. * fix Clean up scene migration synchronization table when NetworkManager shuts down. * fix adding check for any in-scene placed NetworkObjects. removing whenever destroyed (if entry still exists). * test Minor test to validate the NetworkObject is no longer within the NetworkObject.NetworkObjectsToSynchronizeSceneChanges list upon destroying it prior to it being despawned. * update Adding change log entries. * fix Removing static NetworkObjectsToSynchronizeSceneChanges and CleanUpDisposedObjects and migrating them to normal properties within the NetworkSpawnManager. * test Adding a validation at the end of the scene load-unload test to assure the NetworkObjectsToSynchronizeSceneChanges has no remaining in-scene placed NetworkObjects when all scenes are unloaded. Removing a verbose setting from InScenePlacedNetworkObjectTests. * fix Fixing issue with logic for scene migration synchronization. * update switching from a list to a stack * test Had to adjust for the changes from static to NetworkManager relative (for DA mode).
1 parent 4ff5b8e commit 6c46dfd

File tree

7 files changed

+120
-57
lines changed

7 files changed

+120
-57
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
1313
### Fixed
1414

1515
- Fixed exception being thrown when a `GameObject` with an associated `NetworkTransform` is disabled. (#3243)
16+
- Fixed issue where the scene migration synchronization table was not cleaned up if the `GameObject` of a `NetworkObject` is destroyed before it should have been. (#3230)
17+
- Fixed issue where the scene migration synchronization table was not cleaned up upon `NetworkManager` shutting down. (#3230)
1618
- Fixed `NetworkObject.DeferDespawn` to respect the `DestroyGameObject` parameter. (#3219)
1719
- Fixed issue where a `NetworkObject` with nested `NetworkTransform` components of varying authority modes was not being taken into consideration and would break both the initial `NetworkTransform` synchronization and fail to properly handle synchronized state updates of the nested `NetworkTransform` components. (#3209)
1820
- Fixed issue with distributing parented children that have the distributable and/or transferrable permissions set and have the same owner as the root parent, that has the distributable permission set, were not being distributed to the same client upon the owning client disconnecting when using a distributed authority network topology. (#3203)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ public void NetworkUpdate(NetworkUpdateStage updateStage)
401401
}
402402

403403
// Update any NetworkObject's registered to notify of scene migration changes.
404-
NetworkObject.UpdateNetworkObjectSceneChanges();
404+
SpawnManager.UpdateNetworkObjectSceneChanges();
405405

406406
// This should be invoked just prior to the MessageManager processes its outbound queue.
407407
SceneManager.CheckForAndSendNetworkObjectSceneChanged();

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

+21-43
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ private void CheckForInScenePlaced()
319319
EditorUtility.SetDirty(this);
320320
}
321321
IsSceneObject = true;
322+
323+
// Default scene migration synchronization to false for in-scene placed NetworkObjects
324+
SceneMigrationSynchronization = false;
322325
}
323326
}
324327
#endif // UNITY_EDITOR
@@ -1596,6 +1599,9 @@ private void OnDestroy()
15961599
if (NetworkManager.IsListening && !isAuthority && IsSpawned &&
15971600
(IsSceneObject == null || (IsSceneObject.Value != true)))
15981601
{
1602+
// If we destroyed a GameObject with a NetworkObject component on the non-authority side, handle cleaning up the SceneMigrationSynchronization.
1603+
NetworkManager.SpawnManager?.RemoveNetworkObjectFromSceneChangedUpdates(this);
1604+
15991605
// Clients should not despawn NetworkObjects while connected to a session, but we don't want to destroy the current call stack
16001606
// if this happens. Instead, we should just generate a network log error and exit early (as long as we are not shutting down).
16011607
if (!NetworkManager.ShutdownInProgress)
@@ -1617,6 +1623,9 @@ private void OnDestroy()
16171623
// Otherwise, clients can despawn NetworkObjects while shutting down and should not generate any messages when this happens
16181624
}
16191625

1626+
// Always attempt to remove from scene changed updates
1627+
NetworkManager.SpawnManager?.RemoveNetworkObjectFromSceneChangedUpdates(this);
1628+
16201629
if (NetworkManager.SpawnManager != null && NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject))
16211630
{
16221631
if (this == networkObject)
@@ -2392,11 +2401,6 @@ internal void InvokeBehaviourNetworkSpawn()
23922401
{
23932402
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId);
23942403

2395-
if (SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement)
2396-
{
2397-
AddNetworkObjectToSceneChangedUpdates(this);
2398-
}
2399-
24002404
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
24012405
{
24022406
if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy)
@@ -2451,21 +2455,15 @@ internal void InternalInSceneNetworkObjectsSpawned()
24512455
}
24522456
}
24532457

2454-
2455-
24562458
internal void InvokeBehaviourNetworkDespawn()
24572459
{
24582460
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true);
2461+
NetworkManager.SpawnManager.RemoveNetworkObjectFromSceneChangedUpdates(this);
24592462

24602463
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
24612464
{
24622465
ChildNetworkBehaviours[i].InternalOnNetworkDespawn();
24632466
}
2464-
2465-
if (SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement)
2466-
{
2467-
RemoveNetworkObjectFromSceneChangedUpdates(this);
2468-
}
24692467
}
24702468

24712469
private List<NetworkBehaviour> m_ChildNetworkBehaviours;
@@ -3276,31 +3274,6 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false)
32763274
}
32773275
}
32783276

3279-
internal static Dictionary<ulong, NetworkObject> NetworkObjectsToSynchronizeSceneChanges = new Dictionary<ulong, NetworkObject>();
3280-
3281-
internal static void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject)
3282-
{
3283-
if (!NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId))
3284-
{
3285-
NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject);
3286-
}
3287-
3288-
networkObject.UpdateForSceneChanges();
3289-
}
3290-
3291-
internal static void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject)
3292-
{
3293-
NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId);
3294-
}
3295-
3296-
internal static void UpdateNetworkObjectSceneChanges()
3297-
{
3298-
foreach (var entry in NetworkObjectsToSynchronizeSceneChanges)
3299-
{
3300-
entry.Value.UpdateForSceneChanges();
3301-
}
3302-
}
3303-
33043277
private void Awake()
33053278
{
33063279
m_ChildNetworkBehaviours = null;
@@ -3323,20 +3296,25 @@ private void Awake()
33233296
/// to add this same functionality to in-scene placed NetworkObjects until we have a way to generate
33243297
/// per-NetworkObject-instance unique GlobalObjectIdHash values for in-scene placed NetworkObjects.
33253298
/// </remarks>
3326-
internal void UpdateForSceneChanges()
3299+
internal bool UpdateForSceneChanges()
33273300
{
33283301
// Early exit if SceneMigrationSynchronization is disabled, there is no NetworkManager assigned,
33293302
// the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed
33303303
// NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle
33313304
if (!SceneMigrationSynchronization || !IsSpawned || NetworkManager == null || NetworkManager.ShutdownInProgress ||
3332-
!NetworkManager.NetworkConfig.EnableSceneManagement || IsSceneObject != false || gameObject.scene.handle == SceneOriginHandle)
3305+
!NetworkManager.NetworkConfig.EnableSceneManagement || IsSceneObject != false || !gameObject)
33333306
{
3334-
return;
3307+
// Stop checking for a scene migration
3308+
return false;
3309+
}
3310+
else if (gameObject.scene.handle != SceneOriginHandle)
3311+
{
3312+
// If the scene handle has changed, then update and send notification
3313+
SceneChangedUpdate(gameObject.scene, true);
33353314
}
33363315

3337-
// Otherwise, this has to be a dynamically spawned NetworkObject that has been
3338-
// migrated to a new scene.
3339-
SceneChangedUpdate(gameObject.scene, true);
3316+
// Return true (continue checking for scene migration)
3317+
return true;
33403318
}
33413319

33423320
/// <summary>

com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs

+57-11
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,6 @@ private void RemovePlayerObject(NetworkObject playerObject, bool destroyingObjec
153153
{
154154
NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].PlayerObject = null;
155155
}
156-
157-
// If we want to keep the observers, then exit early
158-
//if (keepObservers)
159-
//{
160-
// return;
161-
//}
162-
163-
//foreach (var player in m_PlayerObjects)
164-
//{
165-
// player.Observers.Remove(playerObject.OwnerClientId);
166-
//}
167156
}
168157

169158
internal void MarkObjectForShowingTo(NetworkObject networkObject, ulong clientId)
@@ -1161,6 +1150,8 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
11611150
networkObject.ApplyNetworkParenting();
11621151
NetworkObject.CheckOrphanChildren();
11631152

1153+
AddNetworkObjectToSceneChangedUpdates(networkObject);
1154+
11641155
networkObject.InvokeBehaviourNetworkSpawn();
11651156

11661157
NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkId);
@@ -1196,6 +1187,50 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
11961187
}
11971188
}
11981189

1190+
internal Dictionary<ulong, NetworkObject> NetworkObjectsToSynchronizeSceneChanges = new Dictionary<ulong, NetworkObject>();
1191+
1192+
// Pre-allocating to avoid the initial constructor hit
1193+
internal Stack<ulong> CleanUpDisposedObjects = new Stack<ulong>();
1194+
1195+
internal void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject)
1196+
{
1197+
if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) &&
1198+
!NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId))
1199+
{
1200+
if (networkObject.UpdateForSceneChanges())
1201+
{
1202+
NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject);
1203+
}
1204+
}
1205+
}
1206+
1207+
internal void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject)
1208+
{
1209+
if ((networkObject.SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) &&
1210+
NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId))
1211+
{
1212+
NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId);
1213+
}
1214+
}
1215+
1216+
internal unsafe void UpdateNetworkObjectSceneChanges()
1217+
{
1218+
foreach (var entry in NetworkObjectsToSynchronizeSceneChanges)
1219+
{
1220+
// If it fails the first update then don't add for updates
1221+
if (!entry.Value.UpdateForSceneChanges())
1222+
{
1223+
CleanUpDisposedObjects.Push(entry.Key);
1224+
}
1225+
}
1226+
1227+
// Clean up any NetworkObjects that no longer exist (destroyed before they should be or the like)
1228+
while (CleanUpDisposedObjects.Count > 0)
1229+
{
1230+
NetworkObjectsToSynchronizeSceneChanges.Remove(CleanUpDisposedObjects.Pop());
1231+
}
1232+
}
1233+
11991234
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
12001235
{
12011236
// If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message.
@@ -1729,6 +1764,17 @@ internal NetworkSpawnManager(NetworkManager networkManager)
17291764
NetworkManager = networkManager;
17301765
}
17311766

1767+
~NetworkSpawnManager()
1768+
{
1769+
Shutdown();
1770+
}
1771+
1772+
internal void Shutdown()
1773+
{
1774+
NetworkObjectsToSynchronizeSceneChanges.Clear();
1775+
CleanUpDisposedObjects.Clear();
1776+
}
1777+
17321778
/// <summary>
17331779
/// DANGO-TODO: Until we have the CMB Server end-to-end with all features verified working via integration tests,
17341780
/// I am keeping this debug toggle available. (NSS)

com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs

+15
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ internal class NetworkObjectDestroyTests : NetcodeIntegrationTest
2323

2424
public NetworkObjectDestroyTests(NetworkTopologyTypes networkTopologyType) : base(networkTopologyType) { }
2525

26+
protected override void OnCreatePlayerPrefab()
27+
{
28+
var playerNetworkObject = m_PlayerPrefab.GetComponent<NetworkObject>();
29+
playerNetworkObject.SceneMigrationSynchronization = true;
30+
base.OnCreatePlayerPrefab();
31+
}
32+
2633
/// <summary>
2734
/// Tests that a server can destroy a NetworkObject and that it gets despawned correctly.
2835
/// </summary>
@@ -133,6 +140,14 @@ public IEnumerator TestNetworkObjectClientDestroy([Values] ClientDestroyObject c
133140
yield return WaitForConditionOrTimeOut(HaveLogsBeenReceived);
134141
AssertOnTimeout($"Not all expected logs were received when destroying a {nameof(NetworkObject)} on the client side during an active session!");
135142
}
143+
if (m_DistributedAuthority)
144+
{
145+
Assert.IsFalse(m_ClientNetworkManagers[1].SpawnManager.NetworkObjectsToSynchronizeSceneChanges.ContainsKey(m_ClientNetworkObjectId), $"Player object {m_ClientNetworkObjectId} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!");
146+
}
147+
else
148+
{
149+
Assert.IsFalse(m_ClientNetworkManagers[0].SpawnManager.NetworkObjectsToSynchronizeSceneChanges.ContainsKey(m_ClientNetworkObjectId), $"Player object {m_ClientNetworkObjectId} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!");
150+
}
136151
}
137152

138153
private bool HaveLogsBeenReceived()

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public IEnumerator InSceneNetworkObjectSynchAndSpawn([Values] DespawnMode despaw
7474
Assert.Ignore($"Test ignored as DeferDespawn is only valid with Distributed Authority mode.");
7575
}
7676

77-
NetworkObjectTestComponent.VerboseDebug = true;
77+
NetworkObjectTestComponent.VerboseDebug = false;
7878
// Because despawning a client will cause it to shutdown and clean everything in the
7979
// scene hierarchy, we have to prevent one of the clients from spawning initially before
8080
// we test synchronizing late joining clients with despawned in-scene placed NetworkObjects.

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

+23-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Text;
56
using NUnit.Framework;
67
using Unity.Netcode;
78
using Unity.Netcode.TestHelpers.Runtime;
@@ -309,6 +310,8 @@ private bool DataPoolVerifySceneClient(int sceneIndex, string sceneName, LoadSce
309310
return true;
310311
}
311312

313+
314+
private StringBuilder m_ErrorMsg = new StringBuilder();
312315
/// <summary>
313316
/// Small to heavy scene loading scenario to test the dynamically generated SceneEventData objects under a load.
314317
/// Will load from 1 to 32 scenes in both single and additive ClientSynchronizationMode
@@ -358,11 +361,30 @@ public IEnumerator SceneEventDataPoolSceneLoadingTest([Values(1, 2, 4, 6)] int n
358361
}
359362

360363
yield return UnloadAllScenes(true);
364+
Assert.IsTrue(CheckNetworkObjectsToSynchronizeSceneChanges(m_ServerNetworkManager), $"{nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)} validation check failure!\n {m_ErrorMsg}");
365+
m_ServerNetworkManager.SceneManager.OnSceneEvent -= ServerSceneManager_OnSceneEvent;
366+
// Validate that the NetworkObjectsToSynchronizeSceneChanges does not persist entries when scenes are unloaded.
361367
foreach (var client in m_ClientNetworkManagers)
362368
{
369+
Assert.IsTrue(CheckNetworkObjectsToSynchronizeSceneChanges(client), $"{nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)} validation check failure!\n {m_ErrorMsg}");
363370
client.SceneManager.OnUnloadComplete -= SceneManager_OnUnloadComplete;
364371
}
365-
m_ServerNetworkManager.SceneManager.OnSceneEvent -= ServerSceneManager_OnSceneEvent;
372+
}
373+
374+
private bool CheckNetworkObjectsToSynchronizeSceneChanges(NetworkManager networkManager)
375+
{
376+
m_ErrorMsg.Clear();
377+
if (networkManager.SpawnManager.NetworkObjectsToSynchronizeSceneChanges.Count > 0)
378+
{
379+
foreach (var entry in networkManager.SpawnManager.NetworkObjectsToSynchronizeSceneChanges)
380+
{
381+
if (entry.Value.IsSceneObject.HasValue && entry.Value.IsSceneObject.Value)
382+
{
383+
m_ErrorMsg.AppendLine($"{entry.Value.name} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!");
384+
}
385+
}
386+
}
387+
return m_ErrorMsg.Length == 0;
366388
}
367389

368390
private string m_SceneBeingUnloaded;

0 commit comments

Comments
 (0)