diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 38491c49d6..d20088feb1 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -417,6 +417,24 @@ private GlobalObjectId GetGlobalId() /// public bool AutoObjectParentSync = true; + /// + /// Determines if the owner will apply transform values sent by the parenting message. + /// + /// + /// When enabled, the resultant parenting transform changes sent by the authority will be applied on all instances.
+ /// When disabled, the resultant parenting transform changes sent by the authority will not be applied on the owner's instance.
+ /// When disabled, all non-owner instances will still be synchronized by the authority's transform values when parented. + ///
+ [Tooltip("When disabled (default enabled), the owner will not apply a server or host's transform properties when parenting changes. Primarily useful for client-server network topology configurations.")] + public bool SyncOwnerTransformWhenParented = true; + + /// + /// Client-Server specific, when enabled an owner of a NetworkObject can parent locally as opposed to requiring the owner to notify the server it would like to be parented. + /// This behavior is always true when using a distributed authority network topology and does not require it to be set. + /// + [Tooltip("When enabled (default disabled), owner's can parent a NetworkObject locally without having to send an RPC to the server or host. Only pertinent when using client-server network topology configurations.")] + public bool AllowOwnerToParent; + internal readonly HashSet Observers = new HashSet(); #if MULTIPLAYER_TOOLS @@ -1086,8 +1104,9 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) { return false; } - - if (!NetworkManager.IsServer && !NetworkManager.ShutdownInProgress) + // If we don't have authority and we are not shutting down, then don't allow any parenting. + // If we are shutting down and don't have authority then allow it. + if (!(NetworkManager.IsServer || (AllowOwnerToParent && IsOwner)) && !NetworkManager.ShutdownInProgress) { return false; } @@ -1102,6 +1121,8 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) return false; } + + m_CachedWorldPositionStays = worldPositionStays; if (parent == null) @@ -1135,7 +1156,9 @@ private void OnTransformParentChanged() return; } - if (!NetworkManager.IsServer) + var hasAuthority = NetworkManager.IsServer || (AllowOwnerToParent && IsOwner); + + if (!hasAuthority) { // Log exception if we are a client and not shutting down. if (!NetworkManager.ShutdownInProgress) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs index cbb6a974e4..e3d11d872a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -106,19 +106,42 @@ public void Handle(ref NetworkContext context) 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) + + // This check is primarily for client-server network topologies when the motion model is owner authoritative: + // When SyncOwnerTransformWhenParented is enabled, then always apply the transform values. + // When SyncOwnerTransformWhenParented is disabled, then only synchronize the transform on non-owner instances. + if (networkObject.SyncOwnerTransformWhenParented || (!networkObject.SyncOwnerTransformWhenParented && !networkObject.IsOwner)) { - networkObject.transform.localPosition = Position; - networkObject.transform.localRotation = Rotation; + // 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; + } } - else + networkObject.transform.localScale = Scale; + + // If client side parenting is enabled and this is the server instance, then notify the rest of the connected clients that parenting has taken place. + if (networkObject.AllowOwnerToParent && context.SenderId == networkObject.OwnerClientId && networkManager.IsServer) { - networkObject.transform.position = Position; - networkObject.transform.rotation = Rotation; + var size = 0; + var message = this; + foreach (var client in networkManager.ConnectedClients) + { + if (client.Value.ClientId == networkObject.OwnerClientId || client.Value.ClientId == networkManager.LocalClientId || !networkObject.IsNetworkVisibleTo(client.Value.ClientId)) + { + continue; + } + size = networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId); + networkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } } - networkObject.transform.localScale = Scale; } } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs index 08789c56ac..0222ebba16 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs @@ -347,7 +347,6 @@ protected override void OnNewClientCreated(NetworkManager networkManager) base.OnNewClientCreated(networkManager); } - /// /// Returns true when the server-host and all clients have /// instantiated the child object to be used in @@ -378,6 +377,26 @@ protected bool AllChildObjectInstancesAreSpawned() return true; } + /// + /// Conditional check that all child object instances also have a child + /// + /// true if they do and false if they do not + protected bool AllFirstLevelChildObjectInstancesHaveChild() + { + foreach (var instance in ChildObjectComponent.ClientInstances.Values) + { + if (instance.transform.parent == null) + { + return false; + } + } + return true; + } + + /// + /// Conditional check that all child instances have a child. + /// + /// true if they do and false if they do not protected bool AllChildObjectInstancesHaveChild() { foreach (var instance in ChildObjectComponent.ClientInstances.Values) @@ -400,6 +419,41 @@ protected bool AllChildObjectInstancesHaveChild() return true; } + /// + /// Conditional check that all first level child objects have no parent. + /// + /// true if they do and false if they do not + protected bool AllFirstLevelChildObjectInstancesHaveNoParent() + { + foreach (var instance in ChildObjectComponent.ClientInstances.Values) + { + if (instance.transform.parent != null) + { + return false; + } + } + return true; + } + + /// + /// Conditional check that all sub-child objects have no parent. + /// + /// true if they do and false if they do not + protected bool AllSubChildObjectInstancesHaveNoParent() + { + if (ChildObjectComponent.HasSubChild) + { + foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values) + { + if (instance.transform.parent != null) + { + return false; + } + } + } + return true; + } + /// /// A wait condition specific method that assures the local space coordinates /// are not impacted by NetworkTransform when parented. diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index e68b64f105..068e5d64e7 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -1,5 +1,6 @@ using System.Collections; using NUnit.Framework; +using Unity.Netcode.Components; using UnityEngine; namespace Unity.Netcode.RuntimeTests @@ -88,6 +89,215 @@ private void AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTran } } + private void UpdateTransformLocal(NetworkTransform networkTransformTestComponent) + { + networkTransformTestComponent.transform.localPosition += GetRandomVector3(0.5f, 2.0f); + var rotation = networkTransformTestComponent.transform.localRotation; + var eulerRotation = rotation.eulerAngles; + eulerRotation += GetRandomVector3(0.5f, 5.0f); + rotation.eulerAngles = eulerRotation; + networkTransformTestComponent.transform.localRotation = rotation; + } + + private void UpdateTransformWorld(NetworkTransform networkTransformTestComponent) + { + networkTransformTestComponent.transform.position += GetRandomVector3(0.5f, 2.0f); + var rotation = networkTransformTestComponent.transform.rotation; + var eulerRotation = rotation.eulerAngles; + eulerRotation += GetRandomVector3(0.5f, 5.0f); + rotation.eulerAngles = eulerRotation; + networkTransformTestComponent.transform.rotation = rotation; + } + + /// + /// This test is based on the v2.x SwitchTransformSpaceWhenParented test but only validates the ability for an owner to + /// apply parenting locally in order to help synchronizing when parenting. + /// + /// various scale values to be applied + [Test] + public void OwnerParentingTest([Values(0.5f, 1.0f, 5.0f)] float scale) + { + m_UseParentingThreshold = true; + // Get the NetworkManager that will have authority in order to spawn with the correct authority + var isServerAuthority = m_Authority == Authority.ServerAuthority; + var authorityNetworkManager = m_ServerNetworkManager; + if (!isServerAuthority) + { + authorityNetworkManager = m_ClientNetworkManagers[0]; + } + + var childAuthorityNetworkManager = m_ClientNetworkManagers[0]; + if (!isServerAuthority) + { + childAuthorityNetworkManager = m_ServerNetworkManager; + } + + // Spawn a parent and children + ChildObjectComponent.HasSubChild = true; + // Modify our prefabs for this specific test + m_ChildObject.AllowOwnerToParent = true; + m_SubChildObject.AllowOwnerToParent = true; + + + var authoritySideParent = SpawnObject(m_ParentObject.gameObject, authorityNetworkManager).GetComponent(); + var authoritySideChild = SpawnObject(m_ChildObject.gameObject, childAuthorityNetworkManager).GetComponent(); + var authoritySideSubChild = SpawnObject(m_SubChildObject.gameObject, childAuthorityNetworkManager).GetComponent(); + + // Assure all of the child object instances are spawned before proceeding to parenting + var success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned); + Assert.True(success, "Timed out waiting for all child instances to be spawned!"); + + // Get the owner instance if in client-server mode with owner authority + if (m_Authority == Authority.OwnerAuthority) + { + authoritySideParent = s_GlobalNetworkObjects[authoritySideParent.OwnerClientId][authoritySideParent.NetworkObjectId]; + authoritySideChild = s_GlobalNetworkObjects[authoritySideChild.OwnerClientId][authoritySideChild.NetworkObjectId]; + authoritySideSubChild = s_GlobalNetworkObjects[authoritySideSubChild.OwnerClientId][authoritySideSubChild.NetworkObjectId]; + } + + // Get the authority parent and child instances + m_AuthorityParentObject = NetworkTransformTestComponent.AuthorityInstance.NetworkObject; + m_AuthorityChildObject = ChildObjectComponent.AuthorityInstance.NetworkObject; + m_AuthoritySubChildObject = ChildObjectComponent.AuthoritySubInstance.NetworkObject; + + // The child NetworkTransform will use world space when world position stays and + // local space when world position does not stay when parenting. + ChildObjectComponent.AuthorityInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = m_Precision == Precision.Half; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = m_Rotation == Rotation.Quaternion; + ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = m_RotationCompression == RotationCompression.QuaternionCompress; + + // Set whether we are interpolating or not + m_AuthorityParentNetworkTransform = m_AuthorityParentObject.GetComponent(); + m_AuthorityParentNetworkTransform.Interpolate = true; + m_AuthorityChildNetworkTransform = m_AuthorityChildObject.GetComponent(); + m_AuthorityChildNetworkTransform.Interpolate = true; + m_AuthoritySubChildNetworkTransform = m_AuthoritySubChildObject.GetComponent(); + m_AuthoritySubChildNetworkTransform.Interpolate = true; + + // Apply a scale to the parent object to make sure the scale on the child is properly updated on + // non-authority instances. + var halfScale = scale * 0.5f; + m_AuthorityParentObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + m_AuthorityChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + m_AuthoritySubChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale); + + // Allow one tick for authority to update these changes + TimeTravelAdvanceTick(); + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); + + Assert.True(success, "All transform values did not match prior to parenting!"); + + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); + + Assert.True(success, "All transform values did not match prior to parenting!"); + + // Move things around while parenting and removing the parent + // Not the absolute "perfect" test, but it validates the clients all synchronize + // parenting and transform values. + for (int i = 0; i < 30; i++) + { + // Provide two network ticks for interpolation to finalize + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + + // This validates each child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients); + + // This validates each sub-child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients); + // Parent while in motion + if (i == 5) + { + // Parent the child under the parent with the current world position stays setting + Assert.True(authoritySideChild.TrySetParent(authoritySideParent.transform), $"[Child][Client-{authoritySideChild.NetworkManagerOwner.LocalClientId}] Failed to set child's parent!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllFirstLevelChildObjectInstancesHaveChild, 300); + Assert.True(success, "Timed out waiting for all instances to have parented a child!"); + } + + if (i == 10) + { + // Parent the sub-child under the child with the current world position stays setting + Assert.True(authoritySideSubChild.TrySetParent(authoritySideChild.transform), $"[Sub-Child][Client-{authoritySideSubChild.NetworkManagerOwner.LocalClientId}] Failed to set sub-child's parent!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild, 300); + Assert.True(success, "Timed out waiting for all instances to have parented a child!"); + } + + if (i == 15) + { + // Verify that a late joining client will synchronize to the parented NetworkObjects properly + CreateAndStartNewClientWithTimeTravel(); + + // Assure all of the child object instances are spawned (basically for the newly connected client) + success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned, 300); + Assert.True(success, "Timed out waiting for all child instances to be spawned!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild, 300); + Assert.True(success, "Timed out waiting for all instances to have parented a child!"); + + // This validates each child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client); + + // This validates each sub-child instance has preserved their local space values + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client); + } + + if (i == 20) + { + // Remove the parent + Assert.True(authoritySideSubChild.TryRemoveParent(), $"[Sub-Child][Client-{authoritySideSubChild.NetworkManagerOwner.LocalClientId}] Failed to set sub-child's parent!"); + + // This waits for all child instances to have the parent removed + success = WaitForConditionOrTimeOutWithTimeTravel(AllSubChildObjectInstancesHaveNoParent, 300); + Assert.True(success, "Timed out waiting for all instances remove the parent!"); + } + + if (i == 25) + { + // Parent the child under the parent with the current world position stays setting + Assert.True(authoritySideChild.TryRemoveParent(), $"[Child][Client-{authoritySideChild.NetworkManagerOwner.LocalClientId}] Failed to remove parent!"); + + // This waits for all child instances to be parented + success = WaitForConditionOrTimeOutWithTimeTravel(AllFirstLevelChildObjectInstancesHaveNoParent, 300); + Assert.True(success, "Timed out waiting for all instances remove the parent!"); + } + UpdateTransformWorld(m_AuthorityParentNetworkTransform); + if (m_AuthorityChildNetworkTransform.InLocalSpace) + { + UpdateTransformLocal(m_AuthorityChildNetworkTransform); + } + else + { + UpdateTransformWorld(m_AuthorityChildNetworkTransform); + } + + if (m_AuthoritySubChildNetworkTransform.InLocalSpace) + { + UpdateTransformLocal(m_AuthoritySubChildNetworkTransform); + } + else + { + UpdateTransformWorld(m_AuthoritySubChildNetworkTransform); + } + } + + success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches, 300); + + Assert.True(success, "All transform values did not match prior to parenting!"); + + // Revert the modifications made for this specific test + m_ChildObject.AllowOwnerToParent = false; + m_SubChildObject.AllowOwnerToParent = false; + } + /// /// Validates that transform values remain the same when a NetworkTransform is /// parented under another NetworkTransform under all of the possible axial conditions