Skip to content

Commit

Permalink
fix: NetworkTransform full precision state updates being lost when in…
Browse files Browse the repository at this point in the history
…terpolating [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.
  • Loading branch information
NoelStephensUnity authored Jul 14, 2023
1 parent 9ca4efb commit 9af9280
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 56 deletions.
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

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

Expand Down
29 changes: 21 additions & 8 deletions com.unity.netcode.gameobjects/Components/NetworkTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,8 +1159,11 @@ public Vector3 GetScale(bool getCurrentState = false)
// Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to
// the portions of the transform being synchronized.
private Vector3 m_CurrentPosition;
private Vector3 m_TargetPosition;
private Vector3 m_CurrentScale;
private Vector3 m_TargetScale;
private Quaternion m_CurrentRotation;
private Vector3 m_TargetRotation;


[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -2009,6 +2012,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
}

m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;

// Apply the position
if (newState.InLocalSpace)
Expand All @@ -2026,7 +2030,6 @@ private void ApplyTeleportingState(NetworkTransformState newState)
if (UseHalfFloatPrecision)
{
currentScale = newState.Scale;
m_CurrentScale = currentScale;
}
else
{
Expand All @@ -2049,6 +2052,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
}

m_CurrentScale = currentScale;
m_TargetScale = currentScale;
m_ScaleInterpolator.ResetTo(currentScale, sentTime);

// Apply the adjusted scale
Expand Down Expand Up @@ -2082,6 +2086,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
}

m_CurrentRotation = currentRotation;
m_TargetRotation = currentRotation.eulerAngles;
m_RotationInterpolator.ResetTo(currentRotation, sentTime);

if (InLocalSpace)
Expand Down Expand Up @@ -2158,28 +2163,29 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
}
else
{
var currentPosition = GetSpaceRelativePosition();
var newTargetPosition = m_TargetPosition;
if (m_LocalAuthoritativeNetworkState.HasPositionX)
{
currentPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
newTargetPosition.x = m_LocalAuthoritativeNetworkState.PositionX;
}

if (m_LocalAuthoritativeNetworkState.HasPositionY)
{
currentPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
newTargetPosition.y = m_LocalAuthoritativeNetworkState.PositionY;
}

if (m_LocalAuthoritativeNetworkState.HasPositionZ)
{
currentPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
newTargetPosition.z = m_LocalAuthoritativeNetworkState.PositionZ;
}
UpdatePositionInterpolator(currentPosition, sentTime);
UpdatePositionInterpolator(newTargetPosition, sentTime);
m_TargetPosition = newTargetPosition;
}
}

if (m_LocalAuthoritativeNetworkState.HasScaleChange)
{
var currentScale = transform.localScale;
var currentScale = m_TargetScale;
if (UseHalfFloatPrecision)
{
for (int i = 0; i < 3; i++)
Expand Down Expand Up @@ -2207,6 +2213,7 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
currentScale.z = m_LocalAuthoritativeNetworkState.ScaleZ;
}
}
m_TargetScale = currentScale;
m_ScaleInterpolator.AddMeasurement(currentScale, sentTime);
}

