Skip to content

Commit 23eab5d

Browse files
fix: In-Scene Placed Object Parenting, Serialization Order, and Transform Parent Detection (#3387)
This resolves issues with in-scene placed NetworkObject parenting where the original parent has changed. This resolves the issue where loading a scene would not order the serialization of loaded in-scene placed NetworkObjects based on their parent-child hierarchy. This also resolves an issue where a `NetworkTransform` would think it was parented if nested under anything as well as removes the spamming error message when parenting a `NetworkObject` with `NetworkTransform` and using local space for synchronization (can be handled in documentation as to why this is a bad idea but there may be edge cases where a user might want to do this anyway). [MTT-11883](https://jira.unity3d.com/browse/MTT-11883) <!-- Add RFC link here if applicable. --> ## 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. - Changed: Removing the error message when a `NetworkObject` with `NetworkTransform` is parented and placed in local space when using rigidbody for motion. ## Testing and Documentation - Includes no additional tests (_requires manual testing due to scene loading constraints_). - No documentation changes or additions were necessary. <!-- Uncomment and mark items off with a * if this PR deprecates any API: ### Deprecated API - [ ] An `[Obsolete]` attribute was added along with a `(RemovedAfter yyyy-mm-dd)` entry. - [ ] An [api updater] was added. - [ ] Deprecation of the API is explained in the CHANGELOG. - [ ] The users can understand why this API was removed and what they should use instead. -->
1 parent 23412a0 commit 23eab5d

File tree

5 files changed

+107
-68
lines changed

5 files changed

+107
-68
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2222

2323
### Fixed
2424

25+
- 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. (#3387)
2526
- Fixed an issue in `UnityTransport` where the transport would accept sends on invalid connections, leading to a useless memory allocation and confusing error message. (#3382)
2627
- Fixed issue where the time delta that interpolators used would not be properly updated during multiple fixed update invocations within the same player loop frame. (#3355)
2728
- Fixed issue when using a distributed authority network topology and many clients attempt to connect simultaneously the session owner could max-out the maximum in-flight reliable messages allowed, start dropping packets, and some of the connecting clients would fail to fully synchronize. (#3350)
@@ -44,6 +45,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
4445

4546
### Changed
4647

48+
- Changed the scene loading event serialization order for in-scene placed `NetworkObject`s to be based on their parent-child hierarchy. (#3387)
4749
- Changed the original `Lerp` interpolation type to `LegacyLerp`. (#3355)
4850
- Changed `BufferedLinearInterpolator<T>.Update(float deltaTime, NetworkTime serverTime)` as being deprecated since this method is only used for internal testing purposes. (#3337)
4951
- Changed error thrown when attempting to build a dedicated server with Unity Transport that uses websockets to provide more useful information to the user. (#3336)

com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,13 +2074,6 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra
20742074
// buffered values to the correct world or local space values.
20752075
forceState = SwitchTransformSpaceWhenParented;
20762076
}
2077-
#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
2078-
else if (InLocalSpace && m_UseRigidbodyForMotion)
2079-
{
2080-
// TODO: Provide more options than just FixedJoint
2081-
Debug.LogError($"[Rigidbody] WHen using a Rigidbody for motion, you cannot use {nameof(InLocalSpace)}! If parenting, use the integrated FixedJoint or use a Joint on Authority side.");
2082-
}
2083-
#endif
20842077

20852078
// Check for parenting when synchronizing and/or teleporting
20862079
if (isSynchronization || networkState.IsTeleportingNextFrame)
@@ -3543,7 +3536,10 @@ private void InternalInitialization(bool isOwnershipChange = false)
35433536
{
35443537
if (CanCommitToTransform)
35453538
{
3546-
InLocalSpace = transform.parent != null;
3539+
if (NetworkObject.HasParentNetworkObject(transform))
3540+
{
3541+
InLocalSpace = true;
3542+
}
35473543
}
35483544
// Always apply this if SwitchTransformSpaceWhenParented is set.
35493545
TickSyncChildren = true;

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

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -308,15 +308,18 @@ internal void OnValidate()
308308
/// </remarks>
309309
private void CheckForInScenePlaced()
310310
{
311-
if (PrefabUtility.IsPartOfAnyPrefab(this) && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
311+
if (gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
312312
{
313-
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
314-
var assetPath = AssetDatabase.GetAssetPath(prefab);
315-
var sourceAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(assetPath);
316-
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
313+
if (PrefabUtility.IsPartOfAnyPrefab(this))
317314
{
318-
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
319-
EditorUtility.SetDirty(this);
315+
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
316+
var assetPath = AssetDatabase.GetAssetPath(prefab);
317+
var sourceAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(assetPath);
318+
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
319+
{
320+
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
321+
EditorUtility.SetDirty(this);
322+
}
320323
}
321324
IsSceneObject = true;
322325

@@ -335,6 +338,24 @@ private void CheckForInScenePlaced()
335338
}
336339
#endif // UNITY_EDITOR
337340

341+
internal bool HasParentNetworkObject(Transform transform)
342+
{
343+
if (transform.parent != null)
344+
{
345+
var networkObject = transform.parent.GetComponent<NetworkObject>();
346+
if (networkObject != null && networkObject != this)
347+
{
348+
return true;
349+
}
350+
351+
if (transform.parent.parent != null)
352+
{
353+
return HasParentNetworkObject(transform.parent);
354+
}
355+
}
356+
return false;
357+
}
358+
338359
/// <summary>
339360
/// Gets the NetworkManager that owns this NetworkObject instance
340361
/// </summary>
@@ -2295,7 +2316,7 @@ private void OnTransformParentChanged()
22952316
// we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one.
22962317
internal static HashSet<NetworkObject> OrphanChildren = new HashSet<NetworkObject>();
22972318

2298-
internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false)
2319+
internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false, bool enableNotification = true)
22992320
{
23002321
if (!AutoObjectParentSync)
23012322
{
@@ -2368,7 +2389,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa
23682389
// to WorldPositionStays which can cause scaling issues if the parent's
23692390
// scale is not the default (Vetctor3.one) value.
23702391
transform.SetParent(null, m_CachedWorldPositionStays);
2371-
InvokeBehaviourOnNetworkObjectParentChanged(null);
2392+
if (enableNotification)
2393+
{
2394+
InvokeBehaviourOnNetworkObjectParentChanged(null);
2395+
}
23722396
return true;
23732397
}
23742398

@@ -2393,7 +2417,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa
23932417
}
23942418
SetCachedParent(parentObject.transform);
23952419
transform.SetParent(parentObject.transform, m_CachedWorldPositionStays);
2396-
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
2420+
if (enableNotification)
2421+
{
2422+
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
2423+
}
23972424
return true;
23982425
}
23992426

@@ -3030,6 +3057,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager
30303057
{
30313058
var obj = new SceneObject
30323059
{
3060+
HasParent = transform.parent != null,
3061+
WorldPositionStays = m_CachedWorldPositionStays,
30333062
NetworkObjectId = NetworkObjectId,
30343063
OwnerClientId = OwnerClientId,
30353064
IsPlayerObject = IsPlayerObject,
@@ -3046,31 +3075,16 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager
30463075
TargetClientId = targetClientId
30473076
};
30483077

3049-
NetworkObject parentNetworkObject = null;
3050-
3051-
if (!AlwaysReplicateAsRoot && transform.parent != null)
3078+
// Handle Parenting
3079+
if (!AlwaysReplicateAsRoot && obj.HasParent)
30523080
{
3053-
parentNetworkObject = transform.parent.GetComponent<NetworkObject>();
3054-
// In-scene placed NetworkObjects parented under GameObjects with no NetworkObject
3055-
// should set the has parent flag and preserve the world position stays value
3056-
if (parentNetworkObject == null && obj.IsSceneObject)
3057-
{
3058-
obj.HasParent = true;
3059-
obj.WorldPositionStays = m_CachedWorldPositionStays;
3060-
}
3061-
}
3081+
var parentNetworkObject = transform.parent.GetComponent<NetworkObject>();
30623082

3063-
if (parentNetworkObject != null)
3064-
{
3065-
obj.HasParent = true;
3066-
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
3067-
obj.WorldPositionStays = m_CachedWorldPositionStays;
3068-
var latestParent = GetNetworkParenting();
3069-
var isLatestParentSet = latestParent != null && latestParent.HasValue;
3070-
obj.IsLatestParentSet = isLatestParentSet;
3071-
if (isLatestParentSet)
3083+
if (parentNetworkObject)
30723084
{
3073-
obj.LatestParent = latestParent.Value;
3085+
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
3086+
obj.LatestParent = GetNetworkParenting();
3087+
obj.IsLatestParentSet = obj.LatestParent != null && obj.LatestParent.HasValue;
30743088
}
30753089
}
30763090

@@ -3083,12 +3097,6 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager
30833097
var syncRotationPositionLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays;
30843098
var syncScaleLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays;
30853099

3086-
// Always synchronize in-scene placed object's scale using local space
3087-
if (obj.IsSceneObject)
3088-
{
3089-
syncScaleLocalSpaceRelative = obj.HasParent;
3090-
}
3091-
30923100
// If auto object synchronization is turned off
30933101
if (!AutoObjectParentSync)
30943102
{
@@ -3166,6 +3174,15 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
31663174
var bufferSerializer = new BufferSerializer<BufferSerializerReader>(new BufferSerializerReader(reader));
31673175
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);
31683176

3177+
// If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are
3178+
// being told we do not have a parent, then we want to clear the latest parent so it is not automatically
3179+
// "re-parented" to the original parent. This can happen if not unloading the scene and the parenting of
3180+
// the in-scene placed Networkobject changes several times over different sessions.
3181+
if (sceneObject.IsSceneObject && !sceneObject.HasParent && networkObject.m_LatestParent.HasValue)
3182+
{
3183+
networkObject.m_LatestParent = null;
3184+
}
3185+
31693186
// Spawn the NetworkObject
31703187
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);
31713188

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,14 @@ internal void AddSpawnedNetworkObjects()
329329
m_NetworkObjectsSync.Add(sobj);
330330
}
331331
}
332+
SortObjectsToSync();
333+
}
332334

335+
/// <summary>
336+
/// Used to order the object serialization for both synchronization and scene loading
337+
/// </summary>
338+
private void SortObjectsToSync()
339+
{
333340
// Sort by INetworkPrefabInstanceHandler implementation before the
334341
// NetworkObjects spawned by the implementation
335342
m_NetworkObjectsSync.Sort(SortNetworkObjects);
@@ -671,20 +678,31 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
671678
// If distributed authority mode and sending to the service, then ignore observers
672679
var distributedAuthoritySendingToService = distributedAuthority && TargetClientId == NetworkManager.ServerClientId;
673680

681+
// Clear our objects to sync and build a list of the in-scene placed NetworkObjects instantiated and spawned locally
682+
m_NetworkObjectsSync.Clear();
674683
foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects)
675684
{
676685
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
677686
{
678687
if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId) || distributedAuthoritySendingToService)
679688
{
680-
// Serialize the NetworkObject
681-
var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId, distributedAuthority);
682-
sceneObject.Serialize(writer);
683-
numberOfObjects++;
689+
m_NetworkObjectsSync.Add(keyValuePairBySceneHandle.Value);
684690
}
685691
}
686692
}
687693

694+
// Sort the objects to sync based on parenting hierarchy
695+
SortObjectsToSync();
696+
697+
// Serialize the sorted objects to sync.
698+
foreach (var objectToSycn in m_NetworkObjectsSync)
699+
{
700+
// Serialize the NetworkObject
701+
var sceneObject = objectToSycn.GetMessageSceneObject(TargetClientId, distributedAuthority);
702+
sceneObject.Serialize(writer);
703+
numberOfObjects++;
704+
}
705+
688706
// Write the number of despawned in-scene placed NetworkObjects
689707
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count);
690708
// Write the scene handle and GlobalObjectIdHash value

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
884884
var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default;
885885
var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default;
886886
var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays;
887-
var isSpawnedByPrefabHandler = false;
888887

889888
// If scene management is disabled or the NetworkObject was dynamically spawned
890889
if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject)
@@ -917,33 +916,41 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
917916
networkObject.DontDestroyWithOwner = sceneObject.DontDestroyWithOwner;
918917
networkObject.Ownership = (NetworkObject.OwnershipStatus)sceneObject.OwnershipFlags;
919918

920-
921919
var nonNetworkObjectParent = false;
922920
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
923921
// This is a special case scenario where a late joining client has joined and loaded one or
924922
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
925-
// synchronization information does not indicate the NetworkObject in question has a parent.
926-
// Under this scenario, we want to remove the parent before spawning and setting the transform values.
923+
// synchronization information does not indicate the NetworkObject in question has a parent =or=
924+
// the parent has changed.
925+
// For this we will want to remove the parent before spawning and setting the transform values based
926+
// on several possible scenarios.
927927
if (sceneObject.IsSceneObject && networkObject.transform.parent != null)
928928
{
929929
var parentNetworkObject = networkObject.transform.parent.GetComponent<NetworkObject>();
930-
// if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not
931-
// include parenting, then we need to force the removal of that parent
932-
if (!sceneObject.HasParent && parentNetworkObject)
933-
{
934-
// remove the parent
935-
networkObject.ApplyNetworkParenting(true, true);
936-
}
937-
else if (sceneObject.HasParent && !parentNetworkObject)
930+
931+
// special case to handle being parented under a GameObject with no NetworkObject
932+
nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent;
933+
934+
// If the in-scene placed NetworkObject has a parent NetworkObject...
935+
if (parentNetworkObject)
938936
{
939-
nonNetworkObjectParent = true;
937+
// Then remove the parent only if:
938+
// - The authority says we don't have a parent (but locally we do).
939+
// - The auhtority says we have a parent but either of the two are true:
940+
// -- It isn't the same parent.
941+
// -- It was parented using world position stays.
942+
if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet
943+
&& (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays)))
944+
{
945+
// If parenting without notifications then we are temporarily removing the parent to set the transform
946+
// values before reparenting under the current parent.
947+
networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent);
948+
}
940949
}
941950
}
942951

943-
// Set the transform unless we were spawned by a prefab handler
944-
// Note: prefab handlers are provided the position and rotation
945-
// but it is up to the user to set those values
946-
if (sceneObject.HasTransform && !isSpawnedByPrefabHandler)
952+
// Set the transform only if the sceneObject includes transform information.
953+
if (sceneObject.HasTransform)
947954
{
948955
// If world position stays is true or we have auto object parent synchronization disabled
949956
// then we want to apply the position and rotation values world space relative
@@ -986,7 +993,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
986993
networkObject.SetNetworkParenting(parentId, worldPositionStays);
987994
}
988995

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

0 commit comments

Comments
 (0)