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