From 4e9174c1d51adf20e877e55d56d9f75db42f3153 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 3 Apr 2025 21:03:18 -0500 Subject: [PATCH 1/3] fix This update resolves the issue where the legacy lerp result was not yielding the same result as the original lerp. This update also resolves a secondary issue discovered by the services group where the final interpolation (i.e. last measurement in the queue) was not using a high enough precision value when determining if it reached its final destination point. This update also includes some inlining additions along with a spelling mistake with the IsApproximately method. It also updates the XML API regarding Legacy lerp to let users know that it does not use the tick latency value when calculating the ticksAgo value. --- .../BufferedLinearInterpolator.cs | 54 +++++++++++++------ .../BufferedLinearInterpolatorFloat.cs | 2 +- .../BufferedLinearInterpolatorQuaternion.cs | 2 +- .../BufferedLinearInterpolatorVector3.cs | 2 +- .../Runtime/Components/NetworkTransform.cs | 19 +++++-- 5 files changed, 56 insertions(+), 23 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index 538a6ba1a1..f168ab2372 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using UnityEngine; namespace Unity.Netcode @@ -14,9 +15,16 @@ public abstract class BufferedLinearInterpolator where T : struct // Constant absolute value for max buffer count instead of dynamic time based value. This is in case we have very low tick rates, so // that we don't have a very small buffer because of this. private const int k_BufferCountLimit = 100; - private const float k_AproximatePrecision = 0.0001f; + private const float k_ApproximateLowPrecision = 0.000001f; + private const float k_ApproximateHighPrecision = 1E-10f; private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float GetPrecision() + { + return m_BufferQueue.Count == 0 ? k_ApproximateHighPrecision : k_ApproximateLowPrecision; + } + #region Legacy notes // Buffer consumption scenarios // Perfect case consumption @@ -132,6 +140,7 @@ internal struct CurrentState public float CurrentDeltaTime => m_CurrentDeltaTime; public double FinalTimeToTarget => Math.Max(0.0, TimeToTargetValue - DeltaTime); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddDeltaTime(float deltaTime) { m_CurrentDeltaTime = deltaTime; @@ -139,6 +148,7 @@ public void AddDeltaTime(float deltaTime) LerpT = (float)(TimeToTargetValue == 0.0 ? 1.0 : DeltaTime / TimeToTargetValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetTimeToTarget(double timeToTarget) { LerpT = 0.0f; @@ -146,6 +156,7 @@ public void SetTimeToTarget(double timeToTarget) TimeToTargetValue = timeToTarget; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TargetTimeAproximatelyReached() { if (!Target.HasValue) @@ -188,6 +199,7 @@ public void Reset(T currentValue) /// The current buffered items received by the authority. /// protected internal readonly Queue m_BufferQueue = new Queue(k_BufferCountLimit); + protected internal readonly List m_BufferList = new List(k_BufferCountLimit); /// /// The current interpolation state @@ -237,6 +249,7 @@ public void ResetTo(T targetValue, double serverTime) InternalReset(targetValue, serverTime); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void InternalReset(T targetValue, double serverTime, bool addMeasurement = true) { m_RateOfChange = default; @@ -271,7 +284,7 @@ private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double { if (!InterpolateState.TargetReached) { - InterpolateState.TargetReached = IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item); + InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); } return; } @@ -291,7 +304,7 @@ private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double potentialItemNeedsProcessing = ((potentialItem.TimeSent <= renderTime) && potentialItem.TimeSent > InterpolateState.Target.Value.TimeSent); if (!InterpolateState.TargetReached) { - InterpolateState.TargetReached = IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item); + InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); } } @@ -424,23 +437,27 @@ internal T Update(float deltaTime, double tickLatencyAsTime, double minDeltaTime /// This version of TryConsumeFromBuffer adheres to the original BufferedLinearInterpolator buffer consumption pattern. /// /// + /// private void TryConsumeFromBuffer(double renderTime, double serverTime) { if (!InterpolateState.Target.HasValue || (InterpolateState.Target.Value.TimeSent <= renderTime)) { BufferedItem? previousItem = null; var alreadyHasBufferItem = false; + var count = 0; while (m_BufferQueue.TryPeek(out BufferedItem potentialItem)) { // If we are still on the same buffered item (FIFO Queue), then exit early as there is nothing - // to consume. + // to consume. (just a safety check but this scenario should never happen based on the below legacy approach of + // consuming until the most current state) if (previousItem.HasValue && previousItem.Value.TimeSent == potentialItem.TimeSent) { break; } - if ((potentialItem.TimeSent <= serverTime) && - (!InterpolateState.Target.HasValue || InterpolateState.Target.Value.TimeSent < potentialItem.TimeSent)) + // Continue to processing until we reach the most current state + if ((potentialItem.TimeSent <= serverTime) && // Inverted logic (below) from original since we have to go from past to present + (!InterpolateState.Target.HasValue || potentialItem.TimeSent > InterpolateState.Target.Value.TimeSent)) { if (m_BufferQueue.TryDequeue(out BufferedItem target)) { @@ -449,6 +466,7 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) InterpolateState.Target = target; alreadyHasBufferItem = true; InterpolateState.NextValue = InterpolateState.CurrentValue; + InterpolateState.PreviousValue = InterpolateState.CurrentValue; InterpolateState.StartTime = target.TimeSent; InterpolateState.EndTime = target.TimeSent; } @@ -458,19 +476,15 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) { alreadyHasBufferItem = true; InterpolateState.StartTime = InterpolateState.Target.Value.TimeSent; - InterpolateState.NextValue = InterpolateState.CurrentValue; + InterpolateState.PreviousValue = InterpolateState.NextValue; InterpolateState.TargetReached = false; } InterpolateState.EndTime = target.TimeSent; - InterpolateState.Target = target; InterpolateState.TimeToTargetValue = InterpolateState.EndTime - InterpolateState.StartTime; + InterpolateState.Target = target; } } } - else - { - break; - } if (!InterpolateState.Target.HasValue) { @@ -505,19 +519,20 @@ public T Update(float deltaTime, double renderTime, double serverTime) InterpolateState.LerpT = Math.Clamp((float)((renderTime - InterpolateState.StartTime) / InterpolateState.TimeToTargetValue), 0.0f, 1.0f); } - var target = Interpolate(InterpolateState.NextValue, InterpolateState.Target.Value.Item, InterpolateState.LerpT); + InterpolateState.NextValue = Interpolate(InterpolateState.PreviousValue, InterpolateState.Target.Value.Item, InterpolateState.LerpT); if (LerpSmoothEnabled) { // Assure our MaximumInterpolationTime is valid and that the second lerp time ranges between deltaTime and 1.0f. - InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, target, deltaTime / MaximumInterpolationTime); + InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, InterpolateState.NextValue, deltaTime / MaximumInterpolationTime); } else { - InterpolateState.CurrentValue = target; + InterpolateState.CurrentValue = InterpolateState.NextValue; } + // Determine if we have reached our target - InterpolateState.TargetReached = IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item); + InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); } else // If the target is reached and we have no more state updates, we want to check to see if we need to reset. if (m_BufferQueue.Count == 0 && InterpolateState.TargetReached) @@ -601,6 +616,7 @@ public void AddMeasurement(T newMeasurement, double sentTime) /// Gets latest value from the interpolator. This is updated every update as time goes by. /// /// The current interpolated value of type 'T' + [MethodImpl(MethodImplOptions.AggressiveInlining)] public T GetInterpolatedValue() { return InterpolateState.CurrentValue; @@ -638,6 +654,7 @@ public T GetInterpolatedValue() /// The increasing delta time from when start to finish. /// Maximum rate of change per pass. /// The smoothed value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected virtual T SmoothDamp(T current, T target, ref T rateOfChange, float duration, float deltaTime, float maxSpeed = Mathf.Infinity) { return target; @@ -653,7 +670,8 @@ private protected virtual T SmoothDamp(T current, T target, ref T rateOfChange, /// Second value of type . /// The precision of the aproximation. /// true if the two values are aproximately the same and false if they are not - private protected virtual bool IsAproximately(T first, T second, float precision = k_AproximatePrecision) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected virtual bool IsApproximately(T first, T second, float precision = k_ApproximateLowPrecision) { return false; } @@ -665,6 +683,7 @@ private protected virtual bool IsAproximately(T first, T second, float precision /// The item to convert. /// local or world space (true or false). /// The converted value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal virtual T OnConvertTransformSpace(Transform transform, T item, bool inLocalSpace) { return default; @@ -675,6 +694,7 @@ protected internal virtual T OnConvertTransformSpace(Transform transform, T item /// /// The transform that the is associated with. /// Whether the is now being tracked in local or world spaced. + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ConvertTransformSpace(Transform transform, bool inLocalSpace) { var count = m_BufferQueue.Count; diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs index 77583468a7..654d64917a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs @@ -25,7 +25,7 @@ protected override float Interpolate(float start, float end, float time) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected override bool IsAproximately(float first, float second, float precision = 1E-07F) + private protected override bool IsApproximately(float first, float second, float precision = 1E-06F) { return Mathf.Approximately(first, second); } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs index 5b8033a977..628498ada0 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs @@ -66,7 +66,7 @@ private protected override Quaternion SmoothDamp(Quaternion current, Quaternion /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected override bool IsAproximately(Quaternion first, Quaternion second, float precision) + private protected override bool IsApproximately(Quaternion first, Quaternion second, float precision = 1E-06F) { return Mathf.Abs(first.x - second.x) <= precision && Mathf.Abs(first.y - second.y) <= precision && diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs index aa5a739683..ce836bc199 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs @@ -59,7 +59,7 @@ protected internal override Vector3 OnConvertTransformSpace(Transform transform, /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected override bool IsAproximately(Vector3 first, Vector3 second, float precision = 0.0001F) + private protected override bool IsApproximately(Vector3 first, Vector3 second, float precision = 1E-06F) { return Math.Round(Mathf.Abs(first.x - second.x), 2) <= precision && Math.Round(Mathf.Abs(first.y - second.y), 2) <= precision && diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index aeb9687e7b..92943ee0b5 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -968,6 +968,8 @@ public enum InterpolationTypes /// The first phase lerps from the previous state update value to the next state update value. /// The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a rate of delta time divided by the respective max interpolation time. /// + /// !!! NOTE !!!
+ /// The legacy lerp interpolation type does not use to determine the buffer depth. This is to preserve the same interpolation results when lerp smoothing is enabled.
/// /// /// For more information:
@@ -4085,6 +4087,17 @@ private void UpdateInterpolation() } } + // Note: This is for the legacy lerp type in order to maintain the same end result for any games under development that have tuned their + // project's to match the legacy lerp's end result. This will not allow changes + var cachedRenderTime = 0.0; + if (PositionInterpolationType == InterpolationTypes.LegacyLerp || RotationInterpolationType == InterpolationTypes.LegacyLerp || ScaleInterpolationType == InterpolationTypes.LegacyLerp) + { + // Since InterpolationBufferTickOffset defaults to zero, this should not impact exist projects but still provides users with the ability to tweak + // their ticks ago time. + var ticksAgo = (!IsServerAuthoritative() && !IsServer ? 2 : 1) + InterpolationBufferTickOffset; + cachedRenderTime = timeSystem.TimeTicksAgo(ticksAgo).Time; + } + // Get the tick latency (ticks ago) as time (in the past) to process state updates in the queue. var tickLatencyAsTime = timeSystem.TimeTicksAgo(tickLatency).Time; @@ -4127,7 +4140,7 @@ private void UpdateInterpolation() if (PositionInterpolationType == InterpolationTypes.LegacyLerp) { - m_PositionInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, currentTime); + m_PositionInterpolator.Update(cachedDeltaTime, cachedRenderTime, currentTime); } else { @@ -4158,7 +4171,7 @@ private void UpdateInterpolation() m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision; if (RotationInterpolationType == InterpolationTypes.LegacyLerp) { - m_RotationInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, currentTime); + m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, currentTime); } else { @@ -4186,7 +4199,7 @@ private void UpdateInterpolation() if (ScaleInterpolationType == InterpolationTypes.LegacyLerp) { - m_ScaleInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, currentTime); + m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, currentTime); } else { From 717765e1b3e47ba6546192c6e6f0da4e0b982892 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 3 Apr 2025 21:10:27 -0500 Subject: [PATCH 2/3] update removing unused list from earlier walk through with emma. --- .../Components/Interpolator/BufferedLinearInterpolator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index f168ab2372..571f3fe0bd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -199,7 +199,6 @@ public void Reset(T currentValue) /// The current buffered items received by the authority. /// protected internal readonly Queue m_BufferQueue = new Queue(k_BufferCountLimit); - protected internal readonly List m_BufferList = new List(k_BufferCountLimit); /// /// The current interpolation state From 361cbe3a7d85b8a175f9a41ccc7c4fc1fc0f8dba Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 3 Apr 2025 22:52:57 -0500 Subject: [PATCH 3/3] style removing unused variable --- .../Components/Interpolator/BufferedLinearInterpolator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index 571f3fe0bd..b07eca8250 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -443,7 +443,6 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) { BufferedItem? previousItem = null; var alreadyHasBufferItem = false; - var count = 0; while (m_BufferQueue.TryPeek(out BufferedItem potentialItem)) { // If we are still on the same buffered item (FIFO Queue), then exit early as there is nothing