Expand All @@ -2221,7 +2228,9 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
}
else
{
currentEulerAngles = m_TargetRotation;
// Adjust based on which axis changed
// (both half precision and full precision apply Eulers to the RotAngle properties when reading the update)
if (m_LocalAuthoritativeNetworkState.HasRotAngleX)
{
currentEulerAngles.x = m_LocalAuthoritativeNetworkState.RotAngleX;
Expand All @@ -2236,6 +2245,7 @@ private void UpdateState(NetworkTransformState oldState, NetworkTransformState n
{
currentEulerAngles.z = m_LocalAuthoritativeNetworkState.RotAngleZ;
}
m_TargetRotation = currentEulerAngles;
currentRotation.eulerAngles = currentEulerAngles;
}

Expand Down Expand Up @@ -2489,8 +2499,11 @@ protected void Initialize()

ResetInterpolatedStateToCurrentAuthoritativeState();
m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;
m_CurrentScale = transform.localScale;
m_TargetScale = transform.localScale;
m_CurrentRotation = currentRotation;
m_TargetRotation = currentRotation.eulerAngles;

}

Expand Down Expand Up @@ -2649,7 +2662,7 @@ protected virtual void Update()
var serverTime = NetworkManager.ServerTime;
var cachedDeltaTime = NetworkManager.RealTimeProvider.DeltaTime;
var cachedServerTime = serverTime.Time;
// TODO: Investigate Further

// With owner authoritative mode, non-authority clients can lag behind
// by more than 1 tick period of time. The current "solution" for now
// is to make their cachedRenderTime run 2 ticks behind.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
Expand Down Expand Up @@ -378,7 +379,7 @@ private void AllChildrenLocalTransformValuesMatch()
{
var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues);
//TimeTravelToNextTick();
var infoMessage = new System.Text.StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
var infoMessage = new StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
var authorityObjectLocalPosition = m_AuthorityChildObject.transform.localPosition;
var authorityObjectLocalRotation = m_AuthorityChildObject.transform.localRotation.eulerAngles;
var authorityObjectLocalScale = m_AuthorityChildObject.transform.localScale;
Expand Down Expand Up @@ -567,9 +568,9 @@ private void WaitForNextTick()
}
}

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

protected override void OnNewClientCreated(NetworkManager networkManager)
{
Expand All @@ -594,22 +595,69 @@ protected override float GetDeltaVarianceThreshold()


private Axis m_CurrentAxis;

private bool m_AxisExcluded;

/// <summary>
/// Randomly determine if an axis should be excluded.
/// If so, then randomly pick one of the axis to be excluded.
/// </summary>
private Vector3 RandomlyExcludeAxis(Vector3 delta)
{
if (Random.Range(0.0f, 1.0f) >= 0.5f)
{
m_AxisExcluded = true;
var axisToIgnore = Random.Range(0, 2);
switch (axisToIgnore)
{
case 0:
{
delta.x = 0;
break;
}
case 1:
{
delta.y = 0;
break;
}
case 2:
{
delta.z = 0;
break;
}
}
}
return delta;
}

/// <summary>
/// This validates that multiple changes can occur within the same tick or over
/// several ticks while still keeping non-authoritative instances synchronized.
/// </summary>
/// <remarks>
/// When testing < 3 axis: Interpolation is disabled and only 3 delta updates are applied per unique test
/// When testing 3 axis: Interpolation is enabled, sometimes an axis is intentionally excluded during a
/// delta update, and it runs through 8 delta updates per unique test.
/// </remarks>
[Test]
public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState,
[Values] Precision precision, [Values] Rotation rotationSynch, [Values] Axis axis)
{
// In the name of reducing the very long time it takes to interpolate and run all of the possible combinations,
// we only interpolate when the second client joins
m_AuthoritativeTransform.Interpolate = false;
m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ;
bool axisY = axis == Axis.Y || axis == Axis.XY || axis == Axis.YZ || axis == Axis.XYZ;
bool axisZ = axis == Axis.Z || axis == Axis.XZ || axis == Axis.YZ || axis == Axis.XYZ;

var axisCount = axisX ? 1 : 0;
axisCount += axisY ? 1 : 0;
axisCount += axisZ ? 1 : 0;

// Enable interpolation when all 3 axis are selected to make sure we are synchronizing properly
// when interpolation is enabled.
m_AuthoritativeTransform.Interpolate = axisCount == 3 ? true : false;

m_CurrentAxis = axis;

// Authority dictates what is synchronized and what the precision is going to be
// so we only need to set this on the authoritative side.
m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
Expand Down Expand Up @@ -640,84 +688,87 @@ public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace test
m_AuthoritativeTransform.SyncScaleY = axisY;
m_AuthoritativeTransform.SyncScaleZ = axisZ;


var positionStart = GetRandomVector3(0.25f, 1.75f);
var rotationStart = GetRandomVector3(1f, 15f);
var scaleStart = GetRandomVector3(0.25f, 2.0f);
var position = positionStart;
var rotation = rotationStart;
var scale = scaleStart;
var success = false;

m_AuthoritativeTransform.StatePushed = false;
// Wait for the deltas to be pushed
WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed);
// Allow the precision settings to propagate first as changing precision
// causes a teleport event to occur
WaitForNextTick();
var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations;

// Move and rotate within the same tick, validate the non-authoritative instance updates
// to each set of changes. Repeat several times.
for (int i = 0; i < k_PositionRotationScaleIterations; i++)
for (int i = 0; i < iterations; i++)
{
// Always reset this per delta update pass
m_AxisExcluded = false;
var deltaPositionDelta = GetRandomVector3(-1.5f, 1.5f);
var deltaRotationDelta = GetRandomVector3(-3.5f, 3.5f);
var deltaScaleDelta = GetRandomVector3(-0.5f, 0.5f);

m_NonAuthoritativeTransform.StateUpdated = false;
m_AuthoritativeTransform.StatePushed = false;
position = positionStart * i;
rotation = rotationStart * i;
scale = scaleStart * i;

// With two or more axis, excluding one of them while chaging another will validate that
// full precision updates are maintaining their target state value(s) to interpolate towards
if (axisCount == 3)
{
position += RandomlyExcludeAxis(deltaPositionDelta);
rotation += RandomlyExcludeAxis(deltaRotationDelta);
scale += RandomlyExcludeAxis(deltaScaleDelta);
}
else
{
position += deltaPositionDelta;
rotation += deltaRotationDelta;
scale += deltaScaleDelta;
}

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

// Wait for the deltas to be pushed
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})!");

// Wait for deltas to synchronize on non-authoritative side
var success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
// Provide additional debug info about what failed (if it fails)
if (!success)
// For 3 axis, we will skip validating that the non-authority interpolates to its target point at least once.
// This will validate that non-authoritative updates are maintaining their target state axis values if only 2
// of the axis are being updated to assure interpolation maintains the targeted axial value per axis.
// For 2 and 1 axis tests we always validate per delta update
if (m_AxisExcluded || axisCount < 3)
{
m_EnableVerboseDebug = true;
PositionRotationScaleMatches();
m_EnableVerboseDebug = false;
// Wait for deltas to synchronize on non-authoritative side
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
// Provide additional debug info about what failed (if it fails)
if (!success)
{
m_EnableVerboseDebug = true;
success = PositionRotationScaleMatches();
m_EnableVerboseDebug = false;
}
Assert.True(success, $"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
}
Assert.True(success, $"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
}

// Only enable interpolation when all axis are set (to reduce the test times)
if (axis == Axis.XYZ)
if (axisCount == 3)
{
// Now, enable interpolation
m_AuthoritativeTransform.Interpolate = true;
m_NonAuthoritativeTransform.StateUpdated = false;
m_AuthoritativeTransform.StatePushed = false;
// Wait for the delta (change in interpolation) to be pushed
var success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
Assert.True(success, $"[Interpolation Enable] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed}) or state to be updated ({m_NonAuthoritativeTransform.StateUpdated})!");

// Continue for one more update with interpolation enabled
// Note: We are just verifying one update with interpolation enabled due to the number of tests this integration test has to run
// and since the NestedNetworkTransformTests already tests interpolation under the same number of conditions (excluding Axis).
// This is just to verify selecting specific axis doesn't cause issues when interpolating as well.
m_NonAuthoritativeTransform.StateUpdated = false;
m_AuthoritativeTransform.StatePushed = false;
position = positionStart * k_PositionRotationScaleIterations;
rotation = rotationStart * k_PositionRotationScaleIterations;
scale = scaleStart * k_PositionRotationScaleIterations;
MoveRotateAndScaleAuthority(position, rotation, scale, overideState);

// Wait for the deltas to be pushed and updated
success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated);
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})!");

success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches, 120);

// As a final test, wait for deltas to synchronize on non-authoritative side to assure it interpolates to th
success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
// Provide additional debug info about what failed (if it fails)
if (!success)
{
m_EnableVerboseDebug = true;
PositionRotationScaleMatches();
success = PositionRotationScaleMatches();
m_EnableVerboseDebug = false;
}
Assert.True(success, $"[Interpolation {k_PositionRotationScaleIterations}] Timed out waiting for non-authority to match authority's position or rotation");
Assert.True(success, $"Timed out waiting for non-authority to match authority's position or rotation");
}
}

Expand Down

0 comments on commit 9af9280

Please sign in to comment.