diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index f007ab7831..a8c49431c3 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -13,6 +13,8 @@ Additional documentation and release notes are available at [Multiplayer Documen - IPv6 is now supported for direct connections when using `UnityTransport`. (#2232) - Added WebSocket support when using UTP 2.0 with `UseWebSockets` property in the `UnityTransport` component of the `NetworkManager` allowing to pick WebSockets for communication. When building for WebGL, this selection happens automatically. (#2201) +- Added position, rotation, and scale to the `ParentSyncMessage` which provides users the ability to specify the final values on the server-side when `OnNetworkObjectParentChanged` is invoked just before the message is created (when the `Transform` values are applied to the message). (#2146) +- Added `NetworkObject.TryRemoveParent` method for convenience purposes opposed to having to cast null to either `GameObject` or `NetworkObject`. (#2146) ### Changed @@ -33,6 +35,11 @@ Additional documentation and release notes are available at [Multiplayer Documen - Implicit conversion of NetworkObjectReference to GameObject will now return null instead of throwing an exception if the referenced object could not be found (i.e., was already despawned) (#2158) - Fixed warning resulting from a stray NetworkAnimator.meta file (#2153) - Fixed Connection Approval Timeout not working client side. (#2164) +- Fixed issue where the `WorldPositionStays` parenting parameter was not being synchronized with clients. (#2146) +- Fixed issue where parented in-scene placed `NetworkObject`s would fail for late joining clients. (#2146) +- Fixed issue where scale was not being synchronized which caused issues with nested parenting and scale when `WorldPositionStays` was true. (#2146) +- Fixed issue with `NetworkTransform.ApplyTransformToNetworkStateWithInfo` where it was not honoring axis sync settings when `NetworkTransformState.IsTeleportingNextFrame` was true. (#2146) +- Fixed issue with `NetworkTransform.TryCommitTransformToServer` where it was not honoring the `InLocalSpace` setting. (#2146) - Fixed ClientRpcs always reporting in the profiler view as going to all clients, even when limited to a subset of clients by `ClientRpcParams`. (#2144) - Fixed RPC codegen failing to choose the correct extension methods for `FastBufferReader` and `FastBufferWriter` when the parameters were a generic type (i.e., List) and extensions for multiple instantiations of that type have been defined (i.e., List and List) (#2142) - Fixed the issue where running a server (i.e. not host) the second player would not receive updates (unless a third player joined). (#2127) diff --git a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs index 2a6fe62305..f8d4dbd6a7 100644 --- a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs @@ -451,19 +451,6 @@ internal NetworkTransformState GetLastSentState() return m_LastSentState; } - /// - /// Calculated when spawned, this is used to offset a newly received non-authority side state by 1 tick duration - /// in order to end the extrapolation for that state's values. - /// - /// - /// Example: - /// NetworkState-A is received, processed, and measurements added - /// NetworkState-A is duplicated (NetworkState-A-Post) and its sent time is offset by the tick frequency - /// One tick later, NetworkState-A-Post is applied to end that delta's extrapolation. - /// to see how NetworkState-A-Post doesn't get excluded/missed - /// - private double m_TickFrequency; - /// /// This will try to send/commit the current transform delta states (if any) /// @@ -488,14 +475,16 @@ protected void TryCommitTransformToServer(Transform transformToCommit, double di } else // Non-Authority { + var position = InLocalSpace ? transformToCommit.localPosition : transformToCommit.position; + var rotation = InLocalSpace ? transformToCommit.localRotation : transformToCommit.rotation; // We are an owner requesting to update our state if (!m_CachedIsServer) { - SetStateServerRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); + SetStateServerRpc(position, rotation, transformToCommit.localScale, false); } else // Server is always authoritative (including owner authoritative) { - SetStateClientRpc(transformToCommit.position, transformToCommit.rotation, transformToCommit.localScale, false); + SetStateClientRpc(position, rotation, transformToCommit.localScale, false); } } } @@ -521,37 +510,24 @@ private void TryCommitTransform(Transform transformToCommit, double dirtyTime) } } + /// + /// Initializes the interpolators with the current transform values + /// private void ResetInterpolatedStateToCurrentAuthoritativeState() { var serverTime = NetworkManager.ServerTime.Time; - - // TODO: Look into a better way to communicate the entire state for late joining clients. - // Since the replicated network state will just be the most recent deltas and not the entire state. - //m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime); - //m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime); - //m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime); - - //m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.RotAngleX, m_LocalAuthoritativeNetworkState.RotAngleY, m_LocalAuthoritativeNetworkState.RotAngleZ), serverTime); - - //m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime); - //m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime); - //m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime); - - // NOTE ABOUT THIS CHANGE: - // !!! This will exclude any scale changes because we currently do not spawn network objects with scale !!! - // Regarding Scale: It will be the same scale as the default scale for the object being spawned. var position = InLocalSpace ? transform.localPosition : transform.position; m_PositionXInterpolator.ResetTo(position.x, serverTime); m_PositionYInterpolator.ResetTo(position.y, serverTime); m_PositionZInterpolator.ResetTo(position.z, serverTime); + var rotation = InLocalSpace ? transform.localRotation : transform.rotation; m_RotationInterpolator.ResetTo(rotation, serverTime); - // TODO: (Create Jira Ticket) Synchronize local scale during NetworkObject synchronization - // (We will probably want to byte pack TransformData to offset the 3 float addition) - m_ScaleXInterpolator.ResetTo(transform.localScale.x, serverTime); - m_ScaleYInterpolator.ResetTo(transform.localScale.y, serverTime); - m_ScaleZInterpolator.ResetTo(transform.localScale.z, serverTime); + var scale = transform.localScale; + m_ScaleXInterpolator.ResetTo(scale.x, serverTime); + m_ScaleYInterpolator.ResetTo(scale.y, serverTime); + m_ScaleZInterpolator.ResetTo(scale.z, serverTime); } /// @@ -602,63 +578,63 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw isDirty = true; } - if (SyncPositionX && Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame) + if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { networkState.PositionX = position.x; networkState.HasPositionX = true; isPositionDirty = true; } - if (SyncPositionY && Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame) + if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { networkState.PositionY = position.y; networkState.HasPositionY = true; isPositionDirty = true; } - if (SyncPositionZ && Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame) + if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame)) { networkState.PositionZ = position.z; networkState.HasPositionZ = true; isPositionDirty = true; } - if (SyncRotAngleX && Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame) + if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; isRotationDirty = true; } - if (SyncRotAngleY && Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame) + if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; isRotationDirty = true; } - if (SyncRotAngleZ && Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame) + if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame)) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; isRotationDirty = true; } - if (SyncScaleX && Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame) + if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { networkState.ScaleX = scale.x; networkState.HasScaleX = true; isScaleDirty = true; } - if (SyncScaleY && Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame) + if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { networkState.ScaleY = scale.y; networkState.HasScaleY = true; isScaleDirty = true; } - if (SyncScaleZ && Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame) + if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame)) { networkState.ScaleZ = scale.z; networkState.HasScaleZ = true; @@ -1007,7 +983,6 @@ public override void OnNetworkSpawn() { m_CachedIsServer = IsServer; m_CachedNetworkManager = NetworkManager; - m_TickFrequency = 1.0 / NetworkManager.NetworkConfig.TickRate; Initialize(); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index b7a0903375..7e068934df 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -2070,14 +2070,31 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe if (response.CreatePlayerObject) { - var networkObject = SpawnManager.CreateLocalNetworkObject( - isSceneObject: false, - response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, - ownerClientId, - parentNetworkId: null, - networkSceneHandle: null, - response.Position, - response.Rotation); + var playerPrefabHash = response.PlayerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash; + + // Generate a SceneObject for the player object to spawn + var sceneObject = new NetworkObject.SceneObject + { + Header = new NetworkObject.SceneObject.HeaderData + { + IsPlayerObject = true, + OwnerClientId = ownerClientId, + IsSceneObject = false, + HasTransform = true, + Hash = playerPrefabHash, + }, + TargetClientId = ownerClientId, + Transform = new NetworkObject.SceneObject.TransformData + { + Position = response.Position.GetValueOrDefault(), + Rotation = response.Rotation.GetValueOrDefault() + } + }; + + // Create the player NetworkObject locally + var networkObject = SpawnManager.CreateLocalNetworkObject(sceneObject); + + // Spawn the player NetworkObject locally SpawnManager.SpawnNetworkObjectLocally( networkObject, SpawnManager.GetNetworkObjectId(), diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 390ae58979..6b21c5f89a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -573,21 +573,21 @@ internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNe } } - private bool m_IsReparented; // Did initial parent (came from the scene hierarchy) change at runtime? private ulong? m_LatestParent; // What is our last set parent NetworkObject's ID? private Transform m_CachedParent; // What is our last set parent Transform reference? + private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent internal void SetCachedParent(Transform parentTransform) { m_CachedParent = parentTransform; } - internal (bool IsReparented, ulong? LatestParent) GetNetworkParenting() => (m_IsReparented, m_LatestParent); + internal ulong? GetNetworkParenting() => m_LatestParent; - internal void SetNetworkParenting(bool isReparented, ulong? latestParent) + internal void SetNetworkParenting(ulong? latestParent, bool worldPositionStays) { - m_IsReparented = isReparented; m_LatestParent = latestParent; + m_CachedWorldPositionStays = worldPositionStays; } /// @@ -598,7 +598,10 @@ internal void SetNetworkParenting(bool isReparented, ulong? latestParent) /// Whether or not reparenting was successful. public bool TrySetParent(Transform parent, bool worldPositionStays = true) { - return TrySetParent(parent.GetComponent(), worldPositionStays); + var networkObject = parent.GetComponent(); + + // If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent + return networkObject == null ? false : TrySetParent(networkObject, worldPositionStays); } /// @@ -609,7 +612,29 @@ public bool TrySetParent(Transform parent, bool worldPositionStays = true) /// Whether or not reparenting was successful. public bool TrySetParent(GameObject parent, bool worldPositionStays = true) { - return TrySetParent(parent.GetComponent(), worldPositionStays); + // If we are removing ourself from a parent + if (parent == null) + { + return TrySetParent((NetworkObject)null, worldPositionStays); + } + + var networkObject = parent.GetComponent(); + + // If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent + return networkObject == null ? false : TrySetParent(networkObject, worldPositionStays); + } + + /// + /// Removes the parent of the NetworkObject's transform + /// + /// + /// This is a more convenient way to remove the parent without having to cast the null value to either or + /// + /// If true, the parent-relative position, scale and rotation are modified such that the object keeps the same world space position, rotation and scale as before. + /// + public bool TryRemoveParent(bool worldPositionStays = true) + { + return TrySetParent((NetworkObject)null, worldPositionStays); } /// @@ -640,17 +665,21 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) return false; } - if (parent == null) + if (parent != null && !parent.IsSpawned) { return false; } + m_CachedWorldPositionStays = worldPositionStays; - if (!parent.IsSpawned) + if (parent == null) { - return false; + transform.SetParent(null, worldPositionStays); + } + else + { + transform.SetParent(parent.transform, worldPositionStays); } - transform.SetParent(parent.transform, worldPositionStays); return true; } @@ -686,7 +715,7 @@ private void OnTransformParentChanged() Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned")); return; } - + var removeParent = false; var parentTransform = transform.parent; if (parentTransform != null) { @@ -709,19 +738,31 @@ private void OnTransformParentChanged() else { m_LatestParent = null; + removeParent = m_CachedParent != null; } - m_IsReparented = true; - ApplyNetworkParenting(); + ApplyNetworkParenting(removeParent); var message = new ParentSyncMessage { NetworkObjectId = NetworkObjectId, - IsReparented = m_IsReparented, IsLatestParentSet = m_LatestParent != null && m_LatestParent.HasValue, - LatestParent = m_LatestParent + LatestParent = m_LatestParent, + RemoveParent = removeParent, + WorldPositionStays = m_CachedWorldPositionStays, + Position = m_CachedWorldPositionStays ? transform.position : transform.localPosition, + Rotation = m_CachedWorldPositionStays ? transform.rotation : transform.localRotation, + Scale = transform.localScale, }; + // We need to preserve the m_CachedWorldPositionStays value until after we create the message + // in order to assure any local space values changed/reset get applied properly. If our + // parent is null then go ahead and reset the m_CachedWorldPositionStays the default value. + if (parentTransform == null) + { + m_CachedWorldPositionStays = true; + } + unsafe { var maxCount = NetworkManager.ConnectedClientsIds.Count; @@ -749,42 +790,90 @@ 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() + internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false) { if (!AutoObjectParentSync) { return false; } - if (!IsSpawned) + // SPECIAL CASE: + // The ignoreNotSpawned 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. This is the only scenario where the ignoreNotSpawned parameter is used. + if (!IsSpawned && !ignoreNotSpawned) { return false; } - if (!m_IsReparented) + // Handle the first in-scene placed NetworkObject parenting scenarios. Once the m_LatestParent + // has been set, this will not be entered into again (i.e. the later code will be invoked and + // users will get notifications when the parent changes). + var isInScenePlaced = IsSceneObject.HasValue && IsSceneObject.Value; + if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && isInScenePlaced) { - return true; + var parentNetworkObject = transform.parent.GetComponent(); + + // If parentNetworkObject is null then the parent is a GameObject without a NetworkObject component + // attached. Under this case, we preserve the hierarchy but we don't keep track of the parenting. + // Note: We only start tracking parenting if the user removes the child from the standard GameObject + // parent and then re-parents the child under a GameObject with a NetworkObject component attached. + if (parentNetworkObject == null) + { + return true; + } + else // If the parent still isn't spawned add this to the orphaned children and return false + if (!parentNetworkObject.IsSpawned) + { + OrphanChildren.Add(this); + return false; + } + else + { + // If we made it this far, go ahead and set the network parenting values + // with the default WorldPoisitonSays value + SetNetworkParenting(parentNetworkObject.NetworkObjectId, true); + + // Set the cached parent + m_CachedParent = parentNetworkObject.transform; + + return true; + } } - if (m_LatestParent == null || !m_LatestParent.HasValue) + // If we are removing the parent or our latest parent is not set, then remove the parent + // removeParent is only set when: + // - The server-side NetworkObject.OnTransformParentChanged is invoked and the parent is being removed + // - The client-side when handling a ParentSyncMessage + // When clients are synchronizing only the m_LatestParent.HasValue will not have a value if there is no parent + // or a parent was removed prior to the client connecting (i.e. in-scene placed NetworkObjects) + if (removeParent || !m_LatestParent.HasValue) { m_CachedParent = null; - transform.parent = null; - + // We must use Transform.SetParent when taking WorldPositionStays into + // consideration, otherwise just setting transform.parent = null defaults + // 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); return true; } - if (!NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value)) + // If we have a latest parent id but it hasn't been spawned yet, then add this instance to the orphanChildren + // HashSet and return false (i.e. parenting not applied yet) + if (m_LatestParent.HasValue && !NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value)) { OrphanChildren.Add(this); return false; } + // If we made it here, then parent this instance under the parentObject var parentObject = NetworkManager.SpawnManager.SpawnedObjects[m_LatestParent.Value]; m_CachedParent = parentObject.transform; - transform.parent = parentObject.transform; + transform.SetParent(parentObject.transform, m_CachedWorldPositionStays); InvokeBehaviourOnNetworkObjectParentChanged(parentObject); return true; @@ -969,7 +1058,6 @@ public struct HeaderData : INetworkSerializeByMemcpy public bool HasParent; public bool IsSceneObject; public bool HasTransform; - public bool IsReparented; } public HeaderData Header; @@ -982,6 +1070,7 @@ public struct TransformData : INetworkSerializeByMemcpy { public Vector3 Position; public Quaternion Rotation; + public Vector3 Scale; } public TransformData Transform; @@ -997,12 +1086,19 @@ public struct TransformData : INetworkSerializeByMemcpy public int NetworkSceneHandle; + public bool WorldPositionStays; + public unsafe void Serialize(FastBufferWriter writer) { var writeSize = sizeof(HeaderData); - writeSize += Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0; - writeSize += Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0; - writeSize += Header.IsReparented ? FastBufferWriter.GetWriteSize(IsLatestParentSet) + (IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0) : 0; + if (Header.HasParent) + { + writeSize += FastBufferWriter.GetWriteSize(ParentObjectId); + writeSize += FastBufferWriter.GetWriteSize(WorldPositionStays); + writeSize += FastBufferWriter.GetWriteSize(IsLatestParentSet); + writeSize += IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0; + } + writeSize += Header.HasTransform ? FastBufferWriter.GetWriteSize() : 0; writeSize += Header.IsSceneObject ? FastBufferWriter.GetWriteSize() : 0; if (!writer.TryBeginWrite(writeSize)) @@ -1015,6 +1111,12 @@ public unsafe void Serialize(FastBufferWriter writer) if (Header.HasParent) { writer.WriteValue(ParentObjectId); + writer.WriteValue(WorldPositionStays); + writer.WriteValue(IsLatestParentSet); + if (IsLatestParentSet) + { + writer.WriteValue(LatestParent.Value); + } } if (Header.HasTransform) @@ -1022,15 +1124,6 @@ public unsafe void Serialize(FastBufferWriter writer) writer.WriteValue(Transform); } - if (Header.IsReparented) - { - writer.WriteValue(IsLatestParentSet); - if (IsLatestParentSet) - { - writer.WriteValue((ulong)LatestParent); - } - } - // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // NetworkSceneHandle and GlobalObjectIdHash. Since each loaded scene has a unique // handle, it provides us with a unique and persistent "scene prefab asset" instance. @@ -1051,19 +1144,41 @@ public unsafe void Deserialize(FastBufferReader reader) throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); } reader.ReadValue(out Header); - var readSize = Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0; - readSize += Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0; - readSize += Header.IsReparented ? FastBufferWriter.GetWriteSize(IsLatestParentSet) + (IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0) : 0; + var readSize = 0; + if (Header.HasParent) + { + readSize += FastBufferWriter.GetWriteSize(ParentObjectId); + readSize += FastBufferWriter.GetWriteSize(WorldPositionStays); + readSize += FastBufferWriter.GetWriteSize(IsLatestParentSet); + // We need to read at this point in order to get the IsLatestParentSet value + if (!reader.TryBeginRead(readSize)) + { + throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); + } + + // Read the initial parenting related properties + reader.ReadValue(out ParentObjectId); + reader.ReadValue(out WorldPositionStays); + reader.ReadValue(out IsLatestParentSet); + + // Now calculate the remaining bytes to read + readSize = 0; + readSize += IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0; + } + + readSize += Header.HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += Header.IsSceneObject ? FastBufferWriter.GetWriteSize() : 0; + // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) { throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); } - if (Header.HasParent) + if (IsLatestParentSet) { - reader.ReadValue(out ParentObjectId); + reader.ReadValueSafe(out ulong latestParent); + LatestParent = latestParent; } if (Header.HasTransform) @@ -1071,16 +1186,6 @@ public unsafe void Deserialize(FastBufferReader reader) reader.ReadValue(out Transform); } - if (Header.IsReparented) - { - reader.ReadValue(out IsLatestParentSet); - if (IsLatestParentSet) - { - reader.ReadValueSafe(out ulong latestParent); - LatestParent = latestParent; - } - } - // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // NetworkSceneHandle and GlobalObjectIdHash. Since each loaded scene has a unique // handle, it provides us with a unique and persistent "scene prefab asset" instance. @@ -1124,33 +1229,39 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) parentNetworkObject = transform.parent.GetComponent(); } - if (parentNetworkObject) + if (parentNetworkObject != null) { obj.Header.HasParent = true; obj.ParentObjectId = parentNetworkObject.NetworkObjectId; + obj.WorldPositionStays = m_CachedWorldPositionStays; + var latestParent = GetNetworkParenting(); + var isLatestParentSet = latestParent != null && latestParent.HasValue; + obj.IsLatestParentSet = isLatestParentSet; + if (isLatestParentSet) + { + obj.LatestParent = latestParent.Value; + } } + if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId)) { obj.Header.HasTransform = true; obj.Transform = new SceneObject.TransformData { - Position = transform.position, - Rotation = transform.rotation + // If we are parented and we have the m_CachedWorldPositionStays disabled, then use local space + // values as opposed world space values. + Position = parentNetworkObject && !m_CachedWorldPositionStays ? transform.localPosition : transform.position, + Rotation = parentNetworkObject && !m_CachedWorldPositionStays ? transform.localRotation : transform.rotation, + + // We only use the lossyScale if the NetworkObject has a parent. Multi-generation nested children scales can + // impact the final scale of the child NetworkObject in question. The solution is to use the lossy scale + // which can be thought of as "world space scale". + // More information: + // https://docs.unity3d.com/ScriptReference/Transform-lossyScale.html + Scale = parentNetworkObject && !m_CachedWorldPositionStays ? transform.localScale : transform.lossyScale, }; } - var (isReparented, latestParent) = GetNetworkParenting(); - obj.Header.IsReparented = isReparented; - if (isReparented) - { - var isLatestParentSet = latestParent != null && latestParent.HasValue; - obj.IsLatestParentSet = isLatestParentSet; - if (isLatestParentSet) - { - obj.LatestParent = latestParent.Value; - } - } - return obj; } @@ -1164,33 +1275,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) /// optional to use NetworkObject deserialized internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader variableData, NetworkManager networkManager) { - Vector3? position = null; - Quaternion? rotation = null; - ulong? parentNetworkId = null; - int? networkSceneHandle = null; - - if (sceneObject.Header.HasTransform) - { - position = sceneObject.Transform.Position; - rotation = sceneObject.Transform.Rotation; - } - - if (sceneObject.Header.HasParent) - { - parentNetworkId = sceneObject.ParentObjectId; - } - - if (sceneObject.Header.IsSceneObject) - { - networkSceneHandle = sceneObject.NetworkSceneHandle; - } - //Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject( - sceneObject.Header.IsSceneObject, sceneObject.Header.Hash, - sceneObject.Header.OwnerClientId, parentNetworkId, networkSceneHandle, position, rotation, sceneObject.Header.IsReparented); - - networkObject?.SetNetworkParenting(sceneObject.Header.IsReparented, sceneObject.LatestParent); + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); if (networkObject == null) { @@ -1205,7 +1291,7 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf return null; } - // Spawn the NetworkObject( + // Spawn the NetworkObject networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, variableData, false); return networkObject; diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs index 95d20ea2f7..0023b1a66c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -1,10 +1,12 @@ +using UnityEngine; + namespace Unity.Netcode { internal struct ParentSyncMessage : INetworkMessage { public ulong NetworkObjectId; - public bool IsReparented; + public bool WorldPositionStays; //If(Metadata.IsReparented) public bool IsLatestParentSet; @@ -12,18 +14,36 @@ internal struct ParentSyncMessage : INetworkMessage //If(IsLatestParentSet) public ulong? LatestParent; + // Is set when the parent should be removed (similar to IsReparented functionality but only for removing the parent) + public bool RemoveParent; + + // These additional properties are used to synchronize clients with the current position, + // rotation, and scale after parenting/de-parenting (world/local space relative). This + // allows users to control the final child's transform values without having to have a + // NetworkTransform component on the child. (i.e. picking something up) + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Scale; + public void Serialize(FastBufferWriter writer) { - writer.WriteValueSafe(NetworkObjectId); - writer.WriteValueSafe(IsReparented); - if (IsReparented) + BytePacker.WriteValuePacked(writer, NetworkObjectId); + writer.WriteValueSafe(RemoveParent); + writer.WriteValueSafe(WorldPositionStays); + if (!RemoveParent) { writer.WriteValueSafe(IsLatestParentSet); + if (IsLatestParentSet) { - writer.WriteValueSafe((ulong)LatestParent); + BytePacker.WriteValueBitPacked(writer, (ulong)LatestParent); } } + + // Whether parenting or removing a parent, we always update the position, rotation, and scale + writer.WriteValueSafe(Position); + writer.WriteValueSafe(Rotation); + writer.WriteValueSafe(Scale); } public bool Deserialize(FastBufferReader reader, ref NetworkContext context) @@ -34,24 +54,30 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) return false; } - reader.ReadValueSafe(out NetworkObjectId); - reader.ReadValueSafe(out IsReparented); - if (IsReparented) + ByteUnpacker.ReadValuePacked(reader, out NetworkObjectId); + reader.ReadValueSafe(out RemoveParent); + reader.ReadValueSafe(out WorldPositionStays); + if (!RemoveParent) { reader.ReadValueSafe(out IsLatestParentSet); + if (IsLatestParentSet) { - reader.ReadValueSafe(out ulong latestParent); + ByteUnpacker.ReadValueBitPacked(reader, out ulong latestParent); LatestParent = latestParent; } } + // Whether parenting or removing a parent, we always update the position, rotation, and scale + reader.ReadValueSafe(out Position); + reader.ReadValueSafe(out Rotation); + reader.ReadValueSafe(out Scale); + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) { networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); return false; } - return true; } @@ -59,8 +85,22 @@ public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId]; - networkObject.SetNetworkParenting(IsReparented, LatestParent); - networkObject.ApplyNetworkParenting(); + networkObject.SetNetworkParenting(LatestParent, WorldPositionStays); + networkObject.ApplyNetworkParenting(RemoveParent); + + // We set all of the transform values after parenting as they are + // the values of the server-side post-parenting transform values + if (!WorldPositionStays) + { + networkObject.transform.localPosition = Position; + networkObject.transform.localRotation = Rotation; + } + else + { + networkObject.transform.position = Position; + networkObject.transform.rotation = Rotation; + } + networkObject.transform.localScale = Scale; } } } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 69640d15cf..ae8d5a31a9 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -243,7 +243,26 @@ internal void AddSpawnedNetworkObjects() m_NetworkObjectsSync.Add(sobj); } } + + // Sort by parents before children + m_NetworkObjectsSync.Sort(SortParentedNetworkObjects); + + // Sort by INetworkPrefabInstanceHandler implementation before the + // NetworkObjects spawned by the implementation m_NetworkObjectsSync.Sort(SortNetworkObjects); + + // This is useful to know what NetworkObjects a client is going to be synchronized with + // as well as the order in which they will be deserialized + if (m_NetworkManager.LogLevel == LogLevel.Developer) + { + var messageBuilder = new System.Text.StringBuilder(0xFFFF); + messageBuilder.Append("[Server-Side Client-Synchronization] NetworkObject serialization order:"); + foreach (var networkObject in m_NetworkObjectsSync) + { + messageBuilder.Append($"{networkObject.name}"); + } + NetworkLog.LogInfo(messageBuilder.ToString()); + } } internal void AddDespawnedInSceneNetworkObjects() @@ -323,6 +342,32 @@ private int SortNetworkObjects(NetworkObject first, NetworkObject second) return 0; } + /// + /// Sorts the synchronization order of the NetworkObjects to be serialized + /// by parents before children. + /// + /// + /// This only handles late joining players. Spawning and nesting several children + /// dynamically is still handled by the orphaned child list when deserialized out of + /// hierarchical order (i.e. Spawn parent and child dynamically, parent message is + /// dropped and re-sent but child object is received and processed) + /// + private int SortParentedNetworkObjects(NetworkObject first, NetworkObject second) + { + // If the first has a parent, move the first down + if (first.transform.parent != null) + { + return 1; + } + else // If the second has a parent and the first does not, then move the first up + if (second.transform.parent != null) + { + return -1; + } + return 0; + } + + /// /// Client and Server Side: /// Serializes data based on the SceneEvent type () diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 91c5a52147..175e21a451 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -314,48 +314,33 @@ internal bool HasPrefab(NetworkObject.SceneObject sceneObject) } /// - /// Should only run on the client + /// Creates a local NetowrkObject to be spawned. /// - internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalObjectIdHash, ulong ownerClientId, ulong? parentNetworkId, int? networkSceneHandle, Vector3? position, Quaternion? rotation, bool isReparented = false) + /// + /// For most cases this is client-side only, with the exception of when the server + /// is spawning a player. + /// + internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject) { - NetworkObject parentNetworkObject = null; - - if (parentNetworkId != null && !isReparented) - { - if (SpawnedObjects.TryGetValue(parentNetworkId.Value, out NetworkObject networkObject)) - { - parentNetworkObject = networkObject; - } - else - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning("Cannot find parent. Parent objects always have to be spawned and replicated BEFORE the child"); - } - } - } - - if (!NetworkManager.NetworkConfig.EnableSceneManagement || !isSceneObject) + NetworkObject networkObject = null; + var globalObjectIdHash = sceneObject.Header.Hash; + var position = sceneObject.Header.HasTransform ? sceneObject.Transform.Position : default; + var rotation = sceneObject.Header.HasTransform ? sceneObject.Transform.Rotation : default; + var scale = sceneObject.Header.HasTransform ? sceneObject.Transform.Scale : default; + var parentNetworkId = sceneObject.Header.HasParent ? sceneObject.ParentObjectId : default; + var worldPositionStays = sceneObject.Header.HasParent ? sceneObject.WorldPositionStays : true; + var isSpawnedByPrefabHandler = false; + + // If scene management is disabled or the NetworkObject was dynamically spawned + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.Header.IsSceneObject) { // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { // Let the handler spawn the NetworkObject - var networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerClientId, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity)); - + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, sceneObject.Header.OwnerClientId, position, rotation); networkObject.NetworkManagerOwner = NetworkManager; - - if (parentNetworkObject != null) - { - networkObject.transform.SetParent(parentNetworkObject.transform, true); - } - - if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) - { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); - } - - return networkObject; + isSpawnedByPrefabHandler = true; } else { @@ -383,31 +368,18 @@ internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalO { NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); } - - return null; - } - - // Otherwise, instantiate an instance of the NetworkPrefab linked to the prefabHash - var networkObject = ((position == null && rotation == null) ? UnityEngine.Object.Instantiate(networkPrefabReference) : UnityEngine.Object.Instantiate(networkPrefabReference, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity))).GetComponent(); - - networkObject.NetworkManagerOwner = NetworkManager; - - if (parentNetworkObject != null) - { - networkObject.transform.SetParent(parentNetworkObject.transform, true); } - - if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + else { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + // Create prefab instance + networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent(); + networkObject.NetworkManagerOwner = NetworkManager; } - - return networkObject; } } - else + else // Get the in-scene placed NetworkObject { - var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, networkSceneHandle); + networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle); if (networkObject == null) { @@ -415,17 +387,71 @@ internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalO { NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); } + } + } - return null; + if (networkObject != null) + { + // SPECIAL CASE: + // 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. + if (sceneObject.Header.IsSceneObject && !sceneObject.Header.HasParent && networkObject.transform.parent != null) + { + // 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 (networkObject.transform.parent.GetComponent() != null) + { + // remove the parent + networkObject.ApplyNetworkParenting(true, true); + } } - if (parentNetworkObject != null) + // 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.Header.HasTransform && !isSpawnedByPrefabHandler) { - networkObject.transform.SetParent(parentNetworkObject.transform, true); + if (worldPositionStays) + { + networkObject.transform.position = position; + networkObject.transform.rotation = rotation; + } + else + { + networkObject.transform.localPosition = position; + networkObject.transform.localRotation = rotation; + } + + // SPECIAL CASE: + // Since players are created uniquely we don't apply scale because + // the ConnectionApprovalResponse does not currently provide the + // ability to specify scale. So, we just use the default scale of + // the network prefab used to represent the player. + // Note: not doing this would set the player's scale to zero since + // that is the default value of Vector3. + if (!sceneObject.Header.IsPlayerObject) + { + networkObject.transform.localScale = scale; + } } - return networkObject; + if (sceneObject.Header.HasParent) + { + // Go ahead and set network parenting properties + networkObject.SetNetworkParenting(parentNetworkId, 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.Header.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + } } + return networkObject; } // Ran on both server and client @@ -545,7 +571,6 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong } } - networkObject.SetCachedParent(networkObject.transform.parent); networkObject.ApplyNetworkParenting(); NetworkObject.CheckOrphanChildren(); @@ -748,8 +773,8 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec // Move child NetworkObjects to the root when parent NetworkObject is destroyed foreach (var spawnedNetObj in SpawnedObjectsList) { - var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting(); - if (isReparented && latestParent == networkObject.NetworkObjectId) + var latestParent = spawnedNetObj.GetNetworkParenting(); + if (latestParent.HasValue && latestParent.Value == networkObject.NetworkObjectId) { spawnedNetObj.gameObject.transform.parent = null; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs index 572b04a1f8..2c56188eb7 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs @@ -4,15 +4,87 @@ namespace Unity.Netcode.RuntimeTests { + + [TestFixture(TransformSpace.World)] + [TestFixture(TransformSpace.Local)] public class NetworkTransformStateTests { + public enum SyncAxis + { + SyncPosX, + SyncPosY, + SyncPosZ, + SyncPosXY, + SyncPosXZ, + SyncPosYZ, + SyncPosXYZ, + SyncRotX, + SyncRotY, + SyncRotZ, + SyncRotXY, + SyncRotXZ, + SyncRotYZ, + SyncRotXYZ, + SyncScaleX, + SyncScaleY, + SyncScaleZ, + SyncScaleXY, + SyncScaleXZ, + SyncScaleYZ, + SyncScaleXYZ, + SyncAllX, + SyncAllY, + SyncAllZ, + SyncAllXY, + SyncAllXZ, + SyncAllYZ, + SyncAllXYZ + } + + public enum TransformSpace + { + World, + Local + } + + public enum SynchronizationType + { + Delta, + Teleport + } + + private TransformSpace m_TransformSpace; + + public NetworkTransformStateTests(TransformSpace transformSpace) + { + m_TransformSpace = transformSpace; + } + + private bool WillAnAxisBeSynchronized(ref NetworkTransform networkTransform) + { + return networkTransform.SyncScaleX || networkTransform.SyncScaleY || networkTransform.SyncScaleZ || + networkTransform.SyncRotAngleX || networkTransform.SyncRotAngleY || networkTransform.SyncRotAngleZ || + networkTransform.SyncPositionX || networkTransform.SyncPositionY || networkTransform.SyncPositionZ; + } + [Test] - public void TestSyncAxes( - [Values] bool inLocalSpace, - [Values] bool syncPosX, [Values] bool syncPosY, [Values] bool syncPosZ, - [Values] bool syncRotX, [Values] bool syncRotY, [Values] bool syncRotZ, - [Values] bool syncScaX, [Values] bool syncScaY, [Values] bool syncScaZ) + public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Values] SyncAxis syncAxis) + { + bool inLocalSpace = m_TransformSpace == TransformSpace.Local; + bool isTeleporting = synchronizationType == SynchronizationType.Teleport; + bool syncPosX = syncAxis == SyncAxis.SyncPosX || syncAxis == SyncAxis.SyncPosXY || syncAxis == SyncAxis.SyncPosXZ || syncAxis == SyncAxis.SyncPosXYZ || syncAxis == SyncAxis.SyncAllX || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncPosY = syncAxis == SyncAxis.SyncPosY || syncAxis == SyncAxis.SyncPosXY || syncAxis == SyncAxis.SyncPosYZ || syncAxis == SyncAxis.SyncPosXYZ || syncAxis == SyncAxis.SyncAllY || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncPosZ = syncAxis == SyncAxis.SyncPosZ || syncAxis == SyncAxis.SyncPosXZ || syncAxis == SyncAxis.SyncPosYZ || syncAxis == SyncAxis.SyncPosXYZ || syncAxis == SyncAxis.SyncAllZ || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + + bool syncRotX = syncAxis == SyncAxis.SyncRotX || syncAxis == SyncAxis.SyncRotXY || syncAxis == SyncAxis.SyncRotXZ || syncAxis == SyncAxis.SyncRotXYZ || syncAxis == SyncAxis.SyncRotX || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncRotY = syncAxis == SyncAxis.SyncRotY || syncAxis == SyncAxis.SyncRotXY || syncAxis == SyncAxis.SyncRotYZ || syncAxis == SyncAxis.SyncRotXYZ || syncAxis == SyncAxis.SyncRotY || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncRotZ = syncAxis == SyncAxis.SyncRotZ || syncAxis == SyncAxis.SyncRotXZ || syncAxis == SyncAxis.SyncRotYZ || syncAxis == SyncAxis.SyncRotXYZ || syncAxis == SyncAxis.SyncRotZ || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + + bool syncScaX = syncAxis == SyncAxis.SyncScaleX || syncAxis == SyncAxis.SyncScaleXY || syncAxis == SyncAxis.SyncScaleXZ || syncAxis == SyncAxis.SyncScaleXYZ || syncAxis == SyncAxis.SyncAllX || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncScaY = syncAxis == SyncAxis.SyncScaleY || syncAxis == SyncAxis.SyncScaleXY || syncAxis == SyncAxis.SyncScaleYZ || syncAxis == SyncAxis.SyncScaleXYZ || syncAxis == SyncAxis.SyncAllY || syncAxis == SyncAxis.SyncAllXY || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + bool syncScaZ = syncAxis == SyncAxis.SyncScaleZ || syncAxis == SyncAxis.SyncScaleXZ || syncAxis == SyncAxis.SyncScaleYZ || syncAxis == SyncAxis.SyncScaleXYZ || syncAxis == SyncAxis.SyncAllZ || syncAxis == SyncAxis.SyncAllXZ || syncAxis == SyncAxis.SyncAllYZ || syncAxis == SyncAxis.SyncAllXYZ; + var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestSyncAxes)}"); var networkObject = gameObject.AddComponent(); var networkTransform = gameObject.AddComponent(); @@ -36,27 +108,13 @@ public void TestSyncAxes( networkTransform.SyncScaleZ = syncScaZ; networkTransform.InLocalSpace = inLocalSpace; + // We want a relatively clean networkTransform state before we try to apply the transform to it + // We only preserve InLocalSpace and IsTeleportingNextFrame properties as they are the only things + // needed when applying a transform to a NetworkTransformState var networkTransformState = new NetworkTransform.NetworkTransformState { - PositionX = initialPosition.x, - PositionY = initialPosition.y, - PositionZ = initialPosition.z, - RotAngleX = initialRotAngles.x, - RotAngleY = initialRotAngles.y, - RotAngleZ = initialRotAngles.z, - ScaleX = initialScale.x, - ScaleY = initialScale.y, - ScaleZ = initialScale.z, - HasPositionX = syncPosX, - HasPositionY = syncPosY, - HasPositionZ = syncPosZ, - HasRotAngleX = syncRotX, - HasRotAngleY = syncRotY, - HasRotAngleZ = syncRotZ, - HasScaleX = syncScaX, - HasScaleY = syncScaY, - HasScaleZ = syncScaZ, - InLocalSpace = inLocalSpace + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, }; // Step 1: change properties, expect state to be dirty @@ -71,95 +129,382 @@ public void TestSyncAxes( } } - // Step 2: disable a particular sync flag, expect state to be not dirty + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + var position = networkTransform.transform.position; + var rotAngles = networkTransform.transform.eulerAngles; + var scale = networkTransform.transform.localScale; + + // Step 2: Verify the state changes in a tick are additive + // TODO: This will need to change if we update NetworkTransform to send all of the + // axis deltas that happened over a tick as a collection instead of collapsing them + // as the changes are detected. + { + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + + // SyncPositionX + if (syncPosX) + { + position.x++; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX); + } + + // SyncPositionY + if (syncPosY) + { + position = networkTransform.transform.position; + position.y++; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY); + } + + // SyncPositionZ + if (syncPosZ) + { + position = networkTransform.transform.position; + position.z++; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ); + } + + // SyncRotAngleX + if (syncRotX) + { + rotAngles = networkTransform.transform.eulerAngles; + rotAngles.x++; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX); + } + + // SyncRotAngleY + if (syncRotY) + { + rotAngles = networkTransform.transform.eulerAngles; + rotAngles.y++; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY); + } + // SyncRotAngleZ + if (syncRotZ) + { + rotAngles = networkTransform.transform.eulerAngles; + rotAngles.z++; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ); + } + + // SyncScaleX + if (syncScaX) + { + scale = networkTransform.transform.localScale; + scale.x++; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ || !syncRotZ); + Assert.IsTrue(networkTransformState.HasScaleX); + } + // SyncScaleY + if (syncScaY) + { + scale = networkTransform.transform.localScale; + scale.y++; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ || !syncRotZ); + Assert.IsTrue(networkTransformState.HasScaleX || !syncScaX); + Assert.IsTrue(networkTransformState.HasScaleY); + } + // SyncScaleZ + if (syncScaZ) + { + scale = networkTransform.transform.localScale; + scale.z++; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsTrue(networkTransformState.HasPositionX || !syncPosX); + Assert.IsTrue(networkTransformState.HasPositionY || !syncPosY); + Assert.IsTrue(networkTransformState.HasPositionZ || !syncPosZ); + Assert.IsTrue(networkTransformState.HasRotAngleX || !syncRotX); + Assert.IsTrue(networkTransformState.HasRotAngleY || !syncRotY); + Assert.IsTrue(networkTransformState.HasRotAngleZ || !syncRotZ); + Assert.IsTrue(networkTransformState.HasScaleX || !syncScaX); + Assert.IsTrue(networkTransformState.HasScaleY || !syncScaY); + Assert.IsTrue(networkTransformState.HasScaleZ); + } + } + + // Step 3: disable a particular sync flag, expect state to be not dirty + // We do this last because it changes which axis will be synchronized. { - var position = networkTransform.transform.position; - var rotAngles = networkTransform.transform.eulerAngles; - var scale = networkTransform.transform.localScale; + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + + position = networkTransform.transform.position; + rotAngles = networkTransform.transform.eulerAngles; + scale = networkTransform.transform.localScale; // SyncPositionX + if (syncPosX) { networkTransform.SyncPositionX = false; position.x++; networkTransform.transform.position = position; - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + // If we are synchronizing more than 1 axis (teleporting impacts this too) + if (syncAxis != SyncAxis.SyncPosX && WillAnAxisBeSynchronized(ref networkTransform)) + { + // For the x axis position value We should expect the state to still be considered dirty (more than one axis is being synchronized and we are teleporting) + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + // However, we expect it to not have applied the position x delta + Assert.IsFalse(networkTransformState.HasPositionX); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } + // SyncPositionY + if (syncPosY) { networkTransform.SyncPositionY = false; position.y++; networkTransform.transform.position = position; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncPosY && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasPositionY); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } + // SyncPositionZ + if (syncPosZ) { networkTransform.SyncPositionZ = false; position.z++; networkTransform.transform.position = position; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncPosZ && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasPositionZ); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncRotAngleX + if (syncRotX) { networkTransform.SyncRotAngleX = false; rotAngles.x++; networkTransform.transform.eulerAngles = rotAngles; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncRotX && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasRotAngleX); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncRotAngleY + if (syncRotY) { networkTransform.SyncRotAngleY = false; rotAngles.y++; networkTransform.transform.eulerAngles = rotAngles; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncRotY && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasRotAngleY); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncRotAngleZ + if (syncRotZ) { networkTransform.SyncRotAngleZ = false; rotAngles.z++; networkTransform.transform.eulerAngles = rotAngles; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncRotZ && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasRotAngleZ); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncScaleX + if (syncScaX) { networkTransform.SyncScaleX = false; scale.x++; networkTransform.transform.localScale = scale; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncScaleX && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasScaleX); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncScaleY + if (syncScaY) { networkTransform.SyncScaleY = false; scale.y++; networkTransform.transform.localScale = scale; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncScaleY && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasScaleY); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } // SyncScaleZ + if (syncScaZ) { networkTransform.SyncScaleZ = false; scale.z++; networkTransform.transform.localScale = scale; - - Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + if (syncAxis != SyncAxis.SyncScaleZ && WillAnAxisBeSynchronized(ref networkTransform)) + { + // We want to start with a fresh NetworkTransformState since it could have other state + // information from the last time we applied the transform + networkTransformState = new NetworkTransform.NetworkTransformState + { + InLocalSpace = inLocalSpace, + IsTeleportingNextFrame = isTeleporting, + }; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + Assert.IsFalse(networkTransformState.HasScaleZ); + } + else + { + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } } + } Object.DestroyImmediate(gameObject); @@ -168,11 +513,11 @@ public void TestSyncAxes( [Test] public void TestThresholds( - [Values] bool inLocalSpace, [Values(NetworkTransform.PositionThresholdDefault, 1.0f)] float positionThreshold, [Values(NetworkTransform.RotAngleThresholdDefault, 1.0f)] float rotAngleThreshold, [Values(NetworkTransform.ScaleThresholdDefault, 0.5f)] float scaleThreshold) { + var inLocalSpace = m_TransformSpace == TransformSpace.Local; var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestThresholds)}"); var networkTransform = gameObject.AddComponent(); networkTransform.enabled = false; // do not tick `FixedUpdate()` or `Update()` diff --git a/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentChildHandler.cs b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentChildHandler.cs new file mode 100644 index 0000000000..80e9ac63e1 --- /dev/null +++ b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentChildHandler.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Unity.Netcode; +using Unity.Netcode.Components; +using TestProject.ManualTests; +using Random = UnityEngine.Random; + +namespace TestProject.RuntimeTests +{ + public class InSceneParentChildHandler : NetworkBehaviour + { + public static InSceneParentChildHandler ServerRootParent; + public static bool EnableVerboseDebug = true; + public static bool AddNetworkTransform; + public static bool WorldPositionStays; + public static InSceneParentChildHandler RootParent { get; private set; } + private static bool s_GenerateRandomValues; + + public bool ParentHasNoNetworkObject; + public bool IsRootParent; + public bool IsLastChild; + public bool PreserveLocalSpace; + + public Vector3 PositionMax = new Vector3(5.0f, 4.0f, 5.0f); + public Vector3 PositionMin = new Vector3(-5.00f, 0.5f, -5.0f); + public Vector3 RotationMax = new Vector3(359.99f, 359.99f, 359.99f); + public Vector3 RotationMin = new Vector3(0.01f, 0.01f, 0.01f); + public float ScaleMax = 3.0f; + public float ScaleMin = 0.75f; + + + private InSceneParentChildHandler m_Parent; + private InSceneParentChildHandler m_Child; + private Vector3 m_TargetLocalPosition; + private Vector3 m_TargetLocalRotation; + private Vector3 m_TargetLocalScale; + + private NetworkTransform m_NetworkTransform; + + public static Dictionary ServerRelativeInstances = new Dictionary(); + public static Dictionary> ClientRelativeInstances = new Dictionary>(); + + public static void ResetInstancesTracking(bool enableVerboseDebug) + { + EnableVerboseDebug = enableVerboseDebug; + ServerRelativeInstances.Clear(); + ClientRelativeInstances.Clear(); + } + + private Vector3 GenerateVector3(Vector3 min, Vector3 max) + { + var result = Vector3.zero; + result.x = Random.Range(min.x, max.y); + result.y = Random.Range(min.x, max.y); + result.z = Random.Range(min.x, max.y); + return result; + } + + private void LogMessage(string message) + { + if (EnableVerboseDebug) + { + Debug.Log(message); + } + } + + private void Start() + { + if (IsRootParent) + { + Random.InitState((int)Random.Range(Time.deltaTime, Time.realtimeSinceStartup)); + RootParent = this; + } + } + + private int CountNestedChildren(Transform currentParent, int count = 0) + { + if (currentParent.childCount > 0) + { + var child = currentParent.GetChild(0); + count++; + CountNestedChildren(child, count); + } + return count; + } + + private Transform GetLastChild(Transform currentParent) + { + if (currentParent.childCount > 0) + { + var child = currentParent.GetChild(0); + var childHandler = child.GetComponent(); + var parentHandler = currentParent.GetComponent(); + parentHandler.m_Child = childHandler; + childHandler.m_Parent = parentHandler; + return GetLastChild(child); + } + return currentParent; + } + + private void RemoveParent(Transform child, bool worldPositionStays = true) + { + var childNetworkObject = child.GetComponent(); + var parentOfChild = child.parent; + if (parentOfChild != null) + { + if (!childNetworkObject.TryRemoveParent(worldPositionStays)) + { + throw new Exception($"[RemoveParent] {child.name} Failed to remove itself from parent {parentOfChild.name}!"); + } + else + { + RemoveParent(parentOfChild, worldPositionStays); + } + } + } + + public void DeparentAllChildren(bool worldPositionStays = true) + { + if (IsRootParent && IsServer) + { + var lastChild = GetLastChild(transform); + if (lastChild != null) + { + RemoveParent(lastChild, worldPositionStays); + } + } + } + + private void ParentChild(InSceneParentChildHandler child, bool worldPositionStays = true) + { + if (child != null) + { + if (child.m_Parent != null) + { + var childNetworkObject = child.GetComponent(); + if (!childNetworkObject.TrySetParent(child.m_Parent.transform, worldPositionStays)) + { + throw new Exception($"[Parent] {child.name} Failed to parent itself under parent {child.m_Parent.name}!"); + } + else + { + ParentChild(child.m_Child, worldPositionStays); + } + } + } + } + + public void ReParentAllChildren(bool worldPositionStays = true) + { + if (IsRootParent && IsServer) + { + ParentChild(m_Child, worldPositionStays); + } + } + + public override void OnNetworkSpawn() + { + if (IsServer) + { + LogMessage($"[{NetworkObjectId}] Pos = ({m_TargetLocalPosition}) | Rotation ({m_TargetLocalRotation}) | Scale ({m_TargetLocalScale})"); + if (AddNetworkTransform) + { + m_NetworkTransform = gameObject.AddComponent(); + m_NetworkTransform.InLocalSpace = PreserveLocalSpace; + } + if (IsRootParent) + { + ServerRootParent = this; + } + } + else + { + if (!ClientRelativeInstances.ContainsKey(NetworkManager.LocalClientId)) + { + ClientRelativeInstances.Add(NetworkManager.LocalClientId, new Dictionary()); + } + if (!ClientRelativeInstances[NetworkManager.LocalClientId].ContainsKey(NetworkObjectId)) + { + ClientRelativeInstances[NetworkManager.LocalClientId].Add(NetworkObjectId, this); + } + } + + base.OnNetworkSpawn(); + } + + public void DeparentSetValuesAndReparent() + { + if (IsServer && IsRootParent) + { + // Back to back de-parenting and re-parenting + s_GenerateRandomValues = true; + DeparentAllChildren(WorldPositionStays); + s_GenerateRandomValues = false; + ReParentAllChildren(WorldPositionStays); + } + } + + /// + /// This handles applying the final desired transform values before the ParentSyncMessage + /// is created and sent. + /// + public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + { + if (!IsServer || !IsSpawned || parentNetworkObject != null || !s_GenerateRandomValues) + { + return; + } + + m_TargetLocalPosition = GenerateVector3(PositionMin, PositionMax); + m_TargetLocalRotation = GenerateVector3(RotationMin, RotationMax); + var scale = Random.Range(ScaleMin, ScaleMax); + m_TargetLocalScale = Vector3.one * scale; + transform.position = m_TargetLocalPosition; + transform.rotation = Quaternion.Euler(m_TargetLocalRotation); + transform.localScale = m_TargetLocalScale; + + base.OnNetworkObjectParentChanged(parentNetworkObject); + } + + + private void LateUpdate() + { + if (!IsSpawned || !IsServer || NetworkManagerTestDisabler.IsIntegrationTest) + { + return; + } + + // De-parent + if (Input.GetKeyDown(KeyCode.D) && IsRootParent) + { + DeparentAllChildren(WorldPositionStays); + } + + // Re-parent + if (Input.GetKeyDown(KeyCode.R) && IsRootParent) + { + ReParentAllChildren(WorldPositionStays); + } + + // De-parent, initialize with new values, and re-parent + if (Input.GetKeyDown(KeyCode.Tab)) + { + RootParent.DeparentSetValuesAndReparent(); + } + } + } +} diff --git a/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentChildHandler.cs.meta b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentChildHandler.cs.meta new file mode 100644 index 0000000000..94a4b95f5a --- /dev/null +++ b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentChildHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f542d9d67c235941831b6428602cd27 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentedUnderGameObjectHandler.cs b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentedUnderGameObjectHandler.cs new file mode 100644 index 0000000000..e1daa100a6 --- /dev/null +++ b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentedUnderGameObjectHandler.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Unity.Netcode; + +namespace TestProject.ManualTests +{ + public class InSceneParentedUnderGameObjectHandler : NetworkBehaviour + { + static public List Instances = new List(); + + public override void OnNetworkSpawn() + { + if (IsServer) + { + Instances.Add(this); + } + } + } +} diff --git a/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentedUnderGameObjectHandler.cs.meta b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentedUnderGameObjectHandler.cs.meta new file mode 100644 index 0000000000..4dbf901158 --- /dev/null +++ b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/InSceneParentedUnderGameObjectHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8384b01f654da304ebe165da1624cf56 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Manual/Scripts/ManualTestAssetsDestroyer.cs b/testproject/Assets/Tests/Manual/Scripts/ManualTestAssetsDestroyer.cs new file mode 100644 index 0000000000..50def2e431 --- /dev/null +++ b/testproject/Assets/Tests/Manual/Scripts/ManualTestAssetsDestroyer.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +namespace TestProject.ManualTests +{ + public class ManualTestAssetsDestroyer : MonoBehaviour + { + public static bool IsIntegrationTest; + + private void Awake() + { + if (IsIntegrationTest) + { + Destroy(gameObject); + } + } + } +} diff --git a/testproject/Assets/Tests/Manual/Scripts/ManualTestAssetsDestroyer.cs.meta b/testproject/Assets/Tests/Manual/Scripts/ManualTestAssetsDestroyer.cs.meta new file mode 100644 index 0000000000..b7f2edb622 --- /dev/null +++ b/testproject/Assets/Tests/Manual/Scripts/ManualTestAssetsDestroyer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60e2340f7e1668a4da162b39850fbdb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Manual/Scripts/NetworkManagerTestDisabler.cs b/testproject/Assets/Tests/Manual/Scripts/NetworkManagerTestDisabler.cs new file mode 100644 index 0000000000..71b86ac71b --- /dev/null +++ b/testproject/Assets/Tests/Manual/Scripts/NetworkManagerTestDisabler.cs @@ -0,0 +1,22 @@ +using UnityEngine; +using Unity.Netcode; + +namespace TestProject.ManualTests +{ + public class NetworkManagerTestDisabler : MonoBehaviour + { + public static bool IsIntegrationTest; + + private void Awake() + { + if (IsIntegrationTest) + { + var networkManager = GetComponent(); + if (networkManager != null) + { + Destroy(gameObject); + } + } + } + } +} diff --git a/testproject/Assets/Tests/Manual/Scripts/NetworkManagerTestDisabler.cs.meta b/testproject/Assets/Tests/Manual/Scripts/NetworkManagerTestDisabler.cs.meta new file mode 100644 index 0000000000..dcaaeea919 --- /dev/null +++ b/testproject/Assets/Tests/Manual/Scripts/NetworkManagerTestDisabler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf54eec7d39507743a95b32caf5b7c2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/IntegrationTestWithApproximation.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/IntegrationTestWithApproximation.cs new file mode 100644 index 0000000000..2547a102ca --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/IntegrationTestWithApproximation.cs @@ -0,0 +1,44 @@ +using UnityEngine; +using Unity.Netcode.TestHelpers.Runtime; + +namespace TestProject.RuntimeTests +{ + public abstract class IntegrationTestWithApproximation : NetcodeIntegrationTest + { + private const float k_AproximateDeltaVariance = 0.01f; + + protected virtual float GetDeltaVarianceThreshold() + { + return k_AproximateDeltaVariance; + } + + protected bool Approximately(float a, float b) + { + return Mathf.Abs(a - b) <= GetDeltaVarianceThreshold(); + } + + protected bool Approximately(Vector2 a, Vector2 b) + { + var deltaVariance = GetDeltaVarianceThreshold(); + return Mathf.Abs(a.x - b.x) <= deltaVariance && + Mathf.Abs(a.y - b.y) <= deltaVariance; + } + + protected bool Approximately(Vector3 a, Vector3 b) + { + var deltaVariance = GetDeltaVarianceThreshold(); + return Mathf.Abs(a.x - b.x) <= deltaVariance && + Mathf.Abs(a.y - b.y) <= deltaVariance && + Mathf.Abs(a.z - b.z) <= deltaVariance; + } + + protected bool Approximately(Quaternion a, Quaternion b) + { + var deltaVariance = GetDeltaVarianceThreshold(); + return Mathf.Abs(a.x - b.x) <= deltaVariance && + Mathf.Abs(a.y - b.y) <= deltaVariance && + Mathf.Abs(a.z - b.z) <= deltaVariance && + Mathf.Abs(a.w - b.w) <= deltaVariance; + } + } +} diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/IntegrationTestWithApproximation.cs.meta b/testproject/Assets/Tests/Runtime/ObjectParenting/IntegrationTestWithApproximation.cs.meta new file mode 100644 index 0000000000..af1adfd855 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/IntegrationTestWithApproximation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50a3b194bb5b8714d883dafd911db1ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity new file mode 100644 index 0000000000..355f3d788b --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity @@ -0,0 +1,1473 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &118026864 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 118026867} + - component: {fileID: 118026866} + - component: {fileID: 118026865} + - component: {fileID: 118026868} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &118026865 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 118026864} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6960e84d07fb87f47956e7a81d71c4e6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ProtocolType: 0 + m_MaxPacketQueueSize: 128 + m_MaxPayloadSize: 6144 + m_MaxSendQueueSize: 98304 + m_HeartbeatTimeoutMS: 500 + m_ConnectTimeoutMS: 1000 + m_MaxConnectAttempts: 60 + m_DisconnectTimeoutMS: 30000 + ConnectionData: + Address: 127.0.0.1 + Port: 7777 + ServerListenAddress: + DebugSimulator: + PacketDelayMS: 0 + PacketJitterMS: 0 + PacketDropRate: 0 +--- !u!114 &118026866 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 118026864} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3} + m_Name: + m_EditorClassIdentifier: + RunInBackground: 1 + LogLevel: 1 + NetworkConfig: + ProtocolVersion: 0 + NetworkTransport: {fileID: 118026865} + PlayerPrefab: {fileID: 0} + NetworkPrefabs: [] + TickRate: 30 + ClientConnectionBufferTimeout: 10 + ConnectionApproval: 0 + ConnectionData: + EnableTimeResync: 0 + TimeResyncInterval: 30 + EnsureNetworkVariableLengthSafety: 0 + EnableSceneManagement: 1 + ForceSamePrefabs: 1 + RecycleNetworkIds: 1 + NetworkIdRecycleDelay: 120 + RpcHashSize: 0 + LoadSceneTimeOut: 120 + SpawnTimeout: 1 + EnableNetworkLogs: 1 +--- !u!4 &118026867 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 118026864} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &118026868 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 118026864} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cf54eec7d39507743a95b32caf5b7c2a, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &295758851 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 295758855} + - component: {fileID: 295758854} + - component: {fileID: 295758853} + - component: {fileID: 295758852} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &295758852 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295758851} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &295758853 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295758851} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &295758854 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295758851} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &295758855 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295758851} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 844324455} + - {fileID: 605493857} + m_Father: {fileID: 1392712516} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &321630914 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 321630915} + - component: {fileID: 321630917} + m_Layer: 0 + m_Name: MainCamera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &321630915 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 321630914} + m_LocalRotation: {x: -0.011012609, y: -0, z: -0, w: 0.99993944} + m_LocalPosition: {x: 0, y: 5, z: -15} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1392712516} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: -1.262, y: 0, z: 0} +--- !u!20 &321630917 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 321630914} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 80 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!1 &505911173 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 505911176} + - component: {fileID: 505911175} + - component: {fileID: 505911174} + - component: {fileID: 505911177} + - component: {fileID: 505911178} + m_Layer: 0 + m_Name: Child-Gen2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!23 &505911174 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 505911173} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 126d2da9b339ba9418b15d150233e786, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &505911175 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 505911173} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &505911176 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 505911173} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1601792191} + m_Father: {fileID: 733348968} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &505911177 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 505911173} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 1818541129 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &505911178 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 505911173} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f542d9d67c235941831b6428602cd27, type: 3} + m_Name: + m_EditorClassIdentifier: + ParentHasNoNetworkObject: 0 + IsRootParent: 0 + IsLastChild: 0 + PreserveLocalSpace: 0 + PositionMax: {x: 100, y: 10, z: 100} + PositionMin: {x: 0, y: 0, z: 0} + RotationMax: {x: 359.99, y: 359.99, z: 359.99} + RotationMin: {x: 0.01, y: 0.01, z: 0.01} + ScaleMax: 3 + ScaleMin: 0.25 +--- !u!1 &605493856 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 605493857} + - component: {fileID: 605493859} + - component: {fileID: 605493858} + m_Layer: 5 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &605493857 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 605493856} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 295758855} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &605493858 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 605493856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &605493859 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 605493856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!1 &631269575 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 631269578} + - component: {fileID: 631269577} + - component: {fileID: 631269576} + - component: {fileID: 631269579} + - component: {fileID: 631269580} + m_Layer: 0 + m_Name: Child-Gen4 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!23 &631269576 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 631269575} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a7b755ad8e4fe4bdb8f5518c951abc70, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &631269577 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 631269575} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &631269578 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 631269575} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1601792191} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &631269579 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 631269575} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 1688266703 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &631269580 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 631269575} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f542d9d67c235941831b6428602cd27, type: 3} + m_Name: + m_EditorClassIdentifier: + ParentHasNoNetworkObject: 0 + IsRootParent: 0 + IsLastChild: 1 + PreserveLocalSpace: 0 + PositionMax: {x: 100, y: 10, z: 100} + PositionMin: {x: 0, y: 0, z: 0} + RotationMax: {x: 359.99, y: 359.99, z: 359.99} + RotationMin: {x: 0.01, y: 0.01, z: 0.01} + ScaleMax: 3 + ScaleMin: 0.25 +--- !u!1 &733348965 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 733348968} + - component: {fileID: 733348967} + - component: {fileID: 733348966} + - component: {fileID: 733348969} + - component: {fileID: 733348970} + m_Layer: 0 + m_Name: Child-Gen1 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!23 &733348966 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 733348965} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: d2f6bf650dfcc483794cdacf53f9fe2b, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &733348967 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 733348965} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &733348968 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 733348965} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 505911176} + m_Father: {fileID: 945457593} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &733348969 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 733348965} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 1181626381 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &733348970 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 733348965} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f542d9d67c235941831b6428602cd27, type: 3} + m_Name: + m_EditorClassIdentifier: + ParentHasNoNetworkObject: 0 + IsRootParent: 0 + IsLastChild: 0 + PreserveLocalSpace: 0 + PositionMax: {x: 100, y: 10, z: 100} + PositionMin: {x: 0, y: 0, z: 0} + RotationMax: {x: 359.99, y: 359.99, z: 359.99} + RotationMin: {x: 0.01, y: 0.01, z: 0.01} + ScaleMax: 3 + ScaleMin: 0.25 +--- !u!1001 &844324454 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 295758855} + m_Modifications: + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_RootOrder + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_AnchorMax.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_SizeDelta.x + value: -952 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_SizeDelta.y + value: -344 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6963777608485144162, guid: d725b5588e1b956458798319e6541d84, + type: 3} + propertyPath: m_Name + value: ConnectionModeButtons + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: d725b5588e1b956458798319e6541d84, type: 3} +--- !u!224 &844324455 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 6633621479308595792, guid: d725b5588e1b956458798319e6541d84, + type: 3} + m_PrefabInstance: {fileID: 844324454} + m_PrefabAsset: {fileID: 0} +--- !u!1 &945457592 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 945457593} + - component: {fileID: 945457595} + - component: {fileID: 945457594} + - component: {fileID: 945457596} + - component: {fileID: 945457597} + m_Layer: 0 + m_Name: RootParent_NetworkObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &945457593 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 945457592} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 733348968} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!23 &945457594 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 945457592} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 16358fcb4e0c94cc8b980fbb17259843, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &945457595 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 945457592} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!114 &945457596 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 945457592} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 3960212516 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &945457597 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 945457592} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f542d9d67c235941831b6428602cd27, type: 3} + m_Name: + m_EditorClassIdentifier: + ParentHasNoNetworkObject: 0 + IsRootParent: 1 + IsLastChild: 0 + PreserveLocalSpace: 0 + PositionMax: {x: 100, y: 10, z: 100} + PositionMin: {x: 0, y: 0, z: 0} + RotationMax: {x: 359.99, y: 359.99, z: 359.99} + RotationMin: {x: 0.01, y: 0.01, z: 0.01} + ScaleMax: 3 + ScaleMin: 0.25 +--- !u!1 &1343771061 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1343771062} + - component: {fileID: 1343771063} + m_Layer: 0 + m_Name: DirectionalLight + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1343771062 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343771061} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 2.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1392712516} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!108 &1343771063 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343771061} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!1 &1392712513 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1392712516} + - component: {fileID: 1392712517} + - component: {fileID: 1392712518} + m_Layer: 0 + m_Name: ManualTestObjects + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1392712516 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392712513} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 295758855} + - {fileID: 321630915} + - {fileID: 1343771062} + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1392712517 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392712513} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 60e2340f7e1668a4da162b39850fbdb9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &1392712518 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392712513} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 2701768183 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!1 &1601792188 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1601792191} + - component: {fileID: 1601792190} + - component: {fileID: 1601792189} + - component: {fileID: 1601792192} + - component: {fileID: 1601792193} + m_Layer: 0 + m_Name: Child-Gen3 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!23 &1601792189 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1601792188} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 7a8ec12ee27208a42a11d2944b1c1371, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1601792190 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1601792188} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1601792191 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1601792188} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 631269578} + m_Father: {fileID: 505911176} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1601792192 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1601792188} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 65702661 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &1601792193 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1601792188} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f542d9d67c235941831b6428602cd27, type: 3} + m_Name: + m_EditorClassIdentifier: + ParentHasNoNetworkObject: 0 + IsRootParent: 0 + IsLastChild: 0 + PreserveLocalSpace: 0 + PositionMax: {x: 100, y: 10, z: 100} + PositionMin: {x: 0, y: 0, z: 0} + RotationMax: {x: 359.99, y: 359.99, z: 359.99} + RotationMin: {x: 0.01, y: 0.01, z: 0.01} + ScaleMax: 3 + ScaleMin: 0.25 +--- !u!1 &1839922012 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1839922013} + m_Layer: 0 + m_Name: RootParent_GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1839922013 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1839922012} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2062433907} + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2062433906 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2062433907} + - component: {fileID: 2062433908} + - component: {fileID: 2062433909} + m_Layer: 0 + m_Name: ChildNetworkObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2062433907 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2062433906} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1839922013} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &2062433908 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2062433906} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 136663336 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &2062433909 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2062433906} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8384b01f654da304ebe165da1624cf56, type: 3} + m_Name: + m_EditorClassIdentifier: + Instances: [] diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity.meta b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity.meta new file mode 100644 index 0000000000..b8b7ca5ac5 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 49fd14bff1eceda4f9299721a9029750 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs new file mode 100644 index 0000000000..419f74cb65 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs @@ -0,0 +1,312 @@ +using System.Collections; +using System.Text; +using NUnit.Framework; +using UnityEngine.SceneManagement; +using UnityEngine.TestTools; +using Unity.Netcode; +using TestProject.ManualTests; + +namespace TestProject.RuntimeTests +{ + public class ParentingInSceneObjectsTests : IntegrationTestWithApproximation + { + private const string k_BaseSceneToLoad = "UnitTestBaseScene"; + private const string k_TestSceneToLoad = "ParentingInSceneObjects"; + private const string k_NestedUndeGameObjectName = "RootParent_GameObject"; + private const int k_NumIterationsDeparentReparent = 100; + private const float k_AproximateThresholdValue = 0.001f; + + private bool m_InitialClientsLoadedScene; + private StringBuilder m_ErrorValidationLog = new StringBuilder(0x2000); + + protected override int NumberOfClients => 2; + + protected override void OnOneTimeSetup() + { + NetworkManagerTestDisabler.IsIntegrationTest = true; + ManualTestAssetsDestroyer.IsIntegrationTest = true; + base.OnOneTimeSetup(); + } + + protected override void OnOneTimeTearDown() + { + NetworkManagerTestDisabler.IsIntegrationTest = false; + ManualTestAssetsDestroyer.IsIntegrationTest = false; + base.OnOneTimeTearDown(); + } + + private Scene m_BaseSceneLoaded; + + protected override IEnumerator OnSetup() + { + InSceneParentChildHandler.ResetInstancesTracking(m_EnableVerboseDebug); + InSceneParentedUnderGameObjectHandler.Instances.Clear(); + return base.OnSetup(); + } + + private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode mode) + { + if (scene.name == k_BaseSceneToLoad) + { + m_BaseSceneLoaded = scene; + SceneManager.sceneLoaded -= SceneManager_sceneLoaded; + } + } + + protected override IEnumerator OnTearDown() + { + if (m_BaseSceneLoaded.IsValid() && m_BaseSceneLoaded.isLoaded) + { + SceneManager.UnloadSceneAsync(m_BaseSceneLoaded); + } + yield return base.OnTearDown(); + } + + + private void GeneratePositionDoesNotMatch(InSceneParentChildHandler serverHandler, InSceneParentChildHandler clientHandler) + { + m_ErrorValidationLog.Append($"[Client-{clientHandler.NetworkManager.LocalClientId}] {nameof(NetworkObject)}-{clientHandler.NetworkObjectId}'s position {clientHandler.transform.position} does not equal the server-side position {serverHandler.transform.position}"); + } + + private void GenerateRotationDoesNotMatch(InSceneParentChildHandler serverHandler, InSceneParentChildHandler clientHandler) + { + m_ErrorValidationLog.Append($"[Client-{clientHandler.NetworkManager.LocalClientId}] {nameof(NetworkObject)}-{clientHandler.NetworkObjectId}'s rotation {clientHandler.transform.eulerAngles} does not equal the server-side scale {serverHandler.transform.eulerAngles}"); + } + + private void GenerateScaleDoesNotMatch(InSceneParentChildHandler serverHandler, InSceneParentChildHandler clientHandler) + { + m_ErrorValidationLog.Append($"[Client-{clientHandler.NetworkManager.LocalClientId}] {nameof(NetworkObject)}-{clientHandler.NetworkObjectId}'s scale {clientHandler.transform.localScale} does not equal the server-side scale {serverHandler.transform.localScale}"); + } + + private void GenerateParentIsNotCorrect(InSceneParentChildHandler handler, bool shouldHaveParent) + { + var serverOrClient = handler.NetworkManager.IsServer ? "Server" : "Client"; + if (!shouldHaveParent) + { + m_ErrorValidationLog.Append($"[{serverOrClient }-{handler.NetworkManager.LocalClientId}] {nameof(NetworkObject)}-{handler.NetworkObjectId}'s still has the parent {handler.transform.parent.name} when it should be null!"); + } + else + { + m_ErrorValidationLog.Append($"[{serverOrClient }-{handler.NetworkManager.LocalClientId}] {nameof(NetworkObject)}-{handler.NetworkObjectId}'s does not have a parent when it should!"); + } + } + + private bool ValidateClientAgainstServerTransformValues() + { + // We reset this each time because we are only interested in the last time it checked and failed + m_ErrorValidationLog.Clear(); + foreach (var instance in InSceneParentChildHandler.ServerRelativeInstances) + { + var serverInstanceTransform = instance.Value.transform; + foreach (var clientInstances in InSceneParentChildHandler.ClientRelativeInstances) + { + Assert.True(clientInstances.Value.ContainsKey(instance.Key), $"Client-{clientInstances.Key} did not spawn NetworkObject-{instance.Key}!"); + var clientInstance = clientInstances.Value[instance.Key]; + var clientInstanceTransform = clientInstance.transform; + if (!Approximately(serverInstanceTransform.position, clientInstanceTransform.position)) + { + GeneratePositionDoesNotMatch(instance.Value, clientInstance); + return false; + } + + if (!Approximately(serverInstanceTransform.eulerAngles, clientInstanceTransform.eulerAngles)) + { + GeneratePositionDoesNotMatch(instance.Value, clientInstance); + return false; + } + + if (!Approximately(serverInstanceTransform.localScale, clientInstanceTransform.localScale)) + { + GeneratePositionDoesNotMatch(instance.Value, clientInstance); + return false; + } + } + } + return true; + } + + private bool ValidateAllChildrenParentingStatus(bool checkForParent) + { + foreach (var instance in InSceneParentChildHandler.ServerRelativeInstances) + { + if (!instance.Value.IsRootParent) + { + if (checkForParent && instance.Value.transform.parent == null) + { + GenerateParentIsNotCorrect(instance.Value, checkForParent); + return false; + } + else if (!checkForParent && instance.Value.transform.parent != null) + { + GenerateParentIsNotCorrect(instance.Value, checkForParent); + return false; + } + } + } + + foreach (var clientInstances in InSceneParentChildHandler.ClientRelativeInstances) + { + foreach (var instance in clientInstances.Value) + { + if (!instance.Value.IsRootParent) + { + if (checkForParent && instance.Value.transform.parent == null) + { + GenerateParentIsNotCorrect(instance.Value, checkForParent); + return false; + } + else if (!checkForParent && instance.Value.transform.parent != null) + { + GenerateParentIsNotCorrect(instance.Value, checkForParent); + return false; + } + } + } + } + + return true; + } + + protected override float GetDeltaVarianceThreshold() + { + return k_AproximateThresholdValue; + } + + public enum ParentingSpace + { + WorldPositionStays, + WorldPositionDoesNotStay + } + + /// + /// This tests various nested children scenarios where it tests: + /// Children have their parent removed + /// The associated children and root parent have their position, rotation, and scale changed + /// Children are placed back under their respective parents + /// Verifying a late joining client's cloned version of the in-scene placed NetworkObject children + /// have the correct transform values and parent set (or no parent) + /// That users can remove parents, assign parents, and set transform values in the same pass. + /// All of the above is tested with WorldPositionStays set to both true and false. + /// + [UnityTest] + public IEnumerator InSceneParentingTest([Values] ParentingSpace parentingSpace) + { + InSceneParentChildHandler.WorldPositionStays = parentingSpace == ParentingSpace.WorldPositionStays; + SceneManager.sceneLoaded += SceneManager_sceneLoaded; + SceneManager.LoadScene(k_BaseSceneToLoad, LoadSceneMode.Additive); + m_InitialClientsLoadedScene = false; + m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; + + var sceneEventStartedStatus = m_ServerNetworkManager.SceneManager.LoadScene(k_TestSceneToLoad, LoadSceneMode.Additive); + Assert.True(sceneEventStartedStatus == SceneEventProgressStatus.Started, $"Failed to load scene {k_TestSceneToLoad} with a return status of {sceneEventStartedStatus}."); + yield return WaitForConditionOrTimeOut(() => m_InitialClientsLoadedScene); + AssertOnTimeout($"Timed out waiting for all clients to load scene {k_TestSceneToLoad}!"); + + // [Currently Connected Clients] + // remove the parents, change all transform values, and re-parent + InSceneParentChildHandler.ServerRootParent.DeparentSetValuesAndReparent(); + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"Timed out waiting for all clients transform values to match the server transform values!\n {m_ErrorValidationLog}"); + + // [Late Join Client #1] + // Make sure the late joining client synchronizes properly + yield return CreateAndStartNewClient(); + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"Timed out waiting for the late joining client's transform values to match the server transform values!\n {m_ErrorValidationLog}"); + + // Remove the parents from all of the children + InSceneParentChildHandler.ServerRootParent.DeparentAllChildren(); + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"[Late Join 1] Timed out waiting for all clients transform values to match the server transform values!\n {m_ErrorValidationLog}"); + + yield return WaitForConditionOrTimeOut(() => ValidateAllChildrenParentingStatus(false)); + AssertOnTimeout($"[Late Join 1] Timed out waiting for all children to be removed from their parent!\n {m_ErrorValidationLog}"); + + // [Late Join Client #2] + // Make sure the late joining client synchronizes properly with all children having their parent removed + yield return CreateAndStartNewClient(); + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"[Late Join 2] Timed out waiting for the late joining client's transform values to match the server transform values!\n {m_ErrorValidationLog}"); + + // Just a sanity check that late joining client #2 has no child parented + yield return WaitForConditionOrTimeOut(() => ValidateAllChildrenParentingStatus(false)); + AssertOnTimeout($"[Late Join 2] Timed out waiting for late joined client's children objects to have no parent!\n {m_ErrorValidationLog}"); + + // Finally, re-parent all of the children to make sure late joining client #2 synchronizes properly + InSceneParentChildHandler.ServerRootParent.ReParentAllChildren(); + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"[Late Join 2] Timed out waiting for all clients transform values to match the server transform values!\n {m_ErrorValidationLog}"); + yield return WaitForConditionOrTimeOut(() => ValidateAllChildrenParentingStatus(true)); + AssertOnTimeout($"[Late Join 2] Timed out waiting for all children to be removed from their parent!\n {m_ErrorValidationLog}"); + + // Now run through many iterations where we remove the parents, set the parents, and while + // the parents are being set the InSceneParentChildHandler assigns new position, rotation, and scale values + // in the OnNetworkObjectParentChanged overridden method on the server side only + for (int i = 0; i < k_NumIterationsDeparentReparent; i++) + { + InSceneParentChildHandler.ServerRootParent.DeparentSetValuesAndReparent(); + + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"[Final Pass] Timed out waiting for all clients transform values to match the server transform values!\n {m_ErrorValidationLog}"); + + yield return WaitForConditionOrTimeOut(() => ValidateAllChildrenParentingStatus(true)); + AssertOnTimeout($"[Final Pass] Timed out waiting for all children to be removed from their parent!\n {m_ErrorValidationLog}"); + } + + // In the final pass, we remove the second generation nested child + var firstGenChild = InSceneParentChildHandler.ServerRootParent.transform.GetChild(0); + var secondGenChild = firstGenChild.GetChild(0); + var secondGenChildNetworkObject = secondGenChild.GetComponent(); + Assert.True(secondGenChildNetworkObject.TrySetParent((NetworkObject)null, false), $"[Final Pass] Failed to remove the parent from the second generation child!"); + + // Validate all transform values match + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"[Final Pass] Timed out waiting for all clients transform values to match the server transform values after the second generation child's parent was removed!\n {m_ErrorValidationLog}"); + + // Now run through one last de-parent, re-parent, and set new values pass to make sure everything still synchronizes + InSceneParentChildHandler.ServerRootParent.DeparentSetValuesAndReparent(); + + yield return WaitForConditionOrTimeOut(ValidateClientAgainstServerTransformValues); + AssertOnTimeout($"[Final Pass - Last Test] Timed out waiting for all clients transform values to match the server transform values!\n {m_ErrorValidationLog}"); + + yield return WaitForConditionOrTimeOut(() => ValidateAllChildrenParentingStatus(true)); + AssertOnTimeout($"[Final Pass - Last Test] Timed out waiting for all children to be removed from their parent!\n {m_ErrorValidationLog}"); + } + + private void SceneManager_OnSceneEvent(SceneEvent sceneEvent) + { + if (sceneEvent.SceneName != k_TestSceneToLoad) + { + return; + } + + if (sceneEvent.ClientId == m_ServerNetworkManager.LocalClientId && sceneEvent.SceneEventType == SceneEventType.LoadEventCompleted) + { + m_InitialClientsLoadedScene = true; + } + } + + /// + /// This verifies in-scene placed NetworkObject's nested under a GameObject without a NetworkObject + /// component will preserve that parent hierarchy. + /// + [UnityTest] + public IEnumerator InSceneNestedUnderGameObjectTest() + { + SceneManager.sceneLoaded += SceneManager_sceneLoaded; + SceneManager.LoadScene(k_BaseSceneToLoad, LoadSceneMode.Additive); + m_InitialClientsLoadedScene = false; + m_ServerNetworkManager.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent; + + var sceneEventStartedStatus = m_ServerNetworkManager.SceneManager.LoadScene(k_TestSceneToLoad, LoadSceneMode.Additive); + Assert.True(sceneEventStartedStatus == SceneEventProgressStatus.Started, $"Failed to load scene {k_TestSceneToLoad} with a return status of {sceneEventStartedStatus}."); + yield return WaitForConditionOrTimeOut(() => m_InitialClientsLoadedScene); + AssertOnTimeout($"Timed out waiting for all clients to load scene {k_TestSceneToLoad}!"); + + foreach (var instance in InSceneParentedUnderGameObjectHandler.Instances) + { + Assert.False(instance.transform.parent == null, $"{instance.name}'s parent is null when it should not be!"); + } + } + } +} diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs.meta b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs.meta new file mode 100644 index 0000000000..383199e0df --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjectsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eb1bdec28131915419a2825132406932 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingWorldPositionStaysTests.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingWorldPositionStaysTests.cs new file mode 100644 index 0000000000..87507cfdc9 --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingWorldPositionStaysTests.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode; +using Unity.Netcode.Components; +using Object = UnityEngine.Object; + +namespace TestProject.RuntimeTests +{ + public class ParentingWorldPositionStaysTests : IntegrationTestWithApproximation + { + private const int k_NestedChildren = 10; + private const string k_ParentName = "Parent"; + private const string k_ChildName = "Child"; + + protected override int NumberOfClients => 2; + + internal class TestComponentHelper : NetworkBehaviour + { + internal class ChildInfo + { + public bool HasBeenParented; + public GameObject Child; + } + + internal class ParentChildInfo + { + public GameObject RootParent; + public List Children = new List(); + } + + public static Dictionary NetworkObjectIdToIndex = new Dictionary(); + + public static Dictionary ClientsRegistered = new Dictionary(); + + public Vector3 Scale; + public bool WorldPositionStays; + + public override void OnNetworkSpawn() + { + if (!IsServer) + { + var localClientId = NetworkManager.LocalClientId; + if (!ClientsRegistered.ContainsKey(localClientId)) + { + ClientsRegistered.Add(localClientId, new ParentChildInfo()); + // Fill the expected entries with null values + for (int i = 0; i < k_NestedChildren; i++) + { + ClientsRegistered[localClientId].Children.Add(new ChildInfo()); + } + } + + var entryToModify = ClientsRegistered[NetworkManager.LocalClientId]; + if (gameObject.name.Contains(k_ParentName)) + { + if (entryToModify.RootParent == null) + { + entryToModify.RootParent = gameObject; + return; + } + else + { + throw new Exception($"Failed to assigned {gameObject.name} as a parent! {nameof(GameObject)} {entryToModify.RootParent.name} is already assigned to Client-{localClientId}'s parent entry!"); + } + } + + + if (gameObject.name.Contains(k_ChildName)) + { + if (!NetworkObjectIdToIndex.ContainsKey(NetworkObjectId)) + { + //This should never happen (sanity check) + throw new Exception($"Client spawned {NetworkObjectId} but there was no index lookup table!"); + } + var childIndex = NetworkObjectIdToIndex[NetworkObjectId]; + var childInfo = ClientsRegistered[localClientId].Children[childIndex]; + if (childInfo.Child == null) + { + childInfo.Child = gameObject; + ClientsRegistered[localClientId].Children[childIndex] = childInfo; + return; + } + else + { + throw new Exception($"Failed to assigned {gameObject.name} already assigned! {nameof(GameObject)} { ClientsRegistered[localClientId].Children[childIndex].Child.name} is already assigned to Client-{localClientId}'s child entry!"); + } + } + // We should never reach this point + throw new Exception($"We spawned {name} but did not assign anything!"); + } + base.OnNetworkSpawn(); + } + + public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + { + base.OnNetworkObjectParentChanged(parentNetworkObject); + if (parentNetworkObject == null || IsServer) + { + if (WorldPositionStays) + { + transform.localScale = Scale; + } + return; + } + var localClientId = NetworkManager.LocalClientId; + if (!ClientsRegistered.ContainsKey(localClientId)) + { + throw new Exception($"Parented {gameObject.name} before it was spawned!"); + } + + var netObjId = NetworkObject.NetworkObjectId; + var childIndex = NetworkObjectIdToIndex[netObjId]; + var childInfo = ClientsRegistered[localClientId].Children[childIndex]; + + if (!NetworkObjectIdToIndex.ContainsKey(netObjId)) + { + if (netObjId == 0) + { + return; + } + //This should never happen (sanity check) + throw new Exception($"Client spawned {NetworkObjectId} but there was no index lookup table!"); + } + + childInfo.HasBeenParented = true; + } + } + + public enum ParentingTestModes + { + LocalPositionStays, + WorldPositionStays + } + + public enum NetworkTransformSettings + { + None, + NetworkTransformInterpolate, + NetworkTransformImmediate + } + + private GameObject m_ParentPrefabObject; + private GameObject m_ChildPrefabObject; + + private GameObject m_ServerSideParent; + private List m_ServerSideChildren = new List(); + + private Vector3 m_ParentStartPosition = new Vector3(1.0f, 1.0f, 1.0f); + private Quaternion m_ParentStartRotation = Quaternion.Euler(0.0f, 90.0f, 0.0f); + private Vector3 m_ChildStartPosition = new Vector3(100.0f, -100.0f, 100.0f); + private Quaternion m_ChildStartRotation = Quaternion.Euler(-35.0f, 0.0f, -180.0f); + private Vector3 m_ChildStartScale = Vector3.one; + + protected override IEnumerator OnSetup() + { + TestComponentHelper.ClientsRegistered.Clear(); + TestComponentHelper.NetworkObjectIdToIndex.Clear(); + for (int i = 0; i < k_NestedChildren; i++) + { + m_ServerSideChildren.Add(null); + } + return base.OnSetup(); + } + + protected override IEnumerator OnTearDown() + { + if (m_ServerSideParent != null && m_ServerSideParent.GetComponent().IsSpawned) + { + // Clean up in reverse order (also makes sure we can despawn parents before children) + m_ServerSideParent.GetComponent().Despawn(); + } + + // Now despawn the children + // (and clean up our test) + for (int i = 0; i < k_NestedChildren; i++) + { + var serverSideChild = m_ServerSideChildren[i]; + if (serverSideChild != null && serverSideChild.GetComponent().IsSpawned) + { + serverSideChild.GetComponent().Despawn(); + } + } + + // Just allow the clients to run through despawning (also assures nothing throws an exception when destroying) + yield return new WaitForSeconds(0.2f); + + m_ServerSideChildren.Clear(); + TestComponentHelper.ClientsRegistered.Clear(); + TestComponentHelper.NetworkObjectIdToIndex.Clear(); + m_ParentPrefabObject = null; + m_ChildPrefabObject = null; + m_ServerSideParent = null; + yield return base.OnTearDown(); + } + + protected override void OnServerAndClientsCreated() + { + m_ParentPrefabObject = CreateNetworkObjectPrefab(k_ParentName); + m_ParentPrefabObject.AddComponent(); + m_ParentPrefabObject.transform.position = m_ParentStartPosition; + m_ParentPrefabObject.transform.rotation = m_ParentStartRotation; + m_ChildPrefabObject = CreateNetworkObjectPrefab(k_ChildName); + m_ChildPrefabObject.AddComponent(); + m_ChildPrefabObject.transform.position = m_ChildStartPosition; + m_ChildPrefabObject.transform.rotation = m_ChildStartRotation; + m_ChildPrefabObject.transform.localScale = m_ChildStartScale; + m_ServerNetworkManager.LogLevel = m_EnableVerboseDebug ? LogLevel.Developer : LogLevel.Normal; + + base.OnServerAndClientsCreated(); + } + + protected override void OnNewClientCreated(NetworkManager networkManager) + { + foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.NetworkPrefabs) + { + networkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + } + } + + private bool HaveAllClientsSpawnedObjects() + { + foreach (var client in m_ClientNetworkManagers) + { + if (!s_GlobalNetworkObjects.ContainsKey(client.LocalClientId)) + { + return false; + } + var clientSpawnedObjects = s_GlobalNetworkObjects[client.LocalClientId]; + foreach (var gameObject in m_ServerSideChildren) + { + var networkOject = gameObject.GetComponent(); + if (!clientSpawnedObjects.ContainsKey(networkOject.NetworkObjectId)) + { + return false; + } + } + } + return true; + } + + private bool HaveAllClientsParentedChild() + { + foreach (var clientEntries in TestComponentHelper.ClientsRegistered) + { + foreach (var clientInfo in clientEntries.Value.Children) + { + if (!clientInfo.HasBeenParented) + { + return false; + } + } + } + return true; + } + + /// + /// Verifies that using worldPositionStays when parenting via NetworkObject.TrySetParent, + /// that the client-side transform values match that of the server-side. + /// This also tests nested parenting and out of hierarchical order child spawning. + /// + [UnityTest] + public IEnumerator WorldPositionStaysTest([Values] ParentingTestModes mode, [Values] NetworkTransformSettings networkTransformSettings) + { + var useNetworkTransform = networkTransformSettings != NetworkTransformSettings.None; + var interpolate = networkTransformSettings == NetworkTransformSettings.NetworkTransformInterpolate; + var worldPositionStays = mode == ParentingTestModes.WorldPositionStays; + var startTime = Time.realtimeSinceStartup; + m_ServerSideParent = Object.Instantiate(m_ParentPrefabObject); + + var serverSideChildNetworkObjects = new List(); + var childPosition = m_ChildStartPosition; + var childRotation = m_ChildStartRotation; + var childScale = m_ChildStartScale; + // Used to store the expected position and rotation for children (local space relative) + var childPositionList = new List(); + var childRotationList = new List(); + var childScaleList = new List(); + var childLarger = 1.15f; + var childSmaller = 0.85f; + if (useNetworkTransform) + { + var networkTransform = m_ChildPrefabObject.AddComponent(); + networkTransform.InLocalSpace = !worldPositionStays; + } + + var serverSideParentNetworkObject = m_ServerSideParent.GetComponent(); + serverSideParentNetworkObject.Spawn(); + + // Instantiate the children + for (int i = 0; i < k_NestedChildren; i++) + { + m_ServerSideChildren[i] = Object.Instantiate(m_ChildPrefabObject); + childPositionList.Add(childPosition); + childRotationList.Add(childRotation.eulerAngles); + childScaleList.Add(childScale); + // Change each child's position, rotation, and scale + childRotation = Quaternion.Euler(childRotation.eulerAngles * 0.80f); + childPosition = childPosition * 0.80f; + if ((i % 2) == 0) + { + childScale = m_ChildStartScale * childLarger; + childLarger *= childLarger; + } + else + { + childScale = m_ChildStartScale * childSmaller; + childSmaller *= childSmaller; + } + var serverSideChild = m_ServerSideChildren[i]; + + var serverSideChildNetworkObject = serverSideChild.GetComponent(); + serverSideChild.transform.position = childPositionList[i]; + serverSideChild.transform.rotation = Quaternion.Euler(childRotationList[i]); + + serverSideChild.transform.localScale = childScaleList[i]; + VerboseDebug($"[Server][PreSpawn] Set scale of NetworkObject to ({childScaleList[i]})"); + serverSideChildNetworkObject.Spawn(); + VerboseDebug($"[Server] Set scale of NetworkObjectID ({serverSideChildNetworkObject.NetworkObjectId}) to ({childScaleList[i]}) and is currently {serverSideChild.transform.localScale}"); + + TestComponentHelper.NetworkObjectIdToIndex.Add(serverSideChildNetworkObject.NetworkObjectId, i); + Assert.IsTrue(Approximately(m_ServerSideParent.transform.position, m_ParentStartPosition)); + Assert.IsTrue(Approximately(m_ServerSideParent.transform.rotation.eulerAngles, m_ParentStartRotation.eulerAngles)); + Assert.IsTrue(Approximately(serverSideChild.transform.position, childPositionList[i])); + Assert.IsTrue(Approximately(serverSideChild.transform.rotation.eulerAngles, childRotationList[i])); + Assert.IsTrue(Approximately(serverSideChild.transform.localScale, childScaleList[i]), $"[Initial Scale] Server-side child scale ({serverSideChild.transform.localScale}) does not equal the expected scale ({childScaleList[i]})"); + } + + VerboseDebug($"[{Time.realtimeSinceStartup - startTime}] Spawned parent and child objects."); + + // Wait for clients to spawn the NetworkObjects + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawnedObjects); + AssertOnTimeout("Timed out waiting for all clients to spawn the respective parent and child objects"); + VerboseDebug($"[{Time.realtimeSinceStartup - startTime}] Clients spawned parent and child objects."); + + // Verify the positions are identical to the default values + foreach (var clientEntry in TestComponentHelper.ClientsRegistered) + { + var children = clientEntry.Value.Children; + var rootParent = clientEntry.Value.RootParent; + Assert.IsTrue(Approximately(rootParent.transform.position, m_ParentStartPosition)); + Assert.IsTrue(Approximately(rootParent.transform.rotation.eulerAngles, m_ParentStartRotation.eulerAngles)); + for (int i = 0; i < k_NestedChildren; i++) + { + var clientChildInfo = children[i]; + var serverChild = m_ServerSideChildren[i]; + + Assert.IsFalse(clientChildInfo.HasBeenParented, $"Client-{clientEntry.Key} has already been parented!"); + + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.position, serverChild.transform.position), $"[Client-{clientEntry.Key}][{clientChildInfo.Child.name}] Child position ({clientChildInfo.Child.transform.position}) does not" + + $" equal the server-side child's position ({serverChild.transform.position})"); + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.eulerAngles, serverChild.transform.eulerAngles), $"[Client-{clientEntry.Key}][{clientChildInfo.Child.name}] Child rotation ({clientChildInfo.Child.transform.eulerAngles}) does not" + + $" equal the server-side child's rotation ({serverChild.transform.eulerAngles})"); + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localScale, serverChild.transform.localScale), $"[Client-{clientEntry.Key}][{clientChildInfo.Child.name}] Child scale ({clientChildInfo.Child.transform.localScale}) does not" + + $" equal the server-side child's scale ({serverChild.transform.localScale})"); + } + } + + var currentParent = serverSideParentNetworkObject; + for (int i = 0; i < k_NestedChildren; i++) + { + var childNetworkObject = m_ServerSideChildren[i].GetComponent(); + VerboseDebug($"[Server Parenting][Before] Scale of NetworkObjectID ({childNetworkObject.NetworkObjectId}) is currently {childNetworkObject.transform.localScale}"); + Assert.True(childNetworkObject.TrySetParent(currentParent, worldPositionStays)); + VerboseDebug($"[Server Parenting][After] Scale of NetworkObjectID ({childNetworkObject.NetworkObjectId}) is now {childNetworkObject.transform.localScale}"); + currentParent = childNetworkObject; + } + + VerboseDebug($"[{Time.realtimeSinceStartup - startTime}] Parented all children."); + + // Wait for all client instances to have been parented. + yield return WaitForConditionOrTimeOut(HaveAllClientsParentedChild); + AssertOnTimeout("Timed out waiting for all clients to parent the child object!"); + VerboseDebug($"[{Time.realtimeSinceStartup - startTime}] All clients parented the child."); + + var serverParentTransform = m_ServerSideParent.transform; + // Verify the positions are identical to the default values + foreach (var clientEntry in TestComponentHelper.ClientsRegistered) + { + var children = clientEntry.Value.Children; + var rootParent = clientEntry.Value.RootParent; + Assert.IsTrue(Approximately(rootParent.transform.position, m_ServerSideParent.transform.position), $"Client-{clientEntry.Key} parent's position ({rootParent.transform.position}) does not equal the server parent's position ({serverParentTransform.position})!"); + Assert.IsTrue(Approximately(rootParent.transform.rotation.eulerAngles, m_ServerSideParent.transform.rotation.eulerAngles), $"Client-{clientEntry.Key} parent's rotation ({rootParent.transform.rotation.eulerAngles}) does not equal the server parent's position ({serverParentTransform.rotation.eulerAngles})!"); + for (int i = 0; i < k_NestedChildren; i++) + { + var clientChildInfo = children[i]; + var serverChild = m_ServerSideChildren[i]; + Assert.IsTrue(clientChildInfo.HasBeenParented, $"Client-{clientEntry.Key} has not been parented!"); + // Assure we mirror the server + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.position, serverChild.transform.position), $"Client-{clientEntry.Key} child's position ({clientChildInfo.Child.transform.position}) does not equal the server child's position ({serverChild.transform.position})!"); + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.eulerAngles, serverChild.transform.rotation.eulerAngles), $"Client-{clientEntry.Key} child's rotation ({clientChildInfo.Child.transform.rotation.eulerAngles}) does not equal the server child's rotation ({serverChild.transform.rotation.eulerAngles})!"); + if (useNetworkTransform) + { + yield return WaitForConditionOrTimeOut(() => Approximately(clientChildInfo.Child.transform.localScale, serverChild.transform.localScale)); + AssertOnTimeout($"Timed out waiting for client-{clientEntry.Key} child's scale ({clientChildInfo.Child.transform.localScale}) to equal the server child's scale ({serverChild.transform.localScale}) [Has NetworkTransform]"); + } + else + { + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localScale, serverChild.transform.localScale), $"Client-{clientEntry.Key} child's scale ({clientChildInfo.Child.transform.localScale}) does not equal the server child's scale ({serverChild.transform.localScale})"); + } + + // Assure we still have the same local space values when not preserving the world position + if (!worldPositionStays) + { + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localPosition, childPositionList[i]), $"Client-{clientEntry.Key} child's local space position ({clientChildInfo.Child.transform.localPosition}) does not equal the default child's position ({childPositionList[i]})!"); + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localRotation.eulerAngles, childRotationList[i]), $"Client-{clientEntry.Key} child's local space rotation ({clientChildInfo.Child.transform.localRotation.eulerAngles}) does not equal the server child's rotation ({childRotationList[i]})!"); + } + } + } + + // Late join a client and run through the same checks + yield return CreateAndStartNewClient(); + AssertOnTimeout("[Late-Join] Timed out waiting for client to late join!"); + + // Wait for clients to spawn the NetworkObjects + yield return WaitForConditionOrTimeOut(HaveAllClientsSpawnedObjects); + AssertOnTimeout("[Late-Join] Timed out waiting for all clients to spawn the respective parent and child objects"); + + // Wait for all client instances to have been parented. + yield return WaitForConditionOrTimeOut(HaveAllClientsParentedChild); + AssertOnTimeout("[Late-Join] Timed out waiting for all clients to parent the child object!"); + + // Verify the positions are identical to the default values + foreach (var clientEntry in TestComponentHelper.ClientsRegistered) + { + var children = clientEntry.Value.Children; + var rootParent = clientEntry.Value.RootParent; + Assert.IsTrue(Approximately(rootParent.transform.position, m_ServerSideParent.transform.position), $"[LateJoin] Client-{clientEntry.Key} parent's position ({rootParent.transform.position}) does not equal the server parent's position ({serverParentTransform.position})!"); + Assert.IsTrue(Approximately(rootParent.transform.rotation.eulerAngles, m_ServerSideParent.transform.rotation.eulerAngles), $"[LateJoin] Client-{clientEntry.Key} parent's rotation ({rootParent.transform.rotation.eulerAngles}) does not equal the server parent's position ({serverParentTransform.rotation.eulerAngles})!"); + for (int i = 0; i < k_NestedChildren; i++) + { + var clientChildInfo = children[i]; + var serverChild = m_ServerSideChildren[i]; + Assert.IsTrue(clientChildInfo.HasBeenParented, $"[LateJoin] Client-{clientEntry.Key} has not been parented!"); + // Assure we mirror the server + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.position, serverChild.transform.position), $"[LateJoin] Client-{clientEntry.Key} child's position ({clientChildInfo.Child.transform.position}) does not equal the server child's position ({serverChild.transform.position})!"); + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.eulerAngles, serverChild.transform.rotation.eulerAngles), $"[LateJoin] Client-{clientEntry.Key} child's rotation ({clientChildInfo.Child.transform.rotation.eulerAngles}) does not equal the server child's rotation ({serverChild.transform.rotation.eulerAngles})!"); + + if (useNetworkTransform) + { + yield return WaitForConditionOrTimeOut(() => Approximately(clientChildInfo.Child.transform.localScale, serverChild.transform.localScale)); + AssertOnTimeout($"[Late Join] Timed out waiting for client-{clientEntry.Key} child's scale ({clientChildInfo.Child.transform.localScale}) to equal the server child's scale ({serverChild.transform.localScale}) [Has NetworkTransform]"); + } + else + { + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localScale, serverChild.transform.localScale), $"[LateJoin] Client-{clientEntry.Key} child's scale ({clientChildInfo.Child.transform.localScale}) does not equal the server child's scale ({serverChild.transform.localScale})"); + } + + // Assure we still have the same local space values when not preserving the world position + if (!worldPositionStays) + { + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localPosition, childPositionList[i]), $"[LateJoin] Client-{clientEntry.Key} child's local space position ({clientChildInfo.Child.transform.localPosition}) does not equal the default child's position ({childPositionList[i]})!"); + Assert.IsTrue(Approximately(clientChildInfo.Child.transform.localRotation.eulerAngles, childRotationList[i]), $"[LateJoin] Client-{clientEntry.Key} child's local space rotation ({clientChildInfo.Child.transform.localRotation.eulerAngles}) does not equal the server child's rotation ({childRotationList[i]})!"); + } + } + } + VerboseDebug($"[{Time.realtimeSinceStartup - startTime}] Late joined client was validated. Test completed!"); + } + } +} diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingWorldPositionStaysTests.cs.meta b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingWorldPositionStaysTests.cs.meta new file mode 100644 index 0000000000..3b3e7d27fb --- /dev/null +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentingWorldPositionStaysTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea191714be7765a41ba7cbd26b6ecfa0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/ProjectSettings/EditorBuildSettings.asset b/testproject/ProjectSettings/EditorBuildSettings.asset index d10c3f223c..a1496f313c 100644 --- a/testproject/ProjectSettings/EditorBuildSettings.asset +++ b/testproject/ProjectSettings/EditorBuildSettings.asset @@ -110,6 +110,9 @@ EditorBuildSettings: - enabled: 1 path: Assets/Samples/Teleport/TeleportSample.unity guid: efa247d1f78ca694f8d2dcb5672e8f8b + - enabled: 1 + path: Assets/Tests/Runtime/ObjectParenting/ParentingInSceneObjects.unity + guid: 49fd14bff1eceda4f9299721a9029750 m_configObjects: com.unity.addressableassets: {fileID: 11400000, guid: 5a3d5c53c25349c48912726ae850f3b0, type: 2}