Skip to content

Commit 9af9280

Browse files
fix: NetworkTransform full precision state updates being lost when interpolating [MTT-6799] (#2624)
* fix This fixes the issue where full precision transform synchronization was losing state while interpolating. * update initial change log entry * update Adding PR number to change log entry * fix Missed including half float precision scale with interpolation enabled since scale only updates the axis that changed (i.e. a target axis value could get stomped otherwise when half precision was enabled...the coming test discovered this). * test Modified NetworkTransformMultipleChangesOverTime to use interpolation when 3 axis are being tested as well as added the additional logic required to catch the issue not caught by this test where a state update could stomp a single axial value if the target value had not yet been reached.
1 parent 9ca4efb commit 9af9280

File tree

3 files changed

+121
-56
lines changed

3 files changed

+121
-56
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1313

1414
### Fixed
1515

16+
- Fixed issue where a `NetworkTransform` using full precision state updates was losing transform state updates when interpolation was enabled. (#2624)
1617
- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored for late joining clients. (#2623)
1718
- Fixed issue where invoking `NetworkManager.Shutdown` multiple times, depending upon the timing, could cause an exception. (#2622)
1819

com.unity.netcode.gameobjects/Components/NetworkTransform.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,8 +1159,11 @@ public Vector3 GetScale(bool getCurrentState = false)
11591159
// Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to
11601160
// the portions of the transform being synchronized.
11611161
private Vector3 m_CurrentPosition;
1162+
private Vector3 m_TargetPosition;
11621163
private Vector3 m_CurrentScale;
1164+
private Vector3 m_TargetScale;
11631165
private Quaternion m_CurrentRotation;
1166+
private Vector3 m_TargetRotation;
11641167

11651168

11661169
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2009,6 +2012,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
20092012
}
20102013

20112014
m_CurrentPosition = currentPosition;
2015+
m_TargetPosition = currentPosition;
20122016

20132017
// Apply the position
20142018
if (newState.InLocalSpace)
@@ -2026,7 +2030,6 @@ private void ApplyTeleportingState(NetworkTransformState newState)
20262030
if (UseHalfFloatPrecision)
20272031
{
20282032
currentScale = newState.Scale;
2029-
m_CurrentScale = currentScale;
20302033
}
20312034
else
20322035
{
@@ -2049,6 +2052,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
20492052
}
20502053

20512054
m_CurrentScale = currentScale;
2055+
m_TargetScale = currentScale;
20522056
m_ScaleInterpolator.ResetTo(currentScale, sentTime);
20532057

20542058
// Apply the adjusted scale
@@ -2082,6 +2086,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
20822086
}
20832087

20842088
m_CurrentRotation = currentRotation;
2089+
m_TargetRotation = currentRotation.eulerAngles;
20852090
m_RotationInterpolator.ResetTo(currentRotation, sentTime);
20862091

20872092
if (InLocalSpace)
@@ -2158,28 +2163,29 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
21582163
}
21592164
else
21602165
{
2161-
var currentPosition = GetSpaceRelativePosition();
2166+
var newTargetPosition = m_TargetPosition;
21622167
if (m_LocalAuthoritativeNetworkState.HasPositionX)
21632168
{
2164-
currentPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
2169+
newTargetPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
21652170
}
21662171

21672172
if (m_LocalAuthoritativeNetworkState.HasPositionY)
21682173
{
2169-
currentPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
2174+
newTargetPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
21702175
}
21712176

21722177
if (m_LocalAuthoritativeNetworkState.HasPositionZ)
21732178
{
2174-
currentPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
2179+
newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
21752180
}
2176-
UpdatePositionInterpolator(currentPosition, sentTime);
2181+
UpdatePositionInterpolator(newTargetPosition, sentTime);
2182+
m_TargetPosition = newTargetPosition;
21772183
}
21782184
}
21792185

21802186
if (m_LocalAuthoritativeNetworkState.HasScaleChange)
21812187
{
2182-
var currentScale = transform.localScale;
2188+
var currentScale = m_TargetScale;
21832189
if (UseHalfFloatPrecision)
21842190
{
21852191
for (int i = 0; i < 3; i++)
@@ -2207,6 +2213,7 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
22072213
currentScale.z = m_LocalAuthoritativeNetworkState.ScaleZ;
22082214
}
22092215
}
2216+
m_TargetScale = currentScale;
22102217
m_ScaleInterpolator.AddMeasurement(currentScale, sentTime);
22112218
}
22122219

@@ -2221,7 +2228,9 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
22212228
}
22222229
else
22232230
{
2231+
currentEulerAngles = m_TargetRotation;
22242232
// Adjust based on which axis changed
2233+
// (both half precision and full precision apply Eulers to the RotAngle properties when reading the update)
22252234
if (m_LocalAuthoritativeNetworkState.HasRotAngleX)
22262235
{
22272236
currentEulerAngles.x = m_LocalAuthoritativeNetworkState.RotAngleX;
@@ -2236,6 +2245,7 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
22362245
{
22372246
currentEulerAngles.z = m_LocalAuthoritativeNetworkState.RotAngleZ;
22382247
}
2248+
m_TargetRotation = currentEulerAngles;
22392249
currentRotation.eulerAngles = currentEulerAngles;
22402250
}
22412251

@@ -2489,8 +2499,11 @@ protected void Initialize()
24892499

24902500
ResetInterpolatedStateToCurrentAuthoritativeState();
24912501
m_CurrentPosition = currentPosition;
2502+
m_TargetPosition = currentPosition;
24922503
m_CurrentScale = transform.localScale;
2504+
m_TargetScale = transform.localScale;
24932505
m_CurrentRotation = currentRotation;
2506+
m_TargetRotation = currentRotation.eulerAngles;
24942507

24952508
}
24962509

@@ -2649,7 +2662,7 @@ protected virtual void Update()
26492662
var serverTime = NetworkManager.ServerTime;
26502663
var cachedDeltaTime = NetworkManager.RealTimeProvider.DeltaTime;
26512664
var cachedServerTime = serverTime.Time;
2652-
// TODO: Investigate Further
2665+
26532666
// With owner authoritative mode, non-authority clients can lag behind
26542667
// by more than 1 tick period of time. The current "solution" for now
26552668
// is to make their cachedRenderTime run 2 ticks behind.

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Text;
23
using NUnit.Framework;
34
using Unity.Netcode.Components;
45
using Unity.Netcode.TestHelpers.Runtime;
@@ -378,7 +379,7 @@ private void AllChildrenLocalTransformValuesMatch()
378379
{
379380
var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues);
380381
//TimeTravelToNextTick();
381-
var infoMessage = new System.Text.StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
382+
var infoMessage = new StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
382383
var authorityObjectLocalPosition = m_AuthorityChildObject.transform.localPosition;
383384
var authorityObjectLocalRotation = m_AuthorityChildObject.transform.localRotation.eulerAngles;
384385
var authorityObjectLocalScale = m_AuthorityChildObject.transform.localScale;
@@ -567,9 +568,9 @@ private void WaitForNextTick()
567568
}
568569
}
569570

570-
// The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
571-
// Note: this was reduced from 8 iterations to 3 due to the number of tests based on all of the various parameter combinations
571+
// The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
572572
private const int k_PositionRotationScaleIterations = 3;
573+
private const int k_PositionRotationScaleIterations3Axis = 8;
573574

