Skip to content
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed

- Fixed issue where scenes additively loaded before a session started were tracked as loaded on the server but had no scene handle entries, which caused `NetworkSceneManager.UnloadScene` to log an error and leave the scene registered as loaded even though it unloaded on all peers. (#4146)
- Issue where lerp smoothing was applied per frame instead of over time, which caused the `Lerp` and `SmoothDampening` interpolation types to smooth by different amounts at different frame rates. Results at 60fps are unchanged. (#4132)
- Issue where setting a maximum interpolation time of 1.0 would stop a `NetworkTransform` from interpolating at all when using the `Lerp` or `SmoothDampening` interpolation types. (#4132)
- Issue with not being able to spawn initially disabled in-scene placed objects. (#4093)
- Issue with pre-instantiated network prefab instances being marked as in-scene placed. Now pre-instantiated network prefabs are dynamically spawned. (#4093)
- Issue where a user could spawn runtime created `NetworkObject` that has a GlobalObjectIdHash of zero. These are not valid instances and will no longer be allowed to spawn. (#4093)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,23 @@ public void Reset(T currentValue)
internal bool LerpSmoothEnabled;

/// <summary>
/// Determines how much smoothing will be applied to the 2nd lerp when using the <see cref="Update(float, double, double)"/> (i.e. lerping and not smooth dampening).
/// The frame rate that <see cref="MaximumInterpolationTime"/> is relative to when lerp smoothing.
/// </summary>
private const float k_LerpSmoothReferenceFrameRate = 60.0f;

/// <summary>
/// Keeps a <see cref="MaximumInterpolationTime"/> of 1.0f from retaining the entire delta each frame,
/// which would stop the value from ever advancing towards the target.
/// </summary>
private const float k_MaximumLerpSmoothRetention = 0.99f;

/// <summary>
/// Determines how much smoothing will be applied to the 2nd lerp.
/// </summary>
/// <remarks>
/// There's two factors affecting interpolation: <br />
/// - Buffering: Which can be adjusted in set in the <see cref="NetworkManager.NetworkTimeSystem"/>.<br />
/// - Interpolation time: The divisor applied to delta time where the quotient is used as the lerp time.
/// Higher values are smoother, lower values are more precise. The amount of smoothing applied is
/// frame rate independent.<br />
/// Buffering also affects interpolation and can be adjusted via <see cref="NetworkManager.NetworkTimeSystem"/>.
/// </remarks>
[Range(0.016f, 1.0f)]
public float MaximumInterpolationTime = 0.1f;
Expand Down Expand Up @@ -420,6 +431,26 @@ internal void ResetCurrentState()
}
}

/// <summary>
/// Calculates the frame rate independent lerp smoothing "t" for the current frame.
/// </summary>
/// <remarks>
/// Raising the retained portion to the number of reference frames elapsed makes the smoothing rate
/// a function of elapsed time rather than of how often this is called.
/// </remarks>
/// <param name="deltaTime">The last frame time.</param>
/// <returns>The lerp smoothing time to apply for this frame.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float GetLerpSmoothTime(float deltaTime)
{
var retained = Mathf.Clamp01(MaximumInterpolationTime);
if (retained >= 1.0f)
{
retained = k_MaximumLerpSmoothRetention;
}
return 1.0f - Mathf.Pow(retained, deltaTime * k_LerpSmoothReferenceFrameRate);
}

/// <summary>
/// Interpolation Update to use when smooth dampening is enabled on a <see cref="Components.NetworkTransform"/>.
/// </summary>
Expand Down Expand Up @@ -459,7 +490,7 @@ internal T Update(float deltaTime, double tickLatencyAsTime, double minDeltaTime
if (LerpSmoothEnabled)
{
// Apply the smooth lerp to the target to help smooth the final value.
InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, InterpolateState.NextValue, Mathf.Clamp(1.0f - MaximumInterpolationTime, 0.0f, 1.0f));
InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, InterpolateState.NextValue, GetLerpSmoothTime(deltaTime));
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,7 @@ public enum InterpolationTypes
/// Uses a 1 to 2 phase interpolation approach where:<br />
/// <list type="bullet">
/// <item><description>The first phase lerps from the previous state update value to the next state update value.</description></item>
/// <item><description>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 1.0 minus the respective maximum interpolation time.</description></item>
/// <item><description>The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a frame rate independent rate determined by the respective maximum interpolation time.</description></item>
/// </list>
/// </summary>
/// <remarks>
Expand All @@ -1156,7 +1156,7 @@ public enum InterpolationTypes
/// Uses a 1 to 2 phase smooth dampening approach where:<br />
/// <list type="bullet">
/// <item><description>The first phase smooth dampens towards the current tick state update being processed by the accumulated delta time relative to the time to target.</description></item>
/// <item><description>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.</description></item>
/// <item><description>The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a frame rate independent rate determined by the respective maximum interpolation time.</description></item>
/// </list>
/// </summary>
/// <remarks>
Expand Down Expand Up @@ -1236,7 +1236,11 @@ public enum InterpolationTypes
/// Controls position interpolation smoothing.
/// </summary>
/// <remarks>
/// When enabled, the <see cref="BufferedLinearInterpolator{T}"/> will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the <see cref="PositionMaxInterpolationTime"/>.
/// When enabled, the <see cref="BufferedLinearInterpolator{T}"/> will apply a final lerping pass towards
/// the interpolated result at a rate determined by <see cref="PositionMaxInterpolationTime"/>.<br />
/// This smoothing pass is frame rate independent under <see cref="InterpolationTypes.Lerp"/> and
/// <see cref="InterpolationTypes.SmoothDampening"/>. <see cref="InterpolationTypes.LegacyLerp"/> keeps its
/// original frame rate dependent smoothing, so the same value does not produce the same result there.
/// </remarks>
public bool PositionLerpSmoothing = true;
private bool m_PreviousPositionLerpSmoothing;
Expand All @@ -1257,7 +1261,11 @@ public enum InterpolationTypes
/// Controls rotation interpolation smoothing.
/// </summary>
/// <remarks>
/// When enabled, the <see cref="BufferedLinearInterpolator{T}"/> will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the <see cref="RotationMaxInterpolationTime"/>.
/// When enabled, the <see cref="BufferedLinearInterpolator{T}"/> will apply a final lerping pass towards
/// the interpolated result at a rate determined by <see cref="RotationMaxInterpolationTime"/>.<br />
/// This smoothing pass is frame rate independent under <see cref="InterpolationTypes.Lerp"/> and
/// <see cref="InterpolationTypes.SmoothDampening"/>. <see cref="InterpolationTypes.LegacyLerp"/> keeps its
/// original frame rate dependent smoothing, so the same value does not produce the same result there.
/// </remarks>
public bool RotationLerpSmoothing = true;
private bool m_PreviousRotationLerpSmoothing;
Expand All @@ -1278,7 +1286,11 @@ public enum InterpolationTypes
/// Controls scale interpolation smoothing.
/// </summary>
/// <remarks>
/// When enabled, the <see cref="BufferedLinearInterpolator{T}"/> will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the <see cref="ScaleMaxInterpolationTime"/>.
/// When enabled, the <see cref="BufferedLinearInterpolator{T}"/> will apply a final lerping pass towards
/// the interpolated result at a rate determined by <see cref="ScaleMaxInterpolationTime"/>.<br />
/// This smoothing pass is frame rate independent under <see cref="InterpolationTypes.Lerp"/> and
/// <see cref="InterpolationTypes.SmoothDampening"/>. <see cref="InterpolationTypes.LegacyLerp"/> keeps its
/// original frame rate dependent smoothing, so the same value does not produce the same result there.
/// </remarks>
public bool ScaleLerpSmoothing = true;
private bool m_PreviousScaleLerpSmoothing;
Expand Down
98 changes: 98 additions & 0 deletions com.unity.netcode.gameobjects/Tests/Editor/InterpolatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,5 +301,103 @@ public void TestDuplicatedValues()
Assert.That(interp, Is.EqualTo(2f));
// Since there is no extrapolation, the rest of this test was removed.
}

#region Lerp Smoothing

// Deliberately not round numbers, so exactly representable values cannot mask a defect.
private const double k_SmoothTickInterval = 1.0d / 30.0d;
private const int k_SmoothTickLatency = 2;
private const float k_SmoothStartValue = 3.17f;
private const float k_SmoothVelocity = 2.3f;
private const double k_SmoothMoveDuration = 1.53d;
private const double k_SmoothTotalDuration = 2.11d;

/// <summary>
/// Drives the lerp and smooth dampening interpolation path with lerp smoothing enabled, where an
/// authority moves at a constant velocity and then holds still while the non-authority renders at
/// <paramref name="frameDeltaTime"/>.
/// </summary>
/// <returns>The interpolated value once <see cref="k_SmoothTotalDuration"/> has elapsed.</returns>
private float RunLerpSmoothing(float maximumInterpolationTime, float frameDeltaTime, bool lerp)
{
var interpolator = new BufferedLinearInterpolatorFloat
{
MaximumInterpolationTime = maximumInterpolationTime,
LerpSmoothEnabled = true,
};
interpolator.ResetTo(k_SmoothStartValue, 0.0d);

var restValue = k_SmoothStartValue + (float)(k_SmoothVelocity * k_SmoothMoveDuration);
var maxDeltaTime = k_SmoothTickLatency * k_SmoothTickInterval;
var nextTick = 1;
var currentValue = k_SmoothStartValue;

for (var time = 0.0d; time < k_SmoothTotalDuration; time += frameDeltaTime)
{
// Deliver every state update whose send time has already passed.
while (nextTick * k_SmoothTickInterval <= time)
{
var sentTime = nextTick * k_SmoothTickInterval;
var sentValue = sentTime <= k_SmoothMoveDuration
? k_SmoothStartValue + (float)(k_SmoothVelocity * sentTime)
: restValue;
interpolator.AddMeasurement(sentValue, sentTime);
nextTick++;
}

currentValue = interpolator.Update(frameDeltaTime, time - maxDeltaTime, k_SmoothTickInterval, maxDeltaTime, lerp);
}

return currentValue;
}

/// <summary>
/// Lerp smoothing must still advance the value at 1.0f, the maximum legal value of the
/// <see cref="Components.NetworkTransform.PositionMaxInterpolationTime"/> family of fields.
/// </summary>
[Test]
public void LerpSmoothingDoesNotFreezeAtMaximumInterpolationTime([Values] bool lerp)
{
var result = RunLerpSmoothing(1.0f, 1.0f / 60.0f, lerp);

Assert.That(result, Is.GreaterThan(k_SmoothStartValue + 1.0f),
$"Interpolated value only advanced {result - k_SmoothStartValue} from {k_SmoothStartValue} over " +
$"{k_SmoothTotalDuration}s of authority motion. The maximum interpolation time froze the transform.");
}

/// <summary>
/// The rate at which lerp smoothing converges must not depend on the frame rate.
/// </summary>
[Test]
public void LerpSmoothingIsFrameRateIndependent()
{
// Heavier than the default, where the frame rate dependency is measurable.
const float maximumInterpolationTime = 0.87f;

var atThirtyFps = RunLerpSmoothing(maximumInterpolationTime, 1.0f / 30.0f, true);
var atTwoFortyFps = RunLerpSmoothing(maximumInterpolationTime, 1.0f / 240.0f, true);

Assert.That(atThirtyFps, Is.EqualTo(atTwoFortyFps).Within(0.01f),
$"The same elapsed time and interpolation settings produced {atThirtyFps} at 30fps but " +
$"{atTwoFortyFps} at 240fps. The smoothing rate is scaling with the frame rate.");
}

/// <summary>
/// Only 1.0f is substituted for, so settings below it keep their own smoothing rate and a heavier
/// setting stays smoother than a lighter one.
/// </summary>
[Test]
public void LerpSmoothingPreservesSettingsBelowTheMaximum()
{
// 0.99f is the retention substituted for 1.0f, so clamping to it would collapse these two.
var lighter = RunLerpSmoothing(0.99f, 1.0f / 60.0f, true);
var heavier = RunLerpSmoothing(0.995f, 1.0f / 60.0f, true);

Assert.That(heavier, Is.LessThan(lighter),
$"0.995 converged to {heavier} and 0.99 to {lighter} over the same motion. A higher maximum " +
"interpolation time has to retain more of the previous value, so it cannot converge first.");
}

#endregion
}
}