Skip to content

Commit 87e25ba

Browse files
fix: In-Scene Placed Object Parenting and Serialization Order (back port) (Unity-Technologies#3388)
This is the back port of Unity-Technologies#3387 that resolves some issues with in-scene placed NetworkObjects in parent-child hierarchy and the synchronization of late joining clients when the hierarchy has changed from that of its default hierarchy defined within the scene. [MTT-11883](https://jira.unity3d.com/browse/MTT-11883) ## Changelog - Fixed: Issue where in-scene placed `NetworkObjects` could fail to synchronize its transform properly (especially without a `NetworkTransform`) if their parenting changes from the default when the scene is loaded and if the same scene remains loaded between network sessions while the parenting is completely different from the original hierarchy. - Changed: The scene loading event serialization order for in-scene placed `NetworkObject`s to be based on their parent-child hierarchy. ## Testing and Documentation - Includes no additional tests (_requires manual testing due to scene loading constraints_). - No documentation changes or additions were necessary.
1 parent 9738a6e commit 87e25ba

File tree

4 files changed

+84
-60
lines changed

4 files changed

+84
-60
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ Additional documentation and release notes are available at [Multiplayer Documen
1616

1717
### Fixed
1818

19+
- Fixed issue where in-scene placed `NetworkObjects` could fail to synchronize its transform properly (especially without a `NetworkTransform`) if their parenting changes from the default when the scene is loaded and if the same scene remains loaded between network sessions while the parenting is completely different from the original hierarchy. (#3388)
1920
- Fixed an issue in `UnityTransport` where the transport would accept sends on invalid connections, leading to a useless memory allocation and confusing error message. (#3383)
2021
- Fixed issue where `NetworkAnimator` would log an error if there was no destination transition information. (#3384)
2122
- Fixed initial `NetworkTransform` spawn, ensure it uses world space. (#3361)
2223
- Fixed issue where `AnticipatedNetworkVariable` previous value returned by `AnticipatedNetworkVariable.OnAuthoritativeValueChanged` is updated correctly on the non-authoritative side. (#3322)
2324

2425
### Changed
2526

27+
- Changed the scene loading event serialization order for in-scene placed `NetworkObject`s to be based on their parent-child hierarchy. (#3388)
2628

2729
## [1.12.2] - 2025-01-17
2830

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

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,18 @@ private bool IsEditingPrefab()
185185
/// </remarks>
186186
private void CheckForInScenePlaced()
187187
{
188-
if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
188+
if (gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
189189
{
190-
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
191-
var assetPath = AssetDatabase.GetAssetPath(prefab);
192-
var sourceAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(assetPath);
193-
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
190+
if (PrefabUtility.IsPartOfAnyPrefab(this))
194191
{
195-
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
196-
EditorUtility.SetDirty(this);
192+
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
193+
var assetPath = AssetDatabase.GetAssetPath(prefab);
194+
var sourceAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(assetPath);
195+
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
196+
{
197+
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
198+
EditorUtility.SetDirty(this);
199+
}
197200
}
198201
IsSceneObject = true;
199202
}
@@ -1241,7 +1244,7 @@ private void OnTransformParentChanged()
12411244
// we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one.
12421245
internal static HashSet<NetworkObject> OrphanChildren = new HashSet<NetworkObject>();
12431246

1244-
internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false)
1247+
internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false, bool enableNotification = true)
12451248
{
12461249
if (!AutoObjectParentSync)
12471250
{
@@ -1314,7 +1317,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa
13141317
// to WorldPositionStays which can cause scaling issues if the parent's
13151318
// scale is not the default (Vetctor3.one) value.
13161319
transform.SetParent(null, m_CachedWorldPositionStays);
1317-
InvokeBehaviourOnNetworkObjectParentChanged(null);
1320+
if (enableNotification)
1321+
{
1322+
InvokeBehaviourOnNetworkObjectParentChanged(null);
1323+
}
13181324
return true;
13191325
}
13201326

@@ -1340,7 +1346,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa
13401346

13411347
m_CachedParent = parentObject.transform;
13421348
transform.SetParent(parentObject.transform, m_CachedWorldPositionStays);
1343-
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
1349+
if (enableNotification)
1350+
{
1351+
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
1352+
}
13441353
return true;
13451354
}
13461355

@@ -1819,6 +1828,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
18191828
{
18201829
var obj = new SceneObject
18211830
{
1831+
HasParent = transform.parent != null,
1832+
WorldPositionStays = m_CachedWorldPositionStays,
18221833
NetworkObjectId = NetworkObjectId,
18231834
OwnerClientId = OwnerClientId,
18241835
IsPlayerObject = IsPlayerObject,
@@ -1829,31 +1840,16 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
18291840
TargetClientId = targetClientId
18301841
};
18311842

1832-
NetworkObject parentNetworkObject = null;
1833-
1834-
if (!AlwaysReplicateAsRoot && transform.parent != null)
1843+
// Handle Parenting
1844+
if (!AlwaysReplicateAsRoot && obj.HasParent)
18351845
{
1836-
parentNetworkObject = transform.parent.GetComponent<NetworkObject>();
1837-
// In-scene placed NetworkObjects parented under GameObjects with no NetworkObject
1838-
// should set the has parent flag and preserve the world position stays value
1839-
if (parentNetworkObject == null && obj.IsSceneObject)
1840-
{
1841-
obj.HasParent = true;
1842-
obj.WorldPositionStays = m_CachedWorldPositionStays;
1843-
}
1844-
}
1846+
var parentNetworkObject = transform.parent.GetComponent<NetworkObject>();
18451847

1846-
if (parentNetworkObject != null)
1847-
{
1848-
obj.HasParent = true;
1849-
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
1850-
obj.WorldPositionStays = m_CachedWorldPositionStays;
1851-
var latestParent = GetNetworkParenting();
1852-
var isLatestParentSet = latestParent != null && latestParent.HasValue;
1853-
obj.IsLatestParentSet = isLatestParentSet;
1854-
if (isLatestParentSet)
1848+
if (parentNetworkObject)
18551849
{
1856-
obj.LatestParent = latestParent.Value;
1850+
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
1851+
obj.LatestParent = GetNetworkParenting();
1852+
obj.IsLatestParentSet = obj.LatestParent != null && obj.LatestParent.HasValue;
18571853
}
18581854
}
18591855

@@ -1866,12 +1862,6 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
18661862
var syncRotationPositionLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays;
18671863
var syncScaleLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays;
18681864

1869-
// Always synchronize in-scene placed object's scale using local space
1870-
if (obj.IsSceneObject)
1871-
{
1872-
syncScaleLocalSpaceRelative = obj.HasParent;
1873-
}
1874-
18751865
// If auto object synchronization is turned off
18761866
if (!AutoObjectParentSync)
18771867
{
@@ -1949,6 +1939,15 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
19491939
var bufferSerializer = new BufferSerializer<BufferSerializerReader>(new BufferSerializerReader(reader));
19501940
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
19511941

1942+
// If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are
1943+
// being told we do not have a parent, then we want to clear the latest parent so it is not automatically
1944+
// "re-parented" to the original parent. This can happen if not unloading the scene and the parenting of
1945+
// the in-scene placed Networkobject changes several times over different sessions.
1946+
if (sceneObject.IsSceneObject && !sceneObject.HasParent && networkObject.m_LatestParent.HasValue)
1947+
{
1948+
networkObject.m_LatestParent = null;
1949+
}
1950+
19521951
// Spawn the NetworkObject
19531952
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
19541953

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,14 @@ internal void AddSpawnedNetworkObjects()
322322
m_NetworkObjectsSync.Add(sobj);
323323
}
324324
}
325+
SortObjectsToSync();
326+
}
325327

328+
/// <summary>
329+
/// Used to order the object serialization for both synchronization and scene loading
330+
/// </summary>
331+
private void SortObjectsToSync()
332+
{
326333
// Sort by INetworkPrefabInstanceHandler implementation before the
327334
// NetworkObjects spawned by the implementation
328335
m_NetworkObjectsSync.Sort(SortNetworkObjects);
@@ -568,20 +575,31 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
568575
// Write our count place holder (must not be packed!)
569576
writer.WriteValueSafe((ushort)0);
570577

578+
// Clear our objects to sync and build a list of the in-scene placed NetworkObjects instantiated and spawned locally
579+
m_NetworkObjectsSync.Clear();
571580
foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects)
572581
{
573582
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
574583
{
575584
if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId))
576585
{
577-
// Serialize the NetworkObject
578-
var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId);
579-
sceneObject.Serialize(writer);
580-
numberOfObjects++;
586+
m_NetworkObjectsSync.Add(keyValuePairBySceneHandle.Value);
581587
}
582588
}
583589
}
584590

591+
// Sort the objects to sync based on parenting hierarchy
592+
SortObjectsToSync();
593+
594+
// Serialize the sorted objects to sync.
595+
foreach (var objectToSycn in m_NetworkObjectsSync)
596+
{
597+
// Serialize the NetworkObject
598+
var sceneObject = objectToSycn.GetMessageSceneObject(TargetClientId);
599+
sceneObject.Serialize(writer);
600+
numberOfObjects++;
601+
}
602+
585603
// Write the number of despawned in-scene placed NetworkObjects
586604
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count);
587605
// Write the scene handle and GlobalObjectIdHash value

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
573573
var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default;
574574
var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default;
575575
var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays;
576-
var isSpawnedByPrefabHandler = false;
577576

578577
// If scene management is disabled or the NetworkObject was dynamically spawned
579578
if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject)
@@ -605,33 +604,40 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
605604
networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
606605
networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
607606

608-
609607
var nonNetworkObjectParent = false;
610608
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
611609
// This is a special case scenario where a late joining client has joined and loaded one or
612610
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
613-
// synchronization information does not indicate the NetworkObject in question has a parent.
614-
// Under this scenario, we want to remove the parent before spawning and setting the transform values.
611+
// synchronization information does not indicate the NetworkObject in question has a parent = or =
612+
// the parent has changed.
613+
// For this we will want to remove the parent before spawning and setting the transform values based
614+
// on several possible scenarios.
615615
if (sceneObject.IsSceneObject && networkObject.transform.parent != null)
616616
{
617617
var parentNetworkObject = networkObject.transform.parent.GetComponent<NetworkObject>();
618-
// if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not
619-
// include parenting, then we need to force the removal of that parent
620-
if (!sceneObject.HasParent && parentNetworkObject)
621-
{
622-
// remove the parent
623-
networkObject.ApplyNetworkParenting(true, true);
624-
}
625-
else if (sceneObject.HasParent && !parentNetworkObject)
618+
// special case to handle being parented under a GameObject with no NetworkObject
619+
nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent;
620+
621+
// If the in-scene placed NetworkObject has a parent NetworkObject...
622+
if (parentNetworkObject)
626623
{
627-
nonNetworkObjectParent = true;
624+
// Then remove the parent only if:
625+
// - The authority says we don't have a parent (but locally we do).
626+
// - The auhtority says we have a parent but either of the two are true:
627+
// -- It isn't the same parent.
628+
// -- It was parented using world position stays.
629+
if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet
630+
&& (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays)))
631+
{
632+
// If parenting without notifications then we are temporarily removing the parent to set the transform
633+
// values before reparenting under the current parent.
634+
networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent);
635+
}
628636
}
629637
}
630638

631-
// Set the transform unless we were spawned by a prefab handler
632-
// Note: prefab handlers are provided the position and rotation
633-
// but it is up to the user to set those values
634-
if (sceneObject.HasTransform && !isSpawnedByPrefabHandler)
639+
// Set the transform only if the sceneObject includes transform information.
640+
if (sceneObject.HasTransform)
635641
{
636642
// If world position stays is true or we have auto object parent synchronization disabled
637643
// then we want to apply the position and rotation values world space relative
@@ -674,7 +680,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
674680
networkObject.SetNetworkParenting(parentId, worldPositionStays);
675681
}
676682

677-
678683
// Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL
679684
// until the scene is loaded. They are then migrated back into the newly loaded and currently active scene.
680685
if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad)

0 commit comments

Comments
 (0)