From 352d63c6f57c4bd84d6401116b249ffcbff0031d Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 29 May 2025 09:47:54 -0500 Subject: [PATCH 1/8] fix Allow non-authority instances to adjust non-synchronized axis on NetworkTransform. --- .../Runtime/Components/NetworkTransform.cs | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 05eeb22cfe..ea54605637 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -2650,7 +2650,18 @@ protected internal void ApplyAuthoritativeState() // Update our current position if it changed or we are interpolating if (networkState.HasPositionChange || Interpolate) { - m_InternalCurrentPosition = adjustedPosition; + if (SyncPositionX && SyncPositionY && SyncPositionZ) + { + m_InternalCurrentPosition = adjustedPosition; + } + else + { + // Preserve any non-synchronized changes to the local instance's position + var position = InLocalSpace ? transform.localPosition : transform.position; + m_InternalCurrentPosition.x = SyncPositionX ? adjustedPosition.x : position.x; + m_InternalCurrentPosition.y = SyncPositionY ? adjustedPosition.y : position.y; + m_InternalCurrentPosition.z = SyncPositionZ ? adjustedPosition.z : position.z; + } } #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D if (m_UseRigidbodyForMotion) @@ -2696,7 +2707,21 @@ protected internal void ApplyAuthoritativeState() // Update our current rotation if it changed or we are interpolating if (networkState.HasRotAngleChange || Interpolate) { - m_InternalCurrentRotation = adjustedRotation; + if ((SyncRotAngleX && SyncRotAngleY && SyncRotAngleZ) || UseQuaternionSynchronization) + { + m_InternalCurrentRotation = adjustedRotation; + } + else + { + // Preserve any non-synchronized changes to the local instance's rotation + var rotation = InLocalSpace ? transform.localRotation.eulerAngles : transform.rotation.eulerAngles; + var currentEuler = m_InternalCurrentRotation.eulerAngles; + var updatedEuler = adjustedRotation.eulerAngles; + currentEuler.x = SyncRotAngleX ? updatedEuler.x : rotation.x; + currentEuler.y = SyncRotAngleY ? updatedEuler.y : rotation.y; + currentEuler.z = SyncRotAngleZ ? updatedEuler.z : rotation.z; + m_InternalCurrentRotation.eulerAngles = currentEuler; + } } #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D @@ -2737,7 +2762,18 @@ protected internal void ApplyAuthoritativeState() // Update our current scale if it changed or we are interpolating if (networkState.HasScaleChange || Interpolate) { - m_InternalCurrentScale = adjustedScale; + if (SyncScaleX && SyncScaleY && SyncScaleZ) + { + m_InternalCurrentScale = adjustedScale; + } + else + { + // Preserve any non-synchronized changes to the local instance's scale + var scale = transform.localScale; + m_InternalCurrentScale.x = SyncScaleX ? adjustedScale.x : scale.x; + m_InternalCurrentScale.y = SyncScaleY ? adjustedScale.y : scale.y; + m_InternalCurrentScale.z = SyncScaleZ ? adjustedScale.z : scale.z; + } } transform.localScale = m_InternalCurrentScale; } From f9226dbf687c3e29fc003724f9906756541a39c9 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 29 May 2025 17:29:30 -0500 Subject: [PATCH 2/8] update work in progress. potential fix for NetworkTransform non-authority's ability to modify portions of the transform not being synchronized. --- .../Runtime/Components/NetworkTransform.cs | 17 +- .../NetworkTransformNonAuthorityTests.cs | 466 ++++++++++++++++++ .../NetworkTransformNonAuthorityTests.cs.meta | 2 + 3 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index ea54605637..faf5d79b29 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -2524,10 +2524,25 @@ protected internal void ApplyAuthoritativeState() // at the end of this method and assure that when not interpolating the non-authoritative side // cannot make adjustments to any portions the transform not being synchronized. var adjustedPosition = m_InternalCurrentPosition; - var adjustedRotation = m_InternalCurrentRotation; + var currentPosistion = GetSpaceRelativePosition(); + adjustedPosition.x = SyncPositionX ? m_InternalCurrentPosition.x : currentPosistion.x; + adjustedPosition.y = SyncPositionY ? m_InternalCurrentPosition.y : currentPosistion.y; + adjustedPosition.z = SyncPositionZ ? m_InternalCurrentPosition.z : currentPosistion.z; + var adjustedRotation = m_InternalCurrentRotation; var adjustedRotAngles = adjustedRotation.eulerAngles; + var currentRotation = GetSpaceRelativeRotation().eulerAngles; + adjustedRotAngles.x = SyncRotAngleX ? adjustedRotAngles.x : currentRotation.x; + adjustedRotAngles.y = SyncRotAngleY ? adjustedRotAngles.y : currentRotation.y; + adjustedRotAngles.z = SyncRotAngleZ ? adjustedRotAngles.z : currentRotation.z; + adjustedRotation.eulerAngles = adjustedRotAngles; + + var adjustedScale = m_InternalCurrentScale; + var currentScale = GetScale(); + adjustedScale.x = SyncScaleX ? adjustedScale.x : currentScale.x; + adjustedScale.y = SyncScaleY ? adjustedScale.y : currentScale.y; + adjustedScale.z = SyncScaleZ ? adjustedScale.z : currentScale.z; // Non-Authority Preservers the authority's transform state update modes InLocalSpace = networkState.InLocalSpace; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs new file mode 100644 index 0000000000..8c5343fe5f --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs @@ -0,0 +1,466 @@ +using NUnit.Framework; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.DAHost)] + internal class NetworkTransformNonAuthorityTests : IntegrationTestWithApproximation + { + private const int k_NumberOfPasses = 3; + protected override int NumberOfClients => 2; + + private StringBuilder m_ErrorMsg = new StringBuilder(); + + private GameObject m_PrefabToSpawn; + + private NetworkObject m_AuthorityInstance; + + public NetworkTransformNonAuthorityTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + public class NetworkTransformTestComponent : NetworkTransform, INetworkUpdateSystem + { + public static NetworkTransformTestComponent AuthorityInstance { get; private set; } + public static readonly List AllInstances = new List(); + + public void SetDirValues(Vector3 positionMotion = default, Vector3 rotationMotion = default, + Vector3 scaleMotion = default, bool shouldMove = false) + { + m_UpdateNonSynchronizedAxis = shouldMove; + if (m_UpdateNonSynchronizedAxis) + { + m_PositionDir = GetSynchronizedPosition(positionMotion); + m_RotationDir = GetSynchronizedRotation(rotationMotion); + m_ScaleDir = GetSynchronizedScale(scaleMotion); + m_TargetPosition = GetSynchronizedPosition(m_PositionDir + transform.position); + var quat = Quaternion.identity; + quat = transform.rotation; + quat.eulerAngles = GetSynchronizedRotation(m_RotationDir + transform.rotation.eulerAngles); + m_TargetRotation = quat.eulerAngles; + m_TargetScale = GetSynchronizedScale(m_ScaleDir + transform.localScale); + NonSynchronizedPositionReached = false; + NonSynchronizedRotationReached = false; + NonSynchronizedScaleReached = false; + + m_PosMag = 1.0f / m_TargetPosition.magnitude; + m_RotMag = 1.0f / m_TargetRotation.magnitude; + m_ScaleMag = 1.0f / m_TargetScale.magnitude; + } + } + + private float m_PosMag; + private float m_RotMag; + private float m_ScaleMag; + + public Vector3 MovePosition(Vector3 position) + { + if (!CanCommitToTransform) + { + return Vector3.zero; + } + + transform.position += GetSynchronizedPosition(position, false); + return transform.position; + } + + public Vector3 MoveRotation(Vector3 eulerAngles) + { + if (!CanCommitToTransform) + { + return Vector3.zero; + } + var rotation = transform.rotation; + rotation.eulerAngles += GetSynchronizedRotation(eulerAngles, false); + transform.rotation = rotation; + return rotation.eulerAngles; + } + + public Vector3 MoveScale(Vector3 scale) + { + if (!CanCommitToTransform) + { + return Vector3.zero; + } + + transform.localScale += GetSynchronizedScale(scale, false); + return transform.localScale; + } + + public bool NonSynchronizedPositionReached { get; private set; } + public bool NonSynchronizedRotationReached { get; private set; } + public bool NonSynchronizedScaleReached { get; private set; } + + private bool m_UpdateNonSynchronizedAxis; + private Vector3 m_PositionDir; + private Vector3 m_RotationDir; + private Vector3 m_ScaleDir; + private Vector3 m_TargetPosition; + private Vector3 m_TargetRotation; + private Vector3 m_TargetScale; + + + public override void OnNetworkSpawn() + { + base.OnNetworkSpawn(); + + if (CanCommitToTransform) + { + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); + AuthorityInstance = this; + } + AllInstances.Add(this); + } + + public override void OnNetworkDespawn() + { + NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.Update); + base.OnNetworkDespawn(); + } + + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + MoveObjectLocally(); + } + + private Vector3 GetSynchronizedPosition(Vector3 position, bool invert = true) + { + if (invert) + { + position.x *= !SyncPositionX ? 1 : 0; + position.y *= !SyncPositionY ? 1 : 0; + position.z *= !SyncPositionZ ? 1 : 0; + } + else + { + position.x *= SyncPositionX ? 1 : 0; + position.y *= SyncPositionY ? 1 : 0; + position.z *= SyncPositionZ ? 1 : 0; + } + return position; + } + + private Vector3 GetSynchronizedRotation(Vector3 rotation, bool invert = true) + { + if (invert) + { + rotation.x *= !SyncRotAngleX ? 1 : 0; + rotation.y *= !SyncRotAngleY ? 1 : 0; + rotation.z *= !SyncRotAngleZ ? 1 : 0; + } + else + { + rotation.x *= SyncRotAngleX ? 1 : 0; + rotation.y *= SyncRotAngleY ? 1 : 0; + rotation.z *= SyncRotAngleZ ? 1 : 0; + } + return rotation; + } + + private Vector3 GetSynchronizedScale(Vector3 scale, bool invert = true) + { + if (invert) + { + scale.x *= !SyncScaleX ? 1 : 0; + scale.y *= !SyncScaleY ? 1 : 0; + scale.z *= !SyncScaleZ ? 1 : 0; + } + else + { + scale.x *= SyncScaleX ? 1 : 0; + scale.y *= SyncScaleY ? 1 : 0; + scale.z *= SyncScaleZ ? 1 : 0; + } + return scale; + } + + public bool HasCompletedMotion() + { + return NonSynchronizedPositionReached && NonSynchronizedRotationReached && NonSynchronizedScaleReached; + } + + public void GetUnSynchronizedTargetInfo(StringBuilder builder) + { + if (!NonSynchronizedPositionReached) + { + builder.Append($"[Position] Current: {GetSynchronizedPosition(transform.position)} | Target: {m_TargetPosition}"); + } + if (!NonSynchronizedRotationReached) + { + builder.Append($"[Rotation] Current: {GetSynchronizedRotation(transform.rotation.eulerAngles)} | Target: {m_TargetRotation}"); + } + if (!NonSynchronizedScaleReached) + { + builder.Append($"[Scale] Current: {GetSynchronizedScale(transform.localScale)} | Target: {m_TargetScale}"); + } + + builder.Append("\n"); + } + + private void MoveObjectLocally() + { + if (!m_UpdateNonSynchronizedAxis || HasCompletedMotion()) + { + return; + } + if (!NonSynchronizedPositionReached) + { + var lerpAmount = Mathf.Clamp(1.0f - (Vector3.Distance(transform.position, m_TargetPosition) * m_PosMag), 0.25f, 1.0f); + transform.position = Vector3.Lerp(transform.position, m_TargetPosition, lerpAmount); + NonSynchronizedPositionReached = Approximately(GetSynchronizedPosition(transform.position), GetSynchronizedPosition(m_TargetPosition)); + } + + if (!NonSynchronizedRotationReached) + { + var rotation = transform.rotation; + var eulerRotation = rotation.eulerAngles; + var lerpAmount = Mathf.Clamp(1.0f - (Vector3.Distance(eulerRotation, m_TargetRotation) * m_RotMag), 0.25f, 1.0f); + eulerRotation = Vector3.Lerp(eulerRotation, m_TargetRotation, lerpAmount); + rotation.eulerAngles = eulerRotation; + transform.rotation = rotation; + NonSynchronizedRotationReached = Approximately(GetSynchronizedRotation(transform.rotation.eulerAngles), m_TargetRotation); + } + + if (!NonSynchronizedScaleReached) + { + var lerpFactor = Vector3.Distance(transform.localScale, m_TargetScale); + var lerpAmount = Mathf.Clamp(1.0f - (Vector3.Distance(transform.localScale, m_TargetScale) * m_ScaleMag), 0.25f, 1.0f); + transform.localScale = Vector3.Lerp(transform.localScale, m_TargetScale, lerpAmount); + NonSynchronizedScaleReached = Approximately(GetSynchronizedScale(transform.localScale), m_TargetScale); + } + } + + public override void OnUpdate() + { + base.OnUpdate(); + + MoveObjectLocally(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool Approximately(Vector3 a, Vector3 b) + { + var deltaVariance = 0.01f; + return System.Math.Round(Mathf.Abs(a.x - b.x), 2) <= deltaVariance && + System.Math.Round(Mathf.Abs(a.y - b.y), 2) <= deltaVariance && + System.Math.Round(Mathf.Abs(a.z - b.z), 2) <= deltaVariance; + } + } + + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab("TestObject"); + var networkTransform = m_PrefabToSpawn.AddComponent(); + networkTransform.SyncPositionX = true; + networkTransform.SyncPositionY = false; + networkTransform.SyncPositionZ = true; + + networkTransform.SyncRotAngleX = true; + networkTransform.SyncRotAngleY = false; + networkTransform.SyncRotAngleZ = false; + + networkTransform.SyncScaleX = false; + networkTransform.SyncScaleY = true; + networkTransform.SyncScaleZ = false; + + base.OnServerAndClientsCreated(); + } + + private bool AllTransformsAreApproximatelyTheSame() + { + m_ErrorMsg.Clear(); + var authorityInstance = NetworkTransformTestComponent.AuthorityInstance; + + foreach (var instance in NetworkTransformTestComponent.AllInstances) + { + if (instance == authorityInstance) + { + continue; + } + if (!Approximately(instance.transform.position, authorityInstance.transform.position)) + { + m_ErrorMsg.AppendLine($"[{instance.name}] Position ({instance.transform.position}) is not " + + $"equal to authority's ({authorityInstance.transform.position})! "); + } + if (!Approximately(instance.transform.rotation, authorityInstance.transform.rotation)) + { + m_ErrorMsg.AppendLine($"[{instance.name}] Rotation ({instance.transform.rotation.eulerAngles}) is not " + + $"equal to authority's ({authorityInstance.transform.rotation.eulerAngles})! "); + } + if (!Approximately(instance.transform.localScale, authorityInstance.transform.localScale)) + { + m_ErrorMsg.AppendLine($"[{instance.name}] Scale ({instance.transform.localScale}) is not " + + $"equal to authority's ({authorityInstance.transform.localScale})! "); + } + } + return m_ErrorMsg.Length == 0; + } + + private bool AllNonSynchronizedMotionCompleted() + { + m_ErrorMsg.Clear(); + foreach (var instance in NetworkTransformTestComponent.AllInstances) + { + if (!instance.HasCompletedMotion()) + { + m_ErrorMsg.Append($"[{instance.name}] Has not completed local motion!\n"); + instance.GetUnSynchronizedTargetInfo(m_ErrorMsg); + } + } + return m_ErrorMsg.Length == 0; + } + + private bool AllClientsSpawnedObject() + { + foreach(var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObjectId)) + { + return false; + } + } + return true; + } + + [UnityTest] + public IEnumerator NonAuthorityUpdateNonSynchronizedAxis() + { + var authority = GetNonAuthorityNetworkManager(); + m_AuthorityInstance = SpawnObject(m_PrefabToSpawn, authority).GetComponent(); + yield return WaitForConditionOrTimeOut(AllClientsSpawnedObject); + AssertOnTimeout($"All clients did not spawn {m_AuthorityInstance.name}!"); + + for(int i = 0; i < k_NumberOfPasses; i++) + { + var positionDelta = GetRandomVector3(-4, 4); + var rotationDelta = GetRandomVector3(-20, 20); + var scaleDelta = GetRandomVector3(-2, 2); + + var movePosition = NetworkTransformTestComponent.AuthorityInstance.MovePosition(GetRandomVector3(-4, 4)); + var moveRotation = NetworkTransformTestComponent.AuthorityInstance.MoveRotation(GetRandomVector3(-20, 20)); + var moveScale = NetworkTransformTestComponent.AuthorityInstance.MoveScale(GetRandomVector3(-2, 2)); + + foreach (var testTransform in NetworkTransformTestComponent.AllInstances) + { + testTransform.SetDirValues(positionDelta, rotationDelta, scaleDelta, true); + } + + // Wait for all instances to finish their local controlled changes + yield return WaitForConditionOrTimeOut(AllNonSynchronizedMotionCompleted); + AssertOnTimeout($"[Iteration: {i}] Not all instances completed local motion! {m_ErrorMsg}"); + + // Wait for all instances' transforms to match + yield return WaitForConditionOrTimeOut(AllTransformsAreApproximatelyTheSame); + AssertOnTimeout($"[Iteration: {i}] Not all instances' transforms match! {m_ErrorMsg}"); + + var builder = new StringBuilder(); + builder.AppendLine($"Final Expected Position: {movePosition + positionDelta}"); + foreach (var testTransform in NetworkTransformTestComponent.AllInstances) + { + builder.AppendLine($"[Client-{testTransform.NetworkManager.LocalClientId}] Position: {testTransform.transform.position}"); + } + Debug.Log(builder.ToString()); + } + } + + private GameObject m_OwnershipObject; + private NetworkObject m_OwnershipNetworkObject; + private bool AllObjectsSpawnedOnClients() + { + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_OwnershipNetworkObject.NetworkObjectId)) + { + return false; + } + } + return true; + } + + private bool ObjectHiddenOnNonAuthorityClients() + { + foreach (var networkManager in m_NetworkManagers) + { + if (networkManager.LocalClientId == m_OwnershipNetworkObject.OwnerClientId) + { + continue; + } + if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_OwnershipNetworkObject.NetworkObjectId)) + { + return false; + } + } + return true; + } + + [UnityTest] + public IEnumerator NetworkShowWithChangeOwnershipTest() + { + var authority = GetAuthorityNetworkManager(); + + m_OwnershipObject = SpawnObject(m_PrefabToSpawn, authority); + m_OwnershipNetworkObject = m_OwnershipObject.GetComponent(); + + yield return WaitForConditionOrTimeOut(AllObjectsSpawnedOnClients); + AssertOnTimeout("Timed out waiting for all clients to spawn the ownership object!"); + + VerboseDebug($"Hiding object {m_OwnershipNetworkObject.NetworkObjectId} on all clients"); + foreach (var client in m_NetworkManagers) + { + if (client == authority) + { + continue; + } + m_OwnershipNetworkObject.NetworkHide(client.LocalClientId); + } + + yield return WaitForConditionOrTimeOut(ObjectHiddenOnNonAuthorityClients); + AssertOnTimeout("Timed out waiting for all clients to hide the ownership object!"); + + m_NewOwner = GetNonAuthorityNetworkManager(); + Assert.AreNotEqual(m_OwnershipNetworkObject.OwnerClientId, m_NewOwner.LocalClientId, $"Client-{m_NewOwner.LocalClientId} should not have ownership of object {m_OwnershipNetworkObject.NetworkObjectId}!"); + Assert.False(m_NewOwner.SpawnManager.SpawnedObjects.ContainsKey(m_OwnershipNetworkObject.NetworkObjectId), $"Client-{m_NewOwner.LocalClientId} should not have object {m_OwnershipNetworkObject.NetworkObjectId} spawned!"); + + // Run NetworkShow and ChangeOwnership directly after one-another + VerboseDebug($"Calling {nameof(NetworkObject.NetworkShow)} on object {m_OwnershipNetworkObject.NetworkObjectId} for client {m_NewOwner.LocalClientId}"); + m_OwnershipNetworkObject.NetworkShow(m_NewOwner.LocalClientId); + VerboseDebug($"Calling {nameof(NetworkObject.ChangeOwnership)} on object {m_OwnershipNetworkObject.NetworkObjectId} for client {m_NewOwner.LocalClientId}"); + m_OwnershipNetworkObject.ChangeOwnership(m_NewOwner.LocalClientId); + m_ObjectId = m_OwnershipNetworkObject.NetworkObjectId; + yield return WaitForConditionOrTimeOut(OwnershipHasChanged); + AssertOnTimeout($"Timed out waiting for clients-{m_NewOwner.LocalClientId} to gain ownership of object {m_OwnershipNetworkObject.NetworkObjectId}!"); + VerboseDebug($"Client {m_NewOwner.LocalClientId} now owns object {m_OwnershipNetworkObject.NetworkObjectId}!"); + } + + private NetworkManager m_NewOwner; + + private bool OwnershipHasChanged() + { + if (!m_NewOwner.SpawnManager.SpawnedObjects.ContainsKey(m_ObjectId)) + { + return false; + } + return m_NewOwner.SpawnManager.SpawnedObjects[m_ObjectId].OwnerClientId == m_NewOwner.LocalClientId; + } + + private ulong m_ObjectId; + private bool ObjectDespawned() + { + foreach (var networkManager in m_NetworkManagers) + { + if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_ObjectId)) + { + return false; + } + } + return true; + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs.meta new file mode 100644 index 0000000000..8c780a2920 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b917fea2cf5d9214ebc068f2f12a1e4f \ No newline at end of file From 9f53d28abf3738f4c8e1e937cb762f7ef58db290 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Fri, 30 May 2025 15:23:49 -0500 Subject: [PATCH 3/8] test Adding the NetworkTransformNonAuthorityTests to validate the fixes in this PR. --- .../NetworkTransformNonAuthorityTests.cs | 622 +++++++++++------- 1 file changed, 373 insertions(+), 249 deletions(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs index 8c5343fe5f..9632e098a1 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs @@ -16,6 +16,7 @@ namespace Unity.Netcode.RuntimeTests internal class NetworkTransformNonAuthorityTests : IntegrationTestWithApproximation { private const int k_NumberOfPasses = 3; + private const float k_LerpTime = 0.1f; protected override int NumberOfClients => 2; private StringBuilder m_ErrorMsg = new StringBuilder(); @@ -26,40 +27,160 @@ internal class NetworkTransformNonAuthorityTests : IntegrationTestWithApproximat public NetworkTransformNonAuthorityTests(HostOrServer hostOrServer) : base(hostOrServer) { } + /// + /// The NetworkTransform testing component used for this test + /// public class NetworkTransformTestComponent : NetworkTransform, INetworkUpdateSystem { - public static NetworkTransformTestComponent AuthorityInstance { get; private set; } + public static NetworkTransformTestComponent AuthorityInstance; public static readonly List AllInstances = new List(); - public void SetDirValues(Vector3 positionMotion = default, Vector3 rotationMotion = default, - Vector3 scaleMotion = default, bool shouldMove = false) + public static bool VerboseDebug; + + public static void Reset() + { + AllInstances.Clear(); + } + + /// + /// All of the below bools are set when the non-synchronized axis + /// have reached their target values. + /// + public bool NonSynchronizedPositionReached { get; private set; } + public bool NonSynchronizedRotationReached { get; private set; } + public bool NonSynchronizedScaleReached { get; private set; } + + private bool m_UpdateNonSynchronizedAxis; + private float m_StartMotionTime; + private float m_Lerp; + + /// + /// The below properties are used to + /// lerp from the current non-synchronized axis values to + /// the target non-synchronized axis values. + /// + private Vector3 m_OriginalPosition; + private Vector3 m_OriginalRotation; + private Vector3 m_OriginalScale; + + /// + /// The below properties are the + /// target non-synchronized axis values. + /// + private Vector3 m_TargetPosition; + private Vector3 m_TargetRotation; + private Vector3 m_TargetScale; + + private void Log(string msg) + { + if (!VerboseDebug) + { + return; + } + Debug.Log(msg); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool Approximately(Vector3 a, Vector3 b, float deltaVariance = 0.0001f) + { + return System.Math.Round(Mathf.Abs(a.x - b.x), 4) <= deltaVariance && + System.Math.Round(Mathf.Abs(a.y - b.y), 4) <= deltaVariance && + System.Math.Round(Mathf.Abs(a.z - b.z), 4) <= deltaVariance; + } + + /// + /// For debugging + /// + public void GetUnSynchronizedTargetInfo(StringBuilder builder) + { + if (!NonSynchronizedPositionReached) + { + builder.Append($"[Position] Current: {GetNonSynchronizedPosition(transform.position)} | Target: {m_TargetPosition}"); + } + if (!NonSynchronizedRotationReached) + { + builder.Append($"[Rotation] Current: {GetNonSynchronizedRotation(transform.rotation.eulerAngles)} | Target: {m_TargetRotation}"); + } + if (!NonSynchronizedScaleReached) + { + builder.Append($"[Scale] Current: {GetNonSynchronizedScale(transform.localScale)} | Target: {m_TargetScale}"); + } + + builder.Append("\n"); + } + + public void ShouldMove(bool shouldMove = false) { m_UpdateNonSynchronizedAxis = shouldMove; if (m_UpdateNonSynchronizedAxis) { - m_PositionDir = GetSynchronizedPosition(positionMotion); - m_RotationDir = GetSynchronizedRotation(rotationMotion); - m_ScaleDir = GetSynchronizedScale(scaleMotion); - m_TargetPosition = GetSynchronizedPosition(m_PositionDir + transform.position); - var quat = Quaternion.identity; - quat = transform.rotation; - quat.eulerAngles = GetSynchronizedRotation(m_RotationDir + transform.rotation.eulerAngles); - m_TargetRotation = quat.eulerAngles; - m_TargetScale = GetSynchronizedScale(m_ScaleDir + transform.localScale); - NonSynchronizedPositionReached = false; - NonSynchronizedRotationReached = false; - NonSynchronizedScaleReached = false; - - m_PosMag = 1.0f / m_TargetPosition.magnitude; - m_RotMag = 1.0f / m_TargetRotation.magnitude; - m_ScaleMag = 1.0f / m_TargetScale.magnitude; + m_StartMotionTime = Time.realtimeSinceStartup; + m_Lerp = 0.0f; } } - private float m_PosMag; - private float m_RotMag; - private float m_ScaleMag; + public bool HasCompletedMotion() + { + return NonSynchronizedPositionReached && NonSynchronizedRotationReached && NonSynchronizedScaleReached; + } + #region Generate Random Non-Synchronized Axis Values + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float GenerateRandom(float range) + { + var random = Random.Range(-range, range); + var negMult = random < 0 ? -1 : 1; + random = Mathf.Clamp(Mathf.Abs(random), range * 0.10f, range) * negMult; + return random; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 SetRandomNonSynchPosition(float range) + { + SetNonSynchPositionTarget(GetNonSynchronizedPosition(Vector3.one) * GenerateRandom(range)); + return m_TargetPosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetNonSynchPositionTarget(Vector3 target) + { + m_OriginalPosition = GetNonSynchronizedPosition(transform.position); + m_TargetPosition = GetNonSynchronizedPosition(target); + NonSynchronizedPositionReached = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 SetRandomNonSynchRotation(float range) + { + m_OriginalRotation = GetNonSynchronizedRotation(transform.rotation.eulerAngles); + SetNonSynchRotationTarget(GetNonSynchronizedRotation(Vector3.one) * GenerateRandom(range)); + return m_TargetRotation; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetNonSynchRotationTarget(Vector3 target) + { + m_TargetRotation = GetNonSynchronizedRotation(target); + NonSynchronizedRotationReached = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 SetRandomNonSynchScale(float range) + { + SetNonSynchScaleTarget(GetNonSynchronizedScale(Vector3.one) * GenerateRandom(range)); + return m_TargetScale; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetNonSynchScaleTarget(Vector3 target) + { + m_OriginalScale = GetNonSynchronizedScale(transform.localScale); + m_TargetScale = target; + NonSynchronizedScaleReached = false; + } + #endregion + + #region Update Synchronized Axis Values public Vector3 MovePosition(Vector3 position) { if (!CanCommitToTransform) @@ -67,7 +188,7 @@ public Vector3 MovePosition(Vector3 position) return Vector3.zero; } - transform.position += GetSynchronizedPosition(position, false); + transform.position += GetSynchronizedPosition(position); return transform.position; } @@ -78,7 +199,7 @@ public Vector3 MoveRotation(Vector3 eulerAngles) return Vector3.zero; } var rotation = transform.rotation; - rotation.eulerAngles += GetSynchronizedRotation(eulerAngles, false); + rotation.eulerAngles += GetSynchronizedRotation(eulerAngles); transform.rotation = rotation; return rotation.eulerAngles; } @@ -90,197 +211,253 @@ public Vector3 MoveScale(Vector3 scale) return Vector3.zero; } - transform.localScale += GetSynchronizedScale(scale, false); + transform.localScale += GetSynchronizedScale(scale); return transform.localScale; } + #endregion - public bool NonSynchronizedPositionReached { get; private set; } - public bool NonSynchronizedRotationReached { get; private set; } - public bool NonSynchronizedScaleReached { get; private set; } - - private bool m_UpdateNonSynchronizedAxis; - private Vector3 m_PositionDir; - private Vector3 m_RotationDir; - private Vector3 m_ScaleDir; - private Vector3 m_TargetPosition; - private Vector3 m_TargetRotation; - private Vector3 m_TargetScale; - - - public override void OnNetworkSpawn() + #region Methods to Get Synchronized and Non-Synchronized Values + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 GetSynchronizedPosition(Vector3 position) { - base.OnNetworkSpawn(); + position.x *= SyncPositionX ? 1 : 0; + position.y *= SyncPositionY ? 1 : 0; + position.z *= SyncPositionZ ? 1 : 0; + return position; + } - if (CanCommitToTransform) - { - NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); - AuthorityInstance = this; - } - AllInstances.Add(this); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 GetNonSynchronizedPosition(Vector3 position) + { + position.x *= !SyncPositionX ? 1 : 0; + position.y *= !SyncPositionY ? 1 : 0; + position.z *= !SyncPositionZ ? 1 : 0; + return position; } - public override void OnNetworkDespawn() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 GetSynchronizedRotation(Vector3 rotation) { - NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.Update); - base.OnNetworkDespawn(); + rotation.x *= SyncRotAngleX ? 1 : 0; + rotation.y *= SyncRotAngleY ? 1 : 0; + rotation.z *= SyncRotAngleZ ? 1 : 0; + return rotation; } - public void NetworkUpdate(NetworkUpdateStage updateStage) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 GetNonSynchronizedRotation(Vector3 rotation) { - MoveObjectLocally(); + + rotation.x *= !SyncRotAngleX ? 1 : 0; + rotation.y *= !SyncRotAngleY ? 1 : 0; + rotation.z *= !SyncRotAngleZ ? 1 : 0; + + return rotation; } - private Vector3 GetSynchronizedPosition(Vector3 position, bool invert = true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 GetSynchronizedScale(Vector3 scale) { - if (invert) - { - position.x *= !SyncPositionX ? 1 : 0; - position.y *= !SyncPositionY ? 1 : 0; - position.z *= !SyncPositionZ ? 1 : 0; - } - else - { - position.x *= SyncPositionX ? 1 : 0; - position.y *= SyncPositionY ? 1 : 0; - position.z *= SyncPositionZ ? 1 : 0; - } - return position; + scale.x *= SyncScaleX ? 1 : 0; + scale.y *= SyncScaleY ? 1 : 0; + scale.z *= SyncScaleZ ? 1 : 0; + return scale; } - private Vector3 GetSynchronizedRotation(Vector3 rotation, bool invert = true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector3 GetNonSynchronizedScale(Vector3 scale, bool invert = false) { - if (invert) - { - rotation.x *= !SyncRotAngleX ? 1 : 0; - rotation.y *= !SyncRotAngleY ? 1 : 0; - rotation.z *= !SyncRotAngleZ ? 1 : 0; - } - else - { - rotation.x *= SyncRotAngleX ? 1 : 0; - rotation.y *= SyncRotAngleY ? 1 : 0; - rotation.z *= SyncRotAngleZ ? 1 : 0; - } - return rotation; + scale.x *= !SyncScaleX ? 1 : 0; + scale.y *= !SyncScaleY ? 1 : 0; + scale.z *= !SyncScaleZ ? 1 : 0; + return scale; } + #endregion - private Vector3 GetSynchronizedScale(Vector3 scale, bool invert = true) + #region Spawn, Despawn, and Update Methods + public override void OnNetworkSpawn() { - if (invert) - { - scale.x *= !SyncScaleX ? 1 : 0; - scale.y *= !SyncScaleY ? 1 : 0; - scale.z *= !SyncScaleZ ? 1 : 0; - } - else + base.OnNetworkSpawn(); + + if (CanCommitToTransform) { - scale.x *= SyncScaleX ? 1 : 0; - scale.y *= SyncScaleY ? 1 : 0; - scale.z *= SyncScaleZ ? 1 : 0; + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate); } - return scale; + AllInstances.Add(this); } - public bool HasCompletedMotion() + public override void OnNetworkDespawn() { - return NonSynchronizedPositionReached && NonSynchronizedRotationReached && NonSynchronizedScaleReached; + NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate); + base.OnNetworkDespawn(); } - public void GetUnSynchronizedTargetInfo(StringBuilder builder) + public void NetworkUpdate(NetworkUpdateStage updateStage) { - if (!NonSynchronizedPositionReached) - { - builder.Append($"[Position] Current: {GetSynchronizedPosition(transform.position)} | Target: {m_TargetPosition}"); - } - if (!NonSynchronizedRotationReached) - { - builder.Append($"[Rotation] Current: {GetSynchronizedRotation(transform.rotation.eulerAngles)} | Target: {m_TargetRotation}"); - } - if (!NonSynchronizedScaleReached) + if (updateStage == NetworkUpdateStage.PreUpdate) { - builder.Append($"[Scale] Current: {GetSynchronizedScale(transform.localScale)} | Target: {m_TargetScale}"); + UpdateNonSynchronizedAxis(); } + } - builder.Append("\n"); + public override void OnUpdate() + { + UpdateNonSynchronizedAxis(); + base.OnUpdate(); } - private void MoveObjectLocally() + /// + /// Updates the non-synchronized axis values + /// + private void UpdateNonSynchronizedAxis() { if (!m_UpdateNonSynchronizedAxis || HasCompletedMotion()) { return; } + + // Calculate the lerp factor based on when we started motion vs the time to + // finish lerping. + var lerpComplete = m_Lerp >= 1.0f; + if (!lerpComplete) + { + var deltaTime = Time.realtimeSinceStartup - m_StartMotionTime; + if (deltaTime > 0.0f) + { + m_Lerp = Mathf.Clamp(deltaTime / k_LerpTime, 0.001f, 1.0f); + } + } + + // Handle non-synchronized position axis updates if (!NonSynchronizedPositionReached) { - var lerpAmount = Mathf.Clamp(1.0f - (Vector3.Distance(transform.position, m_TargetPosition) * m_PosMag), 0.25f, 1.0f); - transform.position = Vector3.Lerp(transform.position, m_TargetPosition, lerpAmount); - NonSynchronizedPositionReached = Approximately(GetSynchronizedPosition(transform.position), GetSynchronizedPosition(m_TargetPosition)); + m_OriginalPosition = Vector3.Lerp(m_OriginalPosition, m_TargetPosition, m_Lerp); + // Get the sum of the synchronized value with the lerped un-synchronized value and apply the new position. + transform.position = GetSynchronizedPosition(transform.position) + m_OriginalPosition; + NonSynchronizedPositionReached = Approximately(m_OriginalPosition, m_TargetPosition); + if (NonSynchronizedPositionReached || lerpComplete) + { + Log($"[{name}][Position] Current: {transform.position} | Current-NonSync: {GetNonSynchronizedPosition(transform.position)} | Original: {m_OriginalPosition} Target: {m_TargetPosition}"); + } } + // Handle non-synchronized rotation axis updates if (!NonSynchronizedRotationReached) { var rotation = transform.rotation; - var eulerRotation = rotation.eulerAngles; - var lerpAmount = Mathf.Clamp(1.0f - (Vector3.Distance(eulerRotation, m_TargetRotation) * m_RotMag), 0.25f, 1.0f); - eulerRotation = Vector3.Lerp(eulerRotation, m_TargetRotation, lerpAmount); - rotation.eulerAngles = eulerRotation; + m_OriginalRotation = Vector3.Lerp(m_OriginalRotation, m_TargetRotation, m_Lerp); + rotation.eulerAngles = GetSynchronizedRotation(rotation.eulerAngles) + m_OriginalRotation; transform.rotation = rotation; - NonSynchronizedRotationReached = Approximately(GetSynchronizedRotation(transform.rotation.eulerAngles), m_TargetRotation); + NonSynchronizedRotationReached = Approximately(m_OriginalRotation, m_TargetRotation); + if (NonSynchronizedRotationReached || lerpComplete) + { + Log($"[{name}][Rotation] Current: {transform.rotation.eulerAngles} | Current-NonSync: {GetNonSynchronizedRotation(transform.rotation.eulerAngles)} | Target: {m_TargetRotation}"); + } } + // Handle non-synchronized scale axis updates if (!NonSynchronizedScaleReached) { - var lerpFactor = Vector3.Distance(transform.localScale, m_TargetScale); - var lerpAmount = Mathf.Clamp(1.0f - (Vector3.Distance(transform.localScale, m_TargetScale) * m_ScaleMag), 0.25f, 1.0f); - transform.localScale = Vector3.Lerp(transform.localScale, m_TargetScale, lerpAmount); - NonSynchronizedScaleReached = Approximately(GetSynchronizedScale(transform.localScale), m_TargetScale); + m_OriginalScale = Vector3.Lerp(m_OriginalScale, m_TargetScale, m_Lerp); + transform.localScale = GetSynchronizedScale(transform.localScale) + m_OriginalScale; + NonSynchronizedScaleReached = Approximately(m_OriginalScale, m_TargetScale); + if (NonSynchronizedScaleReached || lerpComplete) + { + Log($"[{name}][Scale] Current: {transform.localScale} | Current-NonSync: {GetNonSynchronizedScale(transform.localScale)} | Target: {m_TargetScale}"); + } } } + #endregion + } - public override void OnUpdate() - { - base.OnUpdate(); + protected override IEnumerator OnSetup() + { + NetworkTransformTestComponent.Reset(); + return base.OnSetup(); + } - MoveObjectLocally(); + /// + /// All of the below versions of + /// assure that at least 1 axis is disabled and/or 1 axis is enabled + /// + /// + private bool ShouldSyncAxis() + { + return ShouldSyncAxis(true, true, false); + } + + private bool ShouldSyncAxis(bool first) + { + return ShouldSyncAxis(first, true, false); + } + + private bool ShouldSyncAxis(bool first, bool second, bool lastValue) + { + // Increase chances to not synchronize based on previous values + var start = 0; + if (first) + { + start += 20; + } + if (second) + { + start += 30; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected bool Approximately(Vector3 a, Vector3 b) + // If we are on the last axis value, then + // we want to check for the previous two + // being both enabled or disabled in order + // to assure there is at least one axis that + // is enabled and at least one axis that is + // disabled. + if (lastValue) { - var deltaVariance = 0.01f; - return System.Math.Round(Mathf.Abs(a.x - b.x), 2) <= deltaVariance && - System.Math.Round(Mathf.Abs(a.y - b.y), 2) <= deltaVariance && - System.Math.Round(Mathf.Abs(a.z - b.z), 2) <= deltaVariance; + if (first && second) + { + // If the previous two are enabled, then + // make the last one disabled. + return false; + } + else + if (!first && !second) + { + // If both are disabled, then make the + // last one enabled. + return true; + } } + return Random.Range(start, 100) >= 50 ? false : true; } protected override void OnServerAndClientsCreated() { m_PrefabToSpawn = CreateNetworkObjectPrefab("TestObject"); var networkTransform = m_PrefabToSpawn.AddComponent(); - networkTransform.SyncPositionX = true; - networkTransform.SyncPositionY = false; - networkTransform.SyncPositionZ = true; - - networkTransform.SyncRotAngleX = true; - networkTransform.SyncRotAngleY = false; - networkTransform.SyncRotAngleZ = false; - - networkTransform.SyncScaleX = false; - networkTransform.SyncScaleY = true; - networkTransform.SyncScaleZ = false; + // Randomly select one or more axis to disable + networkTransform.SyncPositionX = ShouldSyncAxis(); + networkTransform.SyncPositionY = ShouldSyncAxis(networkTransform.SyncPositionX); + networkTransform.SyncPositionZ = ShouldSyncAxis(networkTransform.SyncPositionX, networkTransform.SyncPositionY, true); + networkTransform.SyncRotAngleX = ShouldSyncAxis(); + networkTransform.SyncRotAngleY = ShouldSyncAxis(networkTransform.SyncRotAngleX); + networkTransform.SyncRotAngleZ = ShouldSyncAxis(networkTransform.SyncRotAngleX, networkTransform.SyncRotAngleY, true); + networkTransform.SyncScaleX = ShouldSyncAxis(); + networkTransform.SyncScaleY = ShouldSyncAxis(networkTransform.SyncScaleX); + networkTransform.SyncScaleZ = ShouldSyncAxis(networkTransform.SyncScaleX, networkTransform.SyncScaleY, true); base.OnServerAndClientsCreated(); } + /// + /// Conditional to verify that all spawned instances' transform values match + /// private bool AllTransformsAreApproximatelyTheSame() { m_ErrorMsg.Clear(); - var authorityInstance = NetworkTransformTestComponent.AuthorityInstance; + var authorityInstance = m_AuthorityInstance.GetComponent(); foreach (var instance in NetworkTransformTestComponent.AllInstances) { - if (instance == authorityInstance) + if (instance == authorityInstance) { continue; } @@ -303,6 +480,10 @@ private bool AllTransformsAreApproximatelyTheSame() return m_ErrorMsg.Length == 0; } + /// + /// Conditional to verify that all spawned instances' finished their local + /// non-synchronized axis motion. + /// private bool AllNonSynchronizedMotionCompleted() { m_ErrorMsg.Clear(); @@ -317,9 +498,12 @@ private bool AllNonSynchronizedMotionCompleted() return m_ErrorMsg.Length == 0; } + /// + /// Conditional to verify that all clients have spawned an instance of the test object. + /// private bool AllClientsSpawnedObject() { - foreach(var networkManager in m_NetworkManagers) + foreach (var networkManager in m_NetworkManagers) { if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObjectId)) { @@ -329,138 +513,78 @@ private bool AllClientsSpawnedObject() return true; } + /// + /// Validates that a non-authority instances can apply changes to any non-synchronized + /// axis value when using a NetworkTransform. + /// [UnityTest] public IEnumerator NonAuthorityUpdateNonSynchronizedAxis() { var authority = GetNonAuthorityNetworkManager(); m_AuthorityInstance = SpawnObject(m_PrefabToSpawn, authority).GetComponent(); + NetworkTransformTestComponent.AuthorityInstance = m_AuthorityInstance.GetComponent(); yield return WaitForConditionOrTimeOut(AllClientsSpawnedObject); AssertOnTimeout($"All clients did not spawn {m_AuthorityInstance.name}!"); - for(int i = 0; i < k_NumberOfPasses; i++) + var authorityComponent = m_AuthorityInstance.GetComponent(); + for (int i = 0; i < k_NumberOfPasses; i++) { - var positionDelta = GetRandomVector3(-4, 4); - var rotationDelta = GetRandomVector3(-20, 20); - var scaleDelta = GetRandomVector3(-2, 2); - + // Start moving the authority on the axis being synchronized var movePosition = NetworkTransformTestComponent.AuthorityInstance.MovePosition(GetRandomVector3(-4, 4)); var moveRotation = NetworkTransformTestComponent.AuthorityInstance.MoveRotation(GetRandomVector3(-20, 20)); var moveScale = NetworkTransformTestComponent.AuthorityInstance.MoveScale(GetRandomVector3(-2, 2)); + // Set the non-synchronized axis delta on the authority and preserve each axis delta + // to be applied to all other non-authority instances. + var positionDelta = authorityComponent.SetRandomNonSynchPosition(4); + var rotationDelta = authorityComponent.SetRandomNonSynchRotation(20); + var scaleDelta = authorityComponent.SetRandomNonSynchScale(2); + + var builder = new StringBuilder(); + builder.AppendLine($"[Iteration-{i}]Final Expected Position: {movePosition + positionDelta} | Non-Synch: {positionDelta}"); + VerboseDebug(builder.ToString()); foreach (var testTransform in NetworkTransformTestComponent.AllInstances) { - testTransform.SetDirValues(positionDelta, rotationDelta, scaleDelta, true); + // We only need to start the authority instance moving + // for the non-synchronized axis + if (testTransform == authorityComponent) + { + testTransform.ShouldMove(true); + continue; + } + // Apply the non-synchronized axis deltas to each cloned instance + // and start the local motion. + testTransform.SetNonSynchPositionTarget(positionDelta); + testTransform.SetNonSynchRotationTarget(rotationDelta); + testTransform.SetNonSynchScaleTarget(scaleDelta); + testTransform.ShouldMove(true); } - // Wait for all instances to finish their local controlled changes + // Wait for all instances to finish their local, non-synchronized, axis changes yield return WaitForConditionOrTimeOut(AllNonSynchronizedMotionCompleted); AssertOnTimeout($"[Iteration: {i}] Not all instances completed local motion! {m_ErrorMsg}"); - // Wait for all instances' transforms to match + // Verify that upon completing motion, all instances' transforms match yield return WaitForConditionOrTimeOut(AllTransformsAreApproximatelyTheSame); - AssertOnTimeout($"[Iteration: {i}] Not all instances' transforms match! {m_ErrorMsg}"); - - var builder = new StringBuilder(); - builder.AppendLine($"Final Expected Position: {movePosition + positionDelta}"); - foreach (var testTransform in NetworkTransformTestComponent.AllInstances) - { - builder.AppendLine($"[Client-{testTransform.NetworkManager.LocalClientId}] Position: {testTransform.transform.position}"); - } - Debug.Log(builder.ToString()); - } - } - - private GameObject m_OwnershipObject; - private NetworkObject m_OwnershipNetworkObject; - private bool AllObjectsSpawnedOnClients() - { - foreach (var networkManager in m_NetworkManagers) - { - if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_OwnershipNetworkObject.NetworkObjectId)) - { - return false; - } - } - return true; - } - - private bool ObjectHiddenOnNonAuthorityClients() - { - foreach (var networkManager in m_NetworkManagers) - { - if (networkManager.LocalClientId == m_OwnershipNetworkObject.OwnerClientId) - { - continue; - } - if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_OwnershipNetworkObject.NetworkObjectId)) - { - return false; - } - } - return true; - } - - [UnityTest] - public IEnumerator NetworkShowWithChangeOwnershipTest() - { - var authority = GetAuthorityNetworkManager(); - - m_OwnershipObject = SpawnObject(m_PrefabToSpawn, authority); - m_OwnershipNetworkObject = m_OwnershipObject.GetComponent(); - yield return WaitForConditionOrTimeOut(AllObjectsSpawnedOnClients); - AssertOnTimeout("Timed out waiting for all clients to spawn the ownership object!"); - - VerboseDebug($"Hiding object {m_OwnershipNetworkObject.NetworkObjectId} on all clients"); - foreach (var client in m_NetworkManagers) - { - if (client == authority) + // For debugging purposes + if (s_GlobalTimeoutHelper.HasTimedOut()) { - continue; - } - m_OwnershipNetworkObject.NetworkHide(client.LocalClientId); - } - - yield return WaitForConditionOrTimeOut(ObjectHiddenOnNonAuthorityClients); - AssertOnTimeout("Timed out waiting for all clients to hide the ownership object!"); - - m_NewOwner = GetNonAuthorityNetworkManager(); - Assert.AreNotEqual(m_OwnershipNetworkObject.OwnerClientId, m_NewOwner.LocalClientId, $"Client-{m_NewOwner.LocalClientId} should not have ownership of object {m_OwnershipNetworkObject.NetworkObjectId}!"); - Assert.False(m_NewOwner.SpawnManager.SpawnedObjects.ContainsKey(m_OwnershipNetworkObject.NetworkObjectId), $"Client-{m_NewOwner.LocalClientId} should not have object {m_OwnershipNetworkObject.NetworkObjectId} spawned!"); - - // Run NetworkShow and ChangeOwnership directly after one-another - VerboseDebug($"Calling {nameof(NetworkObject.NetworkShow)} on object {m_OwnershipNetworkObject.NetworkObjectId} for client {m_NewOwner.LocalClientId}"); - m_OwnershipNetworkObject.NetworkShow(m_NewOwner.LocalClientId); - VerboseDebug($"Calling {nameof(NetworkObject.ChangeOwnership)} on object {m_OwnershipNetworkObject.NetworkObjectId} for client {m_NewOwner.LocalClientId}"); - m_OwnershipNetworkObject.ChangeOwnership(m_NewOwner.LocalClientId); - m_ObjectId = m_OwnershipNetworkObject.NetworkObjectId; - yield return WaitForConditionOrTimeOut(OwnershipHasChanged); - AssertOnTimeout($"Timed out waiting for clients-{m_NewOwner.LocalClientId} to gain ownership of object {m_OwnershipNetworkObject.NetworkObjectId}!"); - VerboseDebug($"Client {m_NewOwner.LocalClientId} now owns object {m_OwnershipNetworkObject.NetworkObjectId}!"); - } - - private NetworkManager m_NewOwner; - - private bool OwnershipHasChanged() - { - if (!m_NewOwner.SpawnManager.SpawnedObjects.ContainsKey(m_ObjectId)) - { - return false; - } - return m_NewOwner.SpawnManager.SpawnedObjects[m_ObjectId].OwnerClientId == m_NewOwner.LocalClientId; - } - - private ulong m_ObjectId; - private bool ObjectDespawned() - { - foreach (var networkManager in m_NetworkManagers) - { - if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_ObjectId)) - { - return false; + builder.Clear(); + builder.AppendLine($"Final Expected Position: {movePosition + positionDelta}"); + builder.AppendLine($"Final Expected Rotation: {moveRotation + rotationDelta}"); + builder.AppendLine($"Final Expected Scale: {moveScale + scaleDelta}"); + foreach (var testTransform in NetworkTransformTestComponent.AllInstances) + { + builder.AppendLine($"[Client-{testTransform.NetworkManager.LocalClientId}] " + + $"Position: {testTransform.transform.position}" + + $"Rotation: {testTransform.transform.rotation.eulerAngles}" + + $"Scale: {testTransform.transform.localScale}"); + } + Debug.Log(builder.ToString()); } + AssertOnTimeout($"[Iteration: {i}] Not all instances' transforms match! {m_ErrorMsg}"); } - return true; } } } From 7cc433abe6fe2a9cc61212d58269d05f352a7486 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Fri, 30 May 2025 16:19:31 -0500 Subject: [PATCH 4/8] style removing whitespace --- .../Runtime/Components/NetworkTransform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index faf5d79b29..c4321958d5 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -2537,7 +2537,7 @@ protected internal void ApplyAuthoritativeState() adjustedRotAngles.z = SyncRotAngleZ ? adjustedRotAngles.z : currentRotation.z; adjustedRotation.eulerAngles = adjustedRotAngles; - + var adjustedScale = m_InternalCurrentScale; var currentScale = GetScale(); adjustedScale.x = SyncScaleX ? adjustedScale.x : currentScale.x; From 94215934affa4a0287feaecde0df328899524e45 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Fri, 30 May 2025 16:38:12 -0500 Subject: [PATCH 5/8] style Fixing imports ordering --- .../NetworkTransform/NetworkTransformNonAuthorityTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs index 9632e098a1..d779de46ec 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs @@ -1,8 +1,8 @@ -using NUnit.Framework; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; +using NUnit.Framework; using Unity.Netcode.Components; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; From 03f5f7bd62228b919651fdba1579cd466dd0e6bc Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 31 May 2025 11:06:24 -0500 Subject: [PATCH 6/8] update Adding change log entry --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 01a351dce1..1fb8217db9 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where non-authority `NetworkTransform` instances would not allow non-synchronized axis values to be updated locally. (#3471) - Fixed issue where `NetworkVariable`s on a `NetworkBehaviour` could fail to synchronize changes if one has `NetworkVariableUpdateTraits` set and is dirty but is not ready to send. (#3466) - Fixed inconsistencies in the `OnSceneEvent` callback. (#3458) - Fixed issues with the `NetworkBehaviour` and `NetworkVariable` length safety checks. (#3405) From 65df092f6890dd57f16b74ba0800afefd3695055 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 31 May 2025 11:55:07 -0500 Subject: [PATCH 7/8] test Switched to a euler comparison to avoid floating point false negative results. --- .../NetworkTransform/NetworkTransformNonAuthorityTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs index d779de46ec..b44871e6a5 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs @@ -466,7 +466,7 @@ private bool AllTransformsAreApproximatelyTheSame() m_ErrorMsg.AppendLine($"[{instance.name}] Position ({instance.transform.position}) is not " + $"equal to authority's ({authorityInstance.transform.position})! "); } - if (!Approximately(instance.transform.rotation, authorityInstance.transform.rotation)) + if (!ApproximatelyEuler(instance.transform.rotation.eulerAngles, authorityInstance.transform.rotation.eulerAngles)) { m_ErrorMsg.AppendLine($"[{instance.name}] Rotation ({instance.transform.rotation.eulerAngles}) is not " + $"equal to authority's ({authorityInstance.transform.rotation.eulerAngles})! "); From 007f0e74f2cc345d9f1a13fbc1dcc34297988b53 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Sat, 31 May 2025 11:58:33 -0500 Subject: [PATCH 8/8] style removing unused parameter. --- .../NetworkTransform/NetworkTransformNonAuthorityTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs index b44871e6a5..e5fb5a4ba9 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs @@ -265,7 +265,7 @@ private Vector3 GetSynchronizedScale(Vector3 scale) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector3 GetNonSynchronizedScale(Vector3 scale, bool invert = false) + private Vector3 GetNonSynchronizedScale(Vector3 scale) { scale.x *= !SyncScaleX ? 1 : 0; scale.y *= !SyncScaleY ? 1 : 0;