diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 82a42250a0..3708d1b0c2 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -16,6 +16,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- 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) - 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) - Fixed issue where `NetworkAnimator` would log an error if there was no destination transition information. (#3384) - Fixed initial `NetworkTransform` spawn, ensure it uses world space. (#3361) @@ -23,6 +24,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Changed +- Changed the scene loading event serialization order for in-scene placed `NetworkObject`s to be based on their parent-child hierarchy. (#3388) ## [1.12.2] - 2025-01-17 diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 3c5551cd4b..ae7d846e90 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -185,15 +185,18 @@ private bool IsEditingPrefab() /// private void CheckForInScenePlaced() { - if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0) + if (gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0) { - var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); - var assetPath = AssetDatabase.GetAssetPath(prefab); - var sourceAsset = AssetDatabase.LoadAssetAtPath(assetPath); - if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash) + if (PrefabUtility.IsPartOfAnyPrefab(this)) { - InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash; - EditorUtility.SetDirty(this); + var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); + var assetPath = AssetDatabase.GetAssetPath(prefab); + var sourceAsset = AssetDatabase.LoadAssetAtPath(assetPath); + if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash) + { + InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash; + EditorUtility.SetDirty(this); + } } IsSceneObject = true; } @@ -1241,7 +1244,7 @@ private void OnTransformParentChanged() // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. internal static HashSet OrphanChildren = new HashSet(); - internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false) + internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false, bool enableNotification = true) { if (!AutoObjectParentSync) { @@ -1314,7 +1317,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa // to WorldPositionStays which can cause scaling issues if the parent's // scale is not the default (Vetctor3.one) value. transform.SetParent(null, m_CachedWorldPositionStays); - InvokeBehaviourOnNetworkObjectParentChanged(null); + if (enableNotification) + { + InvokeBehaviourOnNetworkObjectParentChanged(null); + } return true; } @@ -1340,7 +1346,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa m_CachedParent = parentObject.transform; transform.SetParent(parentObject.transform, m_CachedWorldPositionStays); - InvokeBehaviourOnNetworkObjectParentChanged(parentObject); + if (enableNotification) + { + InvokeBehaviourOnNetworkObjectParentChanged(parentObject); + } return true; } @@ -1819,6 +1828,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) { var obj = new SceneObject { + HasParent = transform.parent != null, + WorldPositionStays = m_CachedWorldPositionStays, NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, IsPlayerObject = IsPlayerObject, @@ -1829,31 +1840,16 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) TargetClientId = targetClientId }; - NetworkObject parentNetworkObject = null; - - if (!AlwaysReplicateAsRoot && transform.parent != null) + // Handle Parenting + if (!AlwaysReplicateAsRoot && obj.HasParent) { - parentNetworkObject = transform.parent.GetComponent(); - // In-scene placed NetworkObjects parented under GameObjects with no NetworkObject - // should set the has parent flag and preserve the world position stays value - if (parentNetworkObject == null && obj.IsSceneObject) - { - obj.HasParent = true; - obj.WorldPositionStays = m_CachedWorldPositionStays; - } - } + var parentNetworkObject = transform.parent.GetComponent(); - if (parentNetworkObject != null) - { - obj.HasParent = true; - obj.ParentObjectId = parentNetworkObject.NetworkObjectId; - obj.WorldPositionStays = m_CachedWorldPositionStays; - var latestParent = GetNetworkParenting(); - var isLatestParentSet = latestParent != null && latestParent.HasValue; - obj.IsLatestParentSet = isLatestParentSet; - if (isLatestParentSet) + if (parentNetworkObject) { - obj.LatestParent = latestParent.Value; + obj.ParentObjectId = parentNetworkObject.NetworkObjectId; + obj.LatestParent = GetNetworkParenting(); + obj.IsLatestParentSet = obj.LatestParent != null && obj.LatestParent.HasValue; } } @@ -1866,12 +1862,6 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) var syncRotationPositionLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays; var syncScaleLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays; - // Always synchronize in-scene placed object's scale using local space - if (obj.IsSceneObject) - { - syncScaleLocalSpaceRelative = obj.HasParent; - } - // If auto object synchronization is turned off if (!AutoObjectParentSync) { @@ -1949,6 +1939,15 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + // If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are + // being told we do not have a parent, then we want to clear the latest parent so it is not automatically + // "re-parented" to the original parent. This can happen if not unloading the scene and the parenting of + // the in-scene placed Networkobject changes several times over different sessions. + if (sceneObject.IsSceneObject && !sceneObject.HasParent && networkObject.m_LatestParent.HasValue) + { + networkObject.m_LatestParent = null; + } + // Spawn the NetworkObject networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene); diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 77da3012d3..b1f2864a52 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -322,7 +322,14 @@ internal void AddSpawnedNetworkObjects() m_NetworkObjectsSync.Add(sobj); } } + SortObjectsToSync(); + } + /// + /// Used to order the object serialization for both synchronization and scene loading + /// + private void SortObjectsToSync() + { // Sort by INetworkPrefabInstanceHandler implementation before the // NetworkObjects spawned by the implementation m_NetworkObjectsSync.Sort(SortNetworkObjects); @@ -568,20 +575,31 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) // Write our count place holder (must not be packed!) writer.WriteValueSafe((ushort)0); + // Clear our objects to sync and build a list of the in-scene placed NetworkObjects instantiated and spawned locally + m_NetworkObjectsSync.Clear(); foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects) { foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value) { if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId)) { - // Serialize the NetworkObject - var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId); - sceneObject.Serialize(writer); - numberOfObjects++; + m_NetworkObjectsSync.Add(keyValuePairBySceneHandle.Value); } } } + // Sort the objects to sync based on parenting hierarchy + SortObjectsToSync(); + + // Serialize the sorted objects to sync. + foreach (var objectToSycn in m_NetworkObjectsSync) + { + // Serialize the NetworkObject + var sceneObject = objectToSycn.GetMessageSceneObject(TargetClientId); + sceneObject.Serialize(writer); + numberOfObjects++; + } + // Write the number of despawned in-scene placed NetworkObjects writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count); // Write the scene handle and GlobalObjectIdHash value diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index b1c31976f8..c6b6444567 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -573,7 +573,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default; var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default; var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays; - var isSpawnedByPrefabHandler = false; // If scene management is disabled or the NetworkObject was dynamically spawned if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) @@ -605,33 +604,40 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO networkObject.DestroyWithScene = sceneObject.DestroyWithScene; networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; - var nonNetworkObjectParent = false; // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) // This is a special case scenario where a late joining client has joined and loaded one or // more scenes that contain nested in-scene placed NetworkObject children yet the server's - // synchronization information does not indicate the NetworkObject in question has a parent. - // Under this scenario, we want to remove the parent before spawning and setting the transform values. + // synchronization information does not indicate the NetworkObject in question has a parent = or = + // the parent has changed. + // For this we will want to remove the parent before spawning and setting the transform values based + // on several possible scenarios. if (sceneObject.IsSceneObject && networkObject.transform.parent != null) { var parentNetworkObject = networkObject.transform.parent.GetComponent(); - // if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not - // include parenting, then we need to force the removal of that parent - if (!sceneObject.HasParent && parentNetworkObject) - { - // remove the parent - networkObject.ApplyNetworkParenting(true, true); - } - else if (sceneObject.HasParent && !parentNetworkObject) + // special case to handle being parented under a GameObject with no NetworkObject + nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent; + + // If the in-scene placed NetworkObject has a parent NetworkObject... + if (parentNetworkObject) { - nonNetworkObjectParent = true; + // Then remove the parent only if: + // - The authority says we don't have a parent (but locally we do). + // - The auhtority says we have a parent but either of the two are true: + // -- It isn't the same parent. + // -- It was parented using world position stays. + if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet + && (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays))) + { + // If parenting without notifications then we are temporarily removing the parent to set the transform + // values before reparenting under the current parent. + networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent); + } } } - // Set the transform unless we were spawned by a prefab handler - // Note: prefab handlers are provided the position and rotation - // but it is up to the user to set those values - if (sceneObject.HasTransform && !isSpawnedByPrefabHandler) + // Set the transform only if the sceneObject includes transform information. + if (sceneObject.HasTransform) { // If world position stays is true or we have auto object parent synchronization disabled // then we want to apply the position and rotation values world space relative @@ -674,7 +680,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO networkObject.SetNetworkParenting(parentId, worldPositionStays); } - // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad)