Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: NetworkTransform full precision state updates being lost when interpolating [MTT-6799] #2624

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading