diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index c57dad8d41..82bb8d4830 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -22,6 +22,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
+- Fixed issue where a client, under above average latency and packet loss conditions, could receive multiple NetworkTransform state updates in one frame and when processing the state updates only the last state update would be applied to the transform if interpolation was disabled. (#3614)
+
### Security
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
index d369e82332..56ab5dcb27 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
@@ -3003,7 +3003,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
///
/// Only non-authoritative instances should invoke this
///
- private void ApplyUpdatedState(NetworkTransformState newState)
+ internal void ApplyUpdatedState(NetworkTransformState newState)
{
// Set the transforms's synchronization modes
InLocalSpace = newState.InLocalSpace;
@@ -3052,6 +3052,7 @@ private void ApplyUpdatedState(NetworkTransformState newState)
if (!Interpolate)
{
+ ApplyAuthoritativeState();
return;
}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs
index 57ea6df51c..04afd12cb5 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs
@@ -37,6 +37,72 @@ protected override void OnOneTimeTearDown()
base.OnOneTimeTearDown();
}
+ ///
+ /// This validates an issue where if multiple state updates are received in a single frame
+ /// and interpolation is disabled, only the last state udpate processed gets applied.
+ ///
+ [Test]
+ public void TestMultipleStateSynchronization([Values] bool isLocal, [Values] bool timeTravelBetweenStateUpdates)
+ {
+ // Assure no new state updates are pushed.
+ TimeTravel(0.5f, 60);
+
+ // Disable interpolation and set world or local space
+ m_NonAuthoritativeTransform.Interpolate = false;
+ m_NonAuthoritativeTransform.InLocalSpace = isLocal;
+
+ // Get the non-authority's state
+ var localState = m_NonAuthoritativeTransform.LocalAuthoritativeNetworkState;
+
+ // Assure this is not set to avoid a false positive result with teleporting
+ localState.IsTeleportingNextFrame = false;
+
+ // Simulate a state update
+ localState.UseInterpolation = false;
+ localState.CurrentPosition = new Vector3(5.0f, 0.0f, 0.0f);
+ localState.HasPositionX = true;
+ localState.PositionX = 5.0f;
+ localState.NetworkTick++;
+
+ var lastStateTick = localState.NetworkTick;
+ // Apply the simualted state update to the non-authority instance
+ m_NonAuthoritativeTransform.ApplyUpdatedState(localState);
+ // Simulate both having time between state updates and having state updates delivered back to back on the same frame
+ if (timeTravelBetweenStateUpdates)
+ {
+ TimeTravelAdvanceTick();
+ }
+
+ // Validate the state update was applied
+ var xValue = isLocal ? m_NonAuthoritativeTransform.transform.localPosition.x : m_NonAuthoritativeTransform.transform.position.x;
+ Assert.IsTrue(xValue == 5.0f, $"[Test1][IsLocal: {isLocal}] X axis ({xValue}) does not equal 5.0f!");
+
+
+ // Get the non-authority state
+ localState = m_NonAuthoritativeTransform.LocalAuthoritativeNetworkState;
+
+ //Assure we have not received any state updates from the authority that could skew the test
+ Assert.IsTrue(localState.NetworkTick == lastStateTick, $"Previous Non-authority state tick was {lastStateTick} but is now {localState.NetworkTick}. Authority pushed a state update.");
+
+ // Simualate a 2nd state update on a different position axis
+ localState.HasPositionX = false;
+ localState.HasPositionZ = true;
+ localState.PositionZ = -5.0f;
+ localState.NetworkTick++;
+ m_NonAuthoritativeTransform.ApplyUpdatedState(localState);
+ // Simulate both having time between state updates and having state updates delivered back to back on the same frame
+ if (timeTravelBetweenStateUpdates)
+ {
+ TimeTravelAdvanceTick();
+ }
+ var zValue = isLocal ? m_NonAuthoritativeTransform.transform.localPosition.z : m_NonAuthoritativeTransform.transform.position.z;
+ xValue = isLocal ? m_NonAuthoritativeTransform.transform.localPosition.x : m_NonAuthoritativeTransform.transform.position.x;
+
+ // Verify the previous state update's position and current state update's position
+ Assert.IsTrue(xValue == 5.0f, $"[Test2][IsLocal: {isLocal}] X axis ({xValue}) does not equal 5.0f!");
+ Assert.IsTrue(zValue == -5.0f, $"[Test2][IsLocal: {isLocal}] Z axis ({zValue}) does not equal -5.0f!");
+ }
+
///
/// Test to verify nonAuthority cannot change the transform directly
///