diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 480152dc66..c4edcc652c 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -22,6 +22,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. (#3387) - 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) - 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) - 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 ### Changed +- Changed the scene loading event serialization order for in-scene placed `NetworkObject`s to be based on their parent-child hierarchy. (#3387) - Changed the original `Lerp` interpolation type to `LegacyLerp`. (#3355) - Changed `BufferedLinearInterpolator.Update(float deltaTime, NetworkTime serverTime)` as being deprecated since this method is only used for internal testing purposes. (#3337) - 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) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 92943ee0b5..10e95c6017 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -2074,13 +2074,6 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra // buffered values to the correct world or local space values. forceState = SwitchTransformSpaceWhenParented; } -#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - else if (InLocalSpace && m_UseRigidbodyForMotion) - { - // TODO: Provide more options than just FixedJoint - 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."); - } -#endif // Check for parenting when synchronizing and/or teleporting if (isSynchronization || networkState.IsTeleportingNextFrame) @@ -3543,7 +3536,10 @@ private void InternalInitialization(bool isOwnershipChange = false) { if (CanCommitToTransform) { - InLocalSpace = transform.parent != null; + if (NetworkObject.HasParentNetworkObject(transform)) + { + InLocalSpace = true; + } } // Always apply this if SwitchTransformSpaceWhenParented is set. TickSyncChildren = true; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 51895a15e8..4b6c43feb4 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -308,15 +308,18 @@ internal void OnValidate() /// private void CheckForInScenePlaced() { - if (PrefabUtility.IsPartOfAnyPrefab(this) && 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; @@ -335,6 +338,24 @@ private void CheckForInScenePlaced() } #endif // UNITY_EDITOR + internal bool HasParentNetworkObject(Transform transform) + { + if (transform.parent != null) + { + var networkObject = transform.parent.GetComponent(); + if (networkObject != null && networkObject != this) + { + return true; + } + + if (transform.parent.parent != null) + { + return HasParentNetworkObject(transform.parent); + } + } + return false; + } + /// /// Gets the NetworkManager that owns this NetworkObject instance /// @@ -2295,7 +2316,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) { @@ -2368,7 +2389,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; } @@ -2393,7 +2417,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa } SetCachedParent(parentObject.transform); transform.SetParent(parentObject.transform, m_CachedWorldPositionStays); - InvokeBehaviourOnNetworkObjectParentChanged(parentObject); + if (enableNotification) + { + InvokeBehaviourOnNetworkObjectParentChanged(parentObject); + } return true; } @@ -3030,6 +3057,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager { var obj = new SceneObject { + HasParent = transform.parent != null, + WorldPositionStays = m_CachedWorldPositionStays, NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, IsPlayerObject = IsPlayerObject, @@ -3046,31 +3075,16 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager 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; } } @@ -3083,12 +3097,6 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager 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) { @@ -3166,6 +3174,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 7f03e08493..943f54aff2 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -329,7 +329,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); @@ -671,20 +678,31 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) // If distributed authority mode and sending to the service, then ignore observers var distributedAuthoritySendingToService = distributedAuthority && TargetClientId == NetworkManager.ServerClientId; + // 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) || distributedAuthoritySendingToService) { - // Serialize the NetworkObject - var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId, distributedAuthority); - 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, distributedAuthority); + 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 0e9558f4bc..e87da1f489 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -884,7 +884,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) @@ -917,33 +916,41 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO networkObject.DontDestroyWithOwner = sceneObject.DontDestroyWithOwner; networkObject.Ownership = (NetworkObject.OwnershipStatus)sceneObject.OwnershipFlags; - 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 @@ -986,7 +993,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)