574575
protected override void OnNewClientCreated(NetworkManager networkManager)
575576
{
@@ -594,22 +595,69 @@ protected override float GetDeltaVarianceThreshold()
594595

595596

596597
private Axis m_CurrentAxis;
598+
599+
private bool m_AxisExcluded;
600+
601+
/// <summary>
602+
/// Randomly determine if an axis should be excluded.
603+
/// If so, then randomly pick one of the axis to be excluded.
604+
/// </summary>
605+
private Vector3 RandomlyExcludeAxis(Vector3 delta)
606+
{
607+
if (Random.Range(0.0f, 1.0f) >= 0.5f)
608+
{
609+
m_AxisExcluded = true;
610+
var axisToIgnore = Random.Range(0, 2);
611+
switch (axisToIgnore)
612+
{
613+
case 0:
614+
{
615+
delta.x = 0;
616+
break;
617+
}
618+
case 1:
619+
{
620+
delta.y = 0;
621+
break;
622+
}
623+
case 2:
624+
{
625+
delta.z = 0;
626+
break;
627+
}
628+
}
629+
}
630+
return delta;
631+
}
632+
597633
/// <summary>
598634
/// This validates that multiple changes can occur within the same tick or over
599635
/// several ticks while still keeping non-authoritative instances synchronized.
600636
/// </summary>
637+
/// <remarks>
638+
/// When testing < 3 axis: Interpolation is disabled and only 3 delta updates are applied per unique test
639+
/// When testing 3 axis: Interpolation is enabled, sometimes an axis is intentionally excluded during a
640+
/// delta update, and it runs through 8 delta updates per unique test.
641+
/// </remarks>
601642
[Test]
602643
public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState,
603644
[Values] Precision precision, [Values] Rotation rotationSynch, [Values] Axis axis)
604645
{
605-
// In the name of reducing the very long time it takes to interpolate and run all of the possible combinations,
606-
// we only interpolate when the second client joins
607-
m_AuthoritativeTransform.Interpolate = false;
608646
m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
609647
bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ;
610648
bool axisY = axis == Axis.Y || axis == Axis.XY || axis == Axis.YZ || axis == Axis.XYZ;
611649
bool axisZ = axis == Axis.Z || axis == Axis.XZ || axis == Axis.YZ || axis == Axis.XYZ;
650+
651+
var axisCount = axisX ? 1 : 0;
652+
axisCount += axisY ? 1 : 0;
653+
axisCount += axisZ ? 1 : 0;
654+
655+
// Enable interpolation when all 3 axis are selected to make sure we are synchronizing properly
656+
// when interpolation is enabled.
657+
m_AuthoritativeTransform.Interpolate = axisCount == 3 ? true : false;
658+
612659
m_CurrentAxis = axis;
660+
613661
// Authority dictates what is synchronized and what the precision is going to be
614662
// so we only need to set this on the authoritative side.
615663
m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
@@ -640,84 +688,87 @@ public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace test
640688
m_AuthoritativeTransform.SyncScaleY = axisY;
641689
m_AuthoritativeTransform.SyncScaleZ = axisZ;
642690

643-
644691
var positionStart = GetRandomVector3(0.25f, 1.75f);
645692
var rotationStart = GetRandomVector3(1f, 15f);
646693
var scaleStart = GetRandomVector3(0.25f, 2.0f);
647694
var position = positionStart;
648695
var rotation = rotationStart;
649696
var scale = scaleStart;
697+
var success = false;
698+
650699
m_AuthoritativeTransform.StatePushed = false;
651700
// Wait for the deltas to be pushed
652701
WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
653702
// Allow the precision settings to propagate first as changing precision
654703
// causes a teleport event to occur
655704
WaitForNextTick();
705+
var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations;
656706

657707
// Move and rotate within the same tick, validate the non-authoritative instance updates
658708
// to each set of changes. Repeat several times.
659-
for (int i = 0; i < k_PositionRotationScaleIterations; i++)
709+
for (int i = 0; i < iterations; i++)
660710
{
711+
// Always reset this per delta update pass
712+
m_AxisExcluded = false;
713+
var deltaPositionDelta = GetRandomVector3(-1.5f, 1.5f);
714+
var deltaRotationDelta = GetRandomVector3(-3.5f, 3.5f);
715+
var deltaScaleDelta = GetRandomVector3(-0.5f, 0.5f);
716+
661717
m_NonAuthoritativeTransform.StateUpdated = false;
662718
m_AuthoritativeTransform.StatePushed = false;
663-
position = positionStart * i;
664-
rotation = rotationStart * i;
665-
scale = scaleStart * i;
719+
720+
// With two or more axis, excluding one of them while chaging another will validate that
721+
// full precision updates are maintaining their target state value(s) to interpolate towards
722+
if (axisCount == 3)
723+
{
724+
position += RandomlyExcludeAxis(deltaPositionDelta);
725+
rotation += RandomlyExcludeAxis(deltaRotationDelta);
726+
scale += RandomlyExcludeAxis(deltaScaleDelta);
727+
}
728+
else
729+
{
730+
position += deltaPositionDelta;
731+
rotation += deltaRotationDelta;
732+
scale += deltaScaleDelta;
733+
}
666734

667735
// Apply delta between ticks
668736
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
669737

670738
// Wait for the deltas to be pushed
671739
Assert.True(WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated), $"[Non-Interpolate {i}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
672740

673-
// Wait for deltas to synchronize on non-authoritative side
674-
var success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
675-
// Provide additional debug info about what failed (if it fails)
676-
if (!success)
741+
// For 3 axis, we will skip validating that the non-authority interpolates to its target point at least once.
742+
// This will validate that non-authoritative updates are maintaining their target state axis values if only 2
743+
// of the axis are being updated to assure interpolation maintains the targeted axial value per axis.
744+
// For 2 and 1 axis tests we always validate per delta update
745+
if (m_AxisExcluded || axisCount < 3)
677746
{
678-
m_EnableVerboseDebug = true;
679-
PositionRotationScaleMatches();
680-
m_EnableVerboseDebug = false;
747+
// Wait for deltas to synchronize on non-authoritative side
748+
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
749+
// Provide additional debug info about what failed (if it fails)
750+
if (!success)
751+
{
752+
m_EnableVerboseDebug = true;
753+
success = PositionRotationScaleMatches();
754+
m_EnableVerboseDebug = false;
755+
}
756+
Assert.True(success, $"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
681757
}
682-
Assert.True(success, $"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
683758
}
684759

685-
// Only enable interpolation when all axis are set (to reduce the test times)
686-
if (axis == Axis.XYZ)
760+
if (axisCount == 3)
687761
{
688-
// Now, enable interpolation
689-
m_AuthoritativeTransform.Interpolate = true;
690-
m_NonAuthoritativeTransform.StateUpdated = false;
691-
m_AuthoritativeTransform.StatePushed = false;
692-
// Wait for the delta (change in interpolation) to be pushed
693-
var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
694-
Assert.True(success, $"[Interpolation Enable] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
695-
696-
// Continue for one more update with interpolation enabled
697-
// Note: We are just verifying one update with interpolation enabled due to the number of tests this integration test has to run
698-
// and since the NestedNetworkTransformTests already tests interpolation under the same number of conditions (excluding Axis).
699-
// This is just to verify selecting specific axis doesn't cause issues when interpolating as well.
700-
m_NonAuthoritativeTransform.StateUpdated = false;
701-
m_AuthoritativeTransform.StatePushed = false;
702-
position = positionStart * k_PositionRotationScaleIterations;
703-
rotation = rotationStart * k_PositionRotationScaleIterations;
704-
scale = scaleStart * k_PositionRotationScaleIterations;
705-
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
706-
707-
// Wait for the deltas to be pushed and updated
708-
success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
709-
Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");
710-
711-
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches, 120);
712-
762+
// As a final test, wait for deltas to synchronize on non-authoritative side to assure it interpolates to th
763+
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
713764
// Provide additional debug info about what failed (if it fails)
714765
if (!success)
715766
{
716767
m_EnableVerboseDebug = true;
717-
PositionRotationScaleMatches();
768+
success = PositionRotationScaleMatches();
718769
m_EnableVerboseDebug = false;
719770
}
720-
Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for non-authority to match authority's position or rotation");
771+
Assert.True(success, $"Timed out waiting for non-authority to match authority's position or rotation");
721772
}
722773
}
723774

0 commit comments

Comments
 (0)