Skip to content

Commit c0c6b02

Browse files
fix: soft synchronization error occurs on late-joining client when an in-scene placed NetworkObject has been parented to another dynamically spawned NetworkObject [MTT-3615] (#1985)
1 parent 79ab5b6 commit c0c6b02

23 files changed

+3066
-65
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

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

2222
### Fixed
2323

24+
- Fixed issue where late joining clients would get a soft synchronization error if any in-scene placed NetworkObjects were parented under another `NetworkObject`. (#1985)
2425
- Fixed issue where `NetworkBehaviourReference` would throw a type cast exception if using `NetworkBehaviourReference.TryGet` and the component type was not found. (#1984)
2526
- Fixed `NetworkSceneManager` was not sending scene event notifications for the currently active scene and any additively loaded scenes when loading a new scene in `LoadSceneMode.Single` mode. (#1975)
2627
- Fixed issue where one or more clients disconnecting during a scene event would cause `LoadEventCompleted` or `UnloadEventCompleted` to wait until the `NetworkConfig.LoadSceneTimeOut` period before being triggered. (#1973)

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

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Runtime.CompilerServices;
44
using UnityEngine;
5+
using UnityEngine.SceneManagement;
56

67
namespace Unity.Netcode
78
{
@@ -180,9 +181,58 @@ public bool IsNetworkVisibleTo(ulong clientId)
180181
return Observers.Contains(clientId);
181182
}
182183

184+
/// <summary>
185+
/// In the event the scene of origin gets unloaded, we keep
186+
/// the most important part to uniquely identify in-scene
187+
/// placed NetworkObjects
188+
/// </summary>
189+
internal int SceneOriginHandle = 0;
190+
191+
private Scene m_SceneOrigin;
192+
/// <summary>
193+
/// The scene where the NetworkObject was first instantiated
194+
/// Note: Primarily for in-scene placed NetworkObjects
195+
/// We need to keep track of the original scene of origin for
196+
/// the NetworkObject in order to be able to uniquely identify it
197+
/// using the scene of origin's handle.
198+
/// </summary>
199+
internal Scene SceneOrigin
200+
{
201+
get
202+
{
203+
return m_SceneOrigin;
204+
}
205+
206+
set
207+
{
208+
// The scene origin should only be set once.
209+
// Once set, it should never change.
210+
if (SceneOriginHandle == 0 && value.IsValid() && value.isLoaded)
211+
{
212+
m_SceneOrigin = value;
213+
SceneOriginHandle = value.handle;
214+
}
215+
}
216+
}
217+
218+
/// <summary>
219+
/// Helper method to return the correct scene handle
220+
/// Note: Do not use this within NetworkSpawnManager.SpawnNetworkObjectLocallyCommon
221+
/// </summary>
222+
internal int GetSceneOriginHandle()
223+
{
224+
// Sanity check to make sure nothing
225+
if (SceneOriginHandle == 0 && IsSpawned)
226+
{
227+
throw new Exception($"{nameof(GetSceneOriginHandle)} called when {nameof(SceneOriginHandle)} is still zero but the {nameof(NetworkObject)} is already spawned!");
228+
}
229+
return SceneOriginHandle != 0 ? SceneOriginHandle : gameObject.scene.handle;
230+
}
231+
183232
private void Awake()
184233
{
185234
SetCachedParent(transform.parent);
235+
SceneOrigin = gameObject.scene;
186236
}
187237

188238
/// <summary>
@@ -909,7 +959,7 @@ public unsafe void Serialize(FastBufferWriter writer)
909959
// sizes for dynamically spawned NetworkObjects.
910960
if (Header.IsSceneObject)
911961
{
912-
writer.WriteValue(OwnerObject.gameObject.scene.handle);
962+
writer.WriteValue(OwnerObject.GetSceneOriginHandle());
913963
}
914964

915965
OwnerObject.WriteNetworkVariableData(writer, TargetClientId);
@@ -959,7 +1009,7 @@ public unsafe void Deserialize(FastBufferReader reader)
9591009
// NetworkObject instance.
9601010
if (Header.IsSceneObject)
9611011
{
962-
reader.ReadValue(out NetworkSceneHandle);
1012+
reader.ReadValueSafe(out NetworkSceneHandle);
9631013
}
9641014
}
9651015
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,13 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene)
15201520
EndSceneEvent(sceneEventId);
15211521
}
15221522

1523+
/// <summary>
1524+
/// Used for integration testing, due to the complexities of having all clients loading scenes
1525+
/// this is needed to "filter" out the scenes not loaded by NetworkSceneManager
1526+
/// (i.e. we don't want a late joining player to load all of the other client scenes)
1527+
/// </summary>
1528+
internal Func<Scene, bool> ExcludeSceneFromSychronization;
1529+
15231530
/// <summary>
15241531
/// Server Side:
15251532
/// This is used for players that have just had their connection approved and will assure they are synchronized
@@ -1546,6 +1553,13 @@ internal void SynchronizeNetworkObjects(ulong clientId)
15461553
{
15471554
var scene = SceneManager.GetSceneAt(i);
15481555

1556+
// NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager
1557+
// unless the scene in question is the currently active scene.
1558+
if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene))
1559+
{
1560+
continue;
1561+
}
1562+
15491563
var sceneHash = SceneHashFromNameOrPath(scene.path);
15501564

15511565
// This would depend upon whether we are additive or not
@@ -1563,7 +1577,6 @@ internal void SynchronizeNetworkObjects(ulong clientId)
15631577
{
15641578
continue;
15651579
}
1566-
15671580
sceneEventData.AddSceneToSynchronize(sceneHash, scene.handle);
15681581
}
15691582

@@ -2020,10 +2033,9 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
20202033
foreach (var networkObjectInstance in networkObjects)
20212034
{
20222035
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
2023-
var sceneHandle = networkObjectInstance.gameObject.scene.handle;
2036+
var sceneHandle = networkObjectInstance.GetSceneOriginHandle();
20242037
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
2025-
if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && networkObjectInstance.gameObject.scene == sceneToFilterBy &&
2026-
sceneHandle == sceneToFilterBy.handle)
2038+
if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && sceneHandle == sceneToFilterBy.handle)
20272039
{
20282040
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
20292041
{

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,6 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer)
388388
writer.WriteValueSafe(ScenesToSynchronize.ToArray());
389389
writer.WriteValueSafe(SceneHandlesToSynchronize.ToArray());
390390

391-
392391
// Store our current position in the stream to come back and say how much data we have written
393392
var positionStart = writer.Position;
394393

@@ -399,13 +398,13 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer)
399398
int totalBytes = 0;
400399

401400
// Write the number of NetworkObjects we are serializing
402-
writer.WriteValueSafe(m_NetworkObjectsSync.Count());
401+
BytePacker.WriteValuePacked(writer, m_NetworkObjectsSync.Count());
403402
// Serialize all NetworkObjects that are spawned
404403
for (var i = 0; i < m_NetworkObjectsSync.Count(); ++i)
405404
{
406405
var noStart = writer.Position;
407406
var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId);
408-
writer.WriteValueSafe(m_NetworkObjectsSync[i].gameObject.scene.handle);
407+
BytePacker.WriteValuePacked(writer, m_NetworkObjectsSync[i].GetSceneOriginHandle());
409408
sceneObject.Serialize(writer);
410409
var noStop = writer.Position;
411410
totalBytes += (int)(noStop - noStart);
@@ -418,8 +417,8 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer)
418417
{
419418
var noStart = writer.Position;
420419
var sceneObject = m_DespawnedInSceneObjectsSync[i].GetMessageSceneObject(TargetClientId);
421-
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].gameObject.scene.handle);
422-
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
420+
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle());
421+
BytePacker.WriteValuePacked(writer, m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash);
423422
var noStop = writer.Position;
424423
totalBytes += (int)(noStop - noStart);
425424
}
@@ -714,14 +713,15 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
714713
try
715714
{
716715
// Process all spawned NetworkObjects for this network session
717-
InternalBuffer.ReadValueSafe(out int newObjectsCount);
716+
ByteUnpacker.ReadValuePacked(InternalBuffer, out int newObjectsCount);
717+
718718

719719
for (int i = 0; i < newObjectsCount; i++)
720720
{
721721
// We want to make sure for each NetworkObject we have the appropriate scene selected as the scene that is
722722
// currently being synchronized. This assures in-scene placed NetworkObjects will use the right NetworkObject
723723
// from the list of populated <see cref="NetworkSceneManager.ScenePlacedObjects"/>
724-
InternalBuffer.ReadValueSafe(out int handle);
724+
ByteUnpacker.ReadValuePacked(InternalBuffer, out int handle);
725725
m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(handle);
726726

727727
var sceneObject = new NetworkObject.SceneObject();
@@ -742,8 +742,8 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
742742
for (int i = 0; i < despawnedObjectsCount; i++)
743743
{
744744
// We just need to get the scene
745-
InternalBuffer.ReadValueSafe(out int networkSceneHandle);
746-
InternalBuffer.ReadValueSafe(out uint globalObjectIdHash);
745+
ByteUnpacker.ReadValuePacked(InternalBuffer, out int networkSceneHandle);
746+
ByteUnpacker.ReadValuePacked(InternalBuffer, out uint globalObjectIdHash);
747747
var sceneRelativeNetworkObjects = new Dictionary<uint, NetworkObject>();
748748
if (!sceneCache.ContainsKey(networkSceneHandle))
749749
{
@@ -753,8 +753,8 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
753753
if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle))
754754
{
755755
var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle];
756-
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) => c.gameObject.scene == objectRelativeScene &&
757-
c.gameObject.scene.handle == localSceneHandle && (c.IsSceneObject != false)).ToList();
756+
var inSceneNetworkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) =>
757+
c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList();
758758

759759
foreach (var inSceneObject in inSceneNetworkObjects)
760760
{
@@ -789,9 +789,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager)
789789
m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary<int, NetworkObject>());
790790
}
791791

792-
if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].gameObject.scene.handle))
792+
if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle()))
793793
{
794-
m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].gameObject.scene.handle, sceneRelativeNetworkObjects[globalObjectIdHash]);
794+
m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(sceneRelativeNetworkObjects[globalObjectIdHash].GetSceneOriginHandle(), sceneRelativeNetworkObjects[globalObjectIdHash]);
795795
}
796796

797797
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -477,16 +477,16 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
477477
return;
478478
}
479479

480-
// this initialization really should be at the bottom of the function
481480
networkObject.IsSpawned = true;
482-
483-
// this initialization really should be at the top of this function. If and when we break the
484-
// NetworkVariable dependency on NetworkBehaviour, this otherwise creates problems because
485-
// SetNetworkVariableData above calls InitializeVariables, and the 'baked out' data isn't ready there;
486-
// the current design banks on getting the network behaviour set and then only reading from it after the
487-
// below initialization code. However cowardice compels me to hold off on moving this until that commit
488481
networkObject.IsSceneObject = sceneObject;
489482

483+
// Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects
484+
// Note: Always check SceneOriginHandle directly at this specific location.
485+
if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle == 0)
486+
{
487+
networkObject.SceneOrigin = networkObject.gameObject.scene;
488+
}
489+
490490
// For integration testing, this makes sure that the appropriate NetworkManager is assigned to
491491
// the NetworkObject since it uses the NetworkManager.Singleton when not set
492492
if (networkObject.NetworkManagerOwner != NetworkManager)

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ private static void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadScen
147147
private static void ProcessInSceneObjects(Scene scene, NetworkManager networkManager)
148148
{
149149
// Get all in-scene placed NeworkObjects that were instantiated when this scene loaded
150-
var inSceneNetworkObjects = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSceneObject != false && c.gameObject.scene.handle == scene.handle);
150+
var inSceneNetworkObjects = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSceneObject != false && c.GetSceneOriginHandle() == scene.handle);
151151
foreach (var sobj in inSceneNetworkObjects)
152152
{
153153
if (sobj.NetworkManagerOwner != networkManager)
@@ -322,7 +322,7 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
322322
var skip = false;
323323
foreach (var networkManager in NetworkManagers)
324324
{
325-
if (NetworkManager.LocalClientId == networkManager.LocalClientId)
325+
if (NetworkManager.LocalClientId == networkManager.LocalClientId || !networkManager.IsListening)
326326
{
327327
continue;
328328
}
@@ -352,12 +352,22 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
352352
throw new Exception($"Failed to find any loaded scene named {sceneName}!");
353353
}
354354

355+
private bool ExcludeSceneFromSynchronizationCheck(Scene scene)
356+
{
357+
if (!NetworkManager.SceneManager.ScenesLoaded.ContainsKey(scene.handle) && SceneManager.GetActiveScene().handle != scene.handle)
358+
{
359+
return false;
360+
}
361+
return true;
362+
}
363+
355364
/// <summary>
356365
/// Constructor now must take NetworkManager
357366
/// </summary>
358367
public IntegrationTestSceneHandler(NetworkManager networkManager)
359368
{
360369
networkManager.SceneManager.OverrideGetAndAddNewlyLoadedSceneByName = GetAndAddNewlyLoadedSceneByName;
370+
networkManager.SceneManager.ExcludeSceneFromSychronization = ExcludeSceneFromSynchronizationCheck;
361371
NetworkManagers.Add(networkManager);
362372
NetworkManagerName = networkManager.name;
363373
if (s_WaitForSeconds == null)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,22 @@ protected void ClientNetworkManagerPostStartInit()
411411
{
412412
ClientNetworkManagerPostStart(networkManager);
413413
}
414+
if (m_UseHost)
415+
{
416+
var clientSideServerPlayerClones = Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsPlayerObject && c.OwnerClientId == m_ServerNetworkManager.LocalClientId);
417+
foreach (var playerNetworkObject in clientSideServerPlayerClones)
418+
{
419+
// When the server is not the host this needs to be done
420+
if (!m_PlayerNetworkObjects.ContainsKey(playerNetworkObject.NetworkManager.LocalClientId))
421+
{
422+
m_PlayerNetworkObjects.Add(playerNetworkObject.NetworkManager.LocalClientId, new Dictionary<ulong, NetworkObject>());
423+
}
424+
if (!m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].ContainsKey(m_ServerNetworkManager.LocalClientId))
425+
{
426+
m_PlayerNetworkObjects[playerNetworkObject.NetworkManager.LocalClientId].Add(m_ServerNetworkManager.LocalClientId, playerNetworkObject);
427+
}
428+
}
429+
}
414430
}
415431

416432
/// <summary>

0 commit comments

Comments
 (0)