diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 73ed64a735..74718c95b9 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -11,6 +11,9 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added - Added `NetworkManager.OnPreShutdown` which is called before the NetworkManager cleans up and shuts down. (#3366) +- Added `Lerp` interpolation type that still uses a lerp approach but uses the new buffer consumption logic. (#3355) +- Added property to enable or disable lerp smoothing for position, rotation, and scale interpolators. (#3355) +- Added `NetworkTransform.InterpolationBufferTickOffset` static property to provide users with a way to increase or decrease the time marker where interpolators will pull state update from the queue. (#3355) - Added interpolator types as an inspector view selection for position, rotation, and scale. (#3337) - Added a new smooth dampening interpolator type that provides a nice balance between precision and smoothing results. (#3337) - Added `NetworkTimeSystem.TickLatency` property that provides the average latency of a client. (#3337) @@ -19,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where the time delta that interpolators used would not be properly updated during multiple fixed update invocations within the same player loop frame. (#3355) - Fixed issue when using a distributed authority network topology and many clients attempt to connect simultaneously the session owner could max-out the maximum in-flight reliable messages allowed, start dropping packets, and some of the connecting clients would fail to fully synchronize. (#3350) - Fixed issue when using a distributed authority network topology and scene management was disabled clients would not be able to spawn any new network prefab instances until synchronization was complete. (#3350) - Fixed issue where an owner that changes ownership, when using a distributed authority network topology, could yield identical previous and current owner identifiers. This could also cause `NetworkTransform` to fail to change ownership which would leave the previous owner still subscribed to network tick events. (#3347) @@ -39,8 +43,9 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Changed +- Changed the original `Lerp` interpolation type to `LegacyLerp`. (#3355) - Changed `BufferedLinearInterpolator.Update(float deltaTime, NetworkTime serverTime)` as being deprecated since this method is only used for internal testing purposes. (#3337) -- Ensured that a useful error is thrown when attempting to build a dedicated server with Unity Transport that uses websockets. (#3336) +- Changed error thrown when attempting to build a dedicated server with Unity Transport that uses websockets to provide more useful information to the user. (#3336) - Changed root in-scene placed `NetworkObject` instances now will always have either the `Distributable` permission set unless the `SessionOwner` permission is set. (#3305) - Changed the `DestroyObject` message to reduce the serialized message size and remove the unnecessary message field. (#3304) - Changed the `NetworkTimeSystem.Sync` method to use half RTT to calculate the desired local time offset as opposed to the full RTT. (#3212) diff --git a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs index b04ad3f724..53d09b782f 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs @@ -31,6 +31,9 @@ public class NetworkTransformEditor : NetcodeEditorBase private SerializedProperty m_PositionInterpolationTypeProperty; private SerializedProperty m_RotationInterpolationTypeProperty; private SerializedProperty m_ScaleInterpolationTypeProperty; + private SerializedProperty m_PositionLerpSmoothing; + private SerializedProperty m_RotationLerpSmoothing; + private SerializedProperty m_ScaleLerpSmoothing; private SerializedProperty m_PositionMaximumInterpolationTimeProperty; private SerializedProperty m_RotationMaximumInterpolationTimeProperty; @@ -77,6 +80,11 @@ public override void OnEnable() m_ScaleInterpolationTypeProperty = serializedObject.FindProperty(nameof(NetworkTransform.ScaleInterpolationType)); m_ScaleMaximumInterpolationTimeProperty = serializedObject.FindProperty(nameof(NetworkTransform.ScaleMaxInterpolationTime)); + m_PositionLerpSmoothing = serializedObject.FindProperty(nameof(NetworkTransform.PositionLerpSmoothing)); + m_RotationLerpSmoothing = serializedObject.FindProperty(nameof(NetworkTransform.RotationLerpSmoothing)); + m_ScaleLerpSmoothing = serializedObject.FindProperty(nameof(NetworkTransform.ScaleLerpSmoothing)); + + m_UseQuaternionSynchronization = serializedObject.FindProperty(nameof(NetworkTransform.UseQuaternionSynchronization)); m_UseQuaternionCompression = serializedObject.FindProperty(nameof(NetworkTransform.UseQuaternionCompression)); @@ -198,36 +206,42 @@ private void DisplayNetworkTransformProperties() if (networkTransform.SynchronizePosition) { DrawPropertyField(m_PositionInterpolationTypeProperty); - // Only display when using Lerp. - if (networkTransform.PositionInterpolationType == NetworkTransform.InterpolationTypes.Lerp) + + BeginIndent(); + if (networkTransform.PositionInterpolationType != NetworkTransform.InterpolationTypes.SmoothDampening) { - BeginIndent(); DrawPropertyField(m_SlerpPosition); + } + DrawPropertyField(m_PositionLerpSmoothing); + if (networkTransform.PositionLerpSmoothing) + { DrawPropertyField(m_PositionMaximumInterpolationTimeProperty); - EndIndent(); } + EndIndent(); } if (networkTransform.SynchronizeRotation) { DrawPropertyField(m_RotationInterpolationTypeProperty); - // Only display when using Lerp. - if (networkTransform.RotationInterpolationType == NetworkTransform.InterpolationTypes.Lerp) + + BeginIndent(); + DrawPropertyField(m_RotationLerpSmoothing); + if (networkTransform.RotationLerpSmoothing) { - BeginIndent(); DrawPropertyField(m_RotationMaximumInterpolationTimeProperty); - EndIndent(); } + EndIndent(); } if (networkTransform.SynchronizeScale) { DrawPropertyField(m_ScaleInterpolationTypeProperty); - // Only display when using Lerp. - if (networkTransform.ScaleInterpolationType == NetworkTransform.InterpolationTypes.Lerp) + + BeginIndent(); + DrawPropertyField(m_ScaleLerpSmoothing); + if (networkTransform.ScaleLerpSmoothing) { - BeginIndent(); DrawPropertyField(m_ScaleMaximumInterpolationTimeProperty); - EndIndent(); } + EndIndent(); } EndIndent(); EditorGUILayout.Space(); diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index 0aabd0226e..538a6ba1a1 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -115,40 +115,35 @@ public BufferedItem(T item, double timeSent, int itemId) internal struct CurrentState { public BufferedItem? Target; - public double StartTime; public double EndTime; - public float TimeToTargetValue; - public float DeltaTime; + public double TimeToTargetValue; + public double DeltaTime; + public double MaxDeltaTime; + public double LastRemainingTime; public float LerpT; - + public bool TargetReached; public T CurrentValue; public T PreviousValue; + public T NextValue; - private float m_AverageDeltaTime; + private float m_CurrentDeltaTime; - public float AverageDeltaTime => m_AverageDeltaTime; - public float FinalTimeToTarget => TimeToTargetValue - DeltaTime; + public float CurrentDeltaTime => m_CurrentDeltaTime; + public double FinalTimeToTarget => Math.Max(0.0, TimeToTargetValue - DeltaTime); public void AddDeltaTime(float deltaTime) { - if (m_AverageDeltaTime == 0.0f) - { - m_AverageDeltaTime = deltaTime; - } - else - { - m_AverageDeltaTime += deltaTime; - m_AverageDeltaTime *= 0.5f; - } - DeltaTime = Math.Min(DeltaTime + m_AverageDeltaTime, TimeToTargetValue); - LerpT = TimeToTargetValue == 0.0f ? 1.0f : DeltaTime / TimeToTargetValue; + m_CurrentDeltaTime = deltaTime; + DeltaTime = Math.Min(DeltaTime + deltaTime, TimeToTargetValue); + LerpT = (float)(TimeToTargetValue == 0.0 ? 1.0 : DeltaTime / TimeToTargetValue); } - public void ResetDelta() + public void SetTimeToTarget(double timeToTarget) { - m_AverageDeltaTime = 0.0f; + LerpT = 0.0f; DeltaTime = 0.0f; + TimeToTargetValue = timeToTarget; } public bool TargetTimeAproximatelyReached() @@ -157,22 +152,27 @@ public bool TargetTimeAproximatelyReached() { return false; } - return m_AverageDeltaTime >= FinalTimeToTarget; + return FinalTimeToTarget <= m_CurrentDeltaTime * 0.5f; } public void Reset(T currentValue) { Target = null; CurrentValue = currentValue; + NextValue = currentValue; PreviousValue = currentValue; - // When reset, we consider ourselves to have already arrived at the target (even if no target is set) + TargetReached = false; LerpT = 0.0f; EndTime = 0.0; StartTime = 0.0; - ResetDelta(); + TimeToTargetValue = 0.0f; + DeltaTime = 0.0f; + m_CurrentDeltaTime = 0.0f; } } + internal bool LerpSmoothEnabled; + /// /// Determines how much smoothing will be applied to the 2nd lerp when using the (i.e. lerping and not smooth dampening). /// @@ -210,16 +210,6 @@ public void Reset(T currentValue) /// private T m_RateOfChange; - /// - /// Represents the predicted rate of change for the value being interpolated when smooth dampening is enabled. - /// - private T m_PredictedRateOfChange; - - /// - /// When true, the value is an angular numeric representation. - /// - private protected bool m_IsAngularValue; - /// /// Resets interpolator to the defaults. /// @@ -230,7 +220,6 @@ public void Clear() m_LastMeasurementAddedTime = 0.0; InterpolateState.Reset(default); m_RateOfChange = default; - m_PredictedRateOfChange = default; } /// @@ -241,21 +230,18 @@ public void Clear() /// /// The target value to reset the interpolator to /// The current server time - /// When rotation is expressed as Euler values (i.e. Vector3 and/or float) this helps determine what kind of smooth dampening to use. - public void ResetTo(T targetValue, double serverTime, bool isAngularValue = false) + public void ResetTo(T targetValue, double serverTime) { // Clear the interpolator Clear(); - InternalReset(targetValue, serverTime, isAngularValue); + InternalReset(targetValue, serverTime); } - private void InternalReset(T targetValue, double serverTime, bool isAngularValue = false, bool addMeasurement = true) + private void InternalReset(T targetValue, double serverTime, bool addMeasurement = true) { m_RateOfChange = default; - m_PredictedRateOfChange = default; // Set our initial value InterpolateState.Reset(targetValue); - m_IsAngularValue = isAngularValue; if (addMeasurement) { @@ -264,78 +250,102 @@ private void InternalReset(T targetValue, double serverTime, bool isAngularValue } } - #region Smooth Dampening Interpolation + #region Smooth Dampening and Lerp Ahead Interpolation Handling /// - /// TryConsumeFromBuffer: Smooth Dampening Version + /// TryConsumeFromBuffer: Smooth Dampening and Lerp Ahead Version /// - /// render time: the time in "ticks ago" relative to the current tick latency - /// minimum time delta (defaults to tick frequency) - /// maximum time delta which defines the maximum time duration when consuming more than one item from the buffer - private void TryConsumeFromBuffer(double renderTime, float minDeltaTime, float maxDeltaTime) + /// render time: the time in "ticks ago" relative to the current tick latency. + /// minimum time delta (defaults to tick frequency). + /// maximum time delta which defines the maximum time duration when consuming more than one item from the buffer. + /// when true, the predicted target will lerp towards the next target by the current delta. + private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double maxDeltaTime) { - if (!InterpolateState.Target.HasValue || (InterpolateState.Target.Value.TimeSent <= renderTime - && (InterpolateState.TargetTimeAproximatelyReached() || IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item)))) + BufferedItem? previousItem = null; + var startTime = 0.0; + var alreadyHasBufferItem = false; + var noStateSet = !InterpolateState.Target.HasValue; + var potentialItemNeedsProcessing = false; + + // In the event there is nothing left in the queue (i.e. motion/change stopped), we still need to determine if the target has been reached. + if (!noStateSet && m_BufferQueue.Count == 0) { - BufferedItem? previousItem = null; - var startTime = 0.0; - var alreadyHasBufferItem = false; - while (m_BufferQueue.TryPeek(out BufferedItem potentialItem)) + if (!InterpolateState.TargetReached) { - // If we are still on the same buffered item (FIFO Queue), then exit early as there is nothing - // to consume. - if (previousItem.HasValue && previousItem.Value.TimeSent == potentialItem.TimeSent) + InterpolateState.TargetReached = IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item); + } + return; + } + + // Continue to process any remaining state updates in the queue (if any) + 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. + if (previousItem.HasValue && previousItem.Value.TimeSent == potentialItem.TimeSent) + { + break; + } + + if (!noStateSet) + { + potentialItemNeedsProcessing = ((potentialItem.TimeSent <= renderTime) && potentialItem.TimeSent > InterpolateState.Target.Value.TimeSent); + if (!InterpolateState.TargetReached) { - break; + InterpolateState.TargetReached = IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item); } + } - // If we haven't set a target or the potential item's time sent is less that the current target's time sent - // then pull the BufferedItem from the queue. The second portion of this accounts for scenarios where there - // was bad latency and the buffer has more than one item in the queue that is less than the renderTime. Under - // this scenario, we just want to continue pulling items from the queue until the last item pulled from the - // queue is greater than the redner time or greater than the currently targeted item. - if (!InterpolateState.Target.HasValue || - ((potentialItem.TimeSent <= renderTime) && InterpolateState.Target.Value.TimeSent <= potentialItem.TimeSent)) + // If we haven't set a target or we have another item that needs processing. + if (noStateSet || potentialItemNeedsProcessing) + { + if (m_BufferQueue.TryDequeue(out BufferedItem target)) { - if (m_BufferQueue.TryDequeue(out BufferedItem target)) + if (!InterpolateState.Target.HasValue) { - if (!InterpolateState.Target.HasValue) + InterpolateState.Target = target; + alreadyHasBufferItem = true; + InterpolateState.NextValue = InterpolateState.CurrentValue; + InterpolateState.PreviousValue = InterpolateState.CurrentValue; + InterpolateState.SetTimeToTarget(minDeltaTime); + startTime = InterpolateState.Target.Value.TimeSent; + InterpolateState.TargetReached = false; + InterpolateState.MaxDeltaTime = maxDeltaTime; + } + else + { + if (!alreadyHasBufferItem) { - InterpolateState.Target = target; - alreadyHasBufferItem = true; - InterpolateState.PreviousValue = InterpolateState.CurrentValue; - InterpolateState.TimeToTargetValue = minDeltaTime; + InterpolateState.LastRemainingTime = InterpolateState.FinalTimeToTarget; + InterpolateState.TargetReached = false; + InterpolateState.MaxDeltaTime = maxDeltaTime; + InterpolateState.PreviousValue = InterpolateState.NextValue; startTime = InterpolateState.Target.Value.TimeSent; } - else - { - if (!alreadyHasBufferItem) - { - alreadyHasBufferItem = true; - startTime = InterpolateState.Target.Value.TimeSent; - InterpolateState.PreviousValue = InterpolateState.CurrentValue; - InterpolateState.LerpT = 0.0f; - } - // TODO: We might consider creating yet another queue to add these items to and assure that the time is accelerated - // for each item as opposed to losing the resolution of the values. - InterpolateState.TimeToTargetValue = Mathf.Clamp((float)(target.TimeSent - startTime), minDeltaTime, maxDeltaTime); - - InterpolateState.Target = target; - } - InterpolateState.ResetDelta(); + InterpolateState.SetTimeToTarget(Math.Max(target.TimeSent - startTime, minDeltaTime)); + InterpolateState.Target = target; } } - else - { - break; - } + } + else + { + break; + } - if (!InterpolateState.Target.HasValue) - { - break; - } - previousItem = potentialItem; + if (!InterpolateState.Target.HasValue) + { + break; } + previousItem = potentialItem; + } + } + + internal void ResetCurrentState() + { + if (InterpolateState.Target.HasValue) + { + InterpolateState.Reset(InterpolateState.CurrentValue); + m_RateOfChange = default; } } @@ -350,23 +360,56 @@ private void TryConsumeFromBuffer(double renderTime, float minDeltaTime, float m /// The tick latency in relative local time. /// The minimum time delta between the current and target value. /// The maximum time delta between the current and target value. + /// Determines whether to use smooth dampening or lerp interpolation type. /// The newly interpolated value of type 'T' - internal T Update(float deltaTime, double tickLatencyAsTime, float minDeltaTime, float maxDeltaTime) + internal T Update(float deltaTime, double tickLatencyAsTime, double minDeltaTime, double maxDeltaTime, bool lerp) { TryConsumeFromBuffer(tickLatencyAsTime, minDeltaTime, maxDeltaTime); - // Only interpolate when there is a start and end point and we have not already reached the end value + // Only begin interpolation when there is a start and end point if (InterpolateState.Target.HasValue) { - InterpolateState.AddDeltaTime(deltaTime); - - // Smooth dampen our current time - var current = SmoothDamp(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, ref m_RateOfChange, InterpolateState.TimeToTargetValue, InterpolateState.DeltaTime); - // Smooth dampen a predicted time based on our average delta time - var predict = SmoothDamp(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, ref m_PredictedRateOfChange, InterpolateState.TimeToTargetValue, InterpolateState.DeltaTime + (InterpolateState.AverageDeltaTime * 2)); - // Lerp between the current and predicted. - // Note: Since smooth dampening cannot over shoot, both current and predict will eventually become the same or will be very close to the same. - // Upon stopping motion, the final resing value should be a very close aproximation of the authority side. - InterpolateState.CurrentValue = Interpolate(current, predict, deltaTime); + // As long as the target hasn't been reached, interpolate or smooth dampen. + if (!InterpolateState.TargetReached) + { + // Increases the time delta relative to the time to target. + // Also calculates the LerpT and LerpTPredicted values. + InterpolateState.AddDeltaTime(deltaTime); + // SmoothDampen + if (!lerp) + { + InterpolateState.NextValue = SmoothDamp(InterpolateState.NextValue, InterpolateState.Target.Value.Item, ref m_RateOfChange, (float)InterpolateState.TimeToTargetValue * InterpolateState.LerpT, deltaTime); + } + else// Lerp + { + InterpolateState.NextValue = Interpolate(InterpolateState.PreviousValue, InterpolateState.Target.Value.Item, InterpolateState.LerpT); + } + + // If lerp smoothing is enabled, then smooth current value towards the target value + 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)); + } + else + { + // Otherwise, just assign the target value. + InterpolateState.CurrentValue = InterpolateState.NextValue; + } + } + 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) + { + // When the delta between the time sent and the current tick latency time-window is greater than the max delta time + // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), + // we will want to reset the interpolator with the current known value. This prevents the next received state update's + // time to be calculated against the last calculated time which if there is an extended period of time between the two + // it would cause a large delta time period between the two states (i.e. it stops moving for a second or two and then + // starts moving again). + if ((tickLatencyAsTime - InterpolateState.Target.Value.TimeSent) > InterpolateState.MaxDeltaTime + minDeltaTime) + { + InterpolateState.Reset(InterpolateState.CurrentValue); + } + } } m_NbItemsReceivedThisFrame = 0; return InterpolateState.CurrentValue; @@ -381,7 +424,6 @@ internal T Update(float deltaTime, double tickLatencyAsTime, float 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)) @@ -406,7 +448,7 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) { InterpolateState.Target = target; alreadyHasBufferItem = true; - InterpolateState.PreviousValue = InterpolateState.CurrentValue; + InterpolateState.NextValue = InterpolateState.CurrentValue; InterpolateState.StartTime = target.TimeSent; InterpolateState.EndTime = target.TimeSent; } @@ -416,12 +458,13 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) { alreadyHasBufferItem = true; InterpolateState.StartTime = InterpolateState.Target.Value.TimeSent; - InterpolateState.PreviousValue = InterpolateState.CurrentValue; + InterpolateState.NextValue = InterpolateState.CurrentValue; + InterpolateState.TargetReached = false; } InterpolateState.EndTime = target.TimeSent; InterpolateState.Target = target; + InterpolateState.TimeToTargetValue = InterpolateState.EndTime - InterpolateState.StartTime; } - InterpolateState.ResetDelta(); } } else @@ -452,32 +495,43 @@ public T Update(float deltaTime, double renderTime, double serverTime) { TryConsumeFromBuffer(renderTime, serverTime); // Only interpolate when there is a start and end point and we have not already reached the end value - if (InterpolateState.Target.HasValue) + if (InterpolateState.Target.HasValue && !InterpolateState.TargetReached) { // The original BufferedLinearInterpolator lerping script to assure the Smooth Dampening updates do not impact // this specific behavior. - float t = 1.0f; - double range = InterpolateState.EndTime - InterpolateState.StartTime; - if (range > k_SmallValue) + InterpolateState.LerpT = 1.0f; + if (InterpolateState.TimeToTargetValue > k_SmallValue) { - t = (float)((renderTime - InterpolateState.StartTime) / range); + InterpolateState.LerpT = Math.Clamp((float)((renderTime - InterpolateState.StartTime) / InterpolateState.TimeToTargetValue), 0.0f, 1.0f); + } - if (t < 0.0f) - { - t = 0.0f; - } + var target = Interpolate(InterpolateState.NextValue, InterpolateState.Target.Value.Item, InterpolateState.LerpT); - if (t > MaxInterpolationBound) // max extrapolation - { - // TODO this causes issues with teleport, investigate - t = 1.0f; - } + 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); + } + else + { + InterpolateState.CurrentValue = target; + } + // Determine if we have reached our target + InterpolateState.TargetReached = IsAproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item); + } + 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) + { + // When the delta between the time sent and the current tick latency time-window is greater than the max delta time + // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), + // we will want to reset the interpolator with the current known value. This prevents the next received state update's + // time to be calculated against the last calculated time which if there is an extended period of time between the two + // it would cause a large delta time period between the two states (i.e. it stops moving for a second or two and then + // starts moving again). + if ((renderTime - InterpolateState.Target.Value.TimeSent) > 0.3f) // If we haven't recevied anything within 300ms, assume we stopped motion. + { + InterpolateState.Reset(InterpolateState.CurrentValue); } - var target = Interpolate(InterpolateState.PreviousValue, InterpolateState.Target.Value.Item, t); - - // Assure our MaximumInterpolationTime is valid and that the second lerp time ranges between deltaTime and 1.0f. - var secondLerpTime = Mathf.Clamp(deltaTime / Mathf.Max(deltaTime, MaximumInterpolationTime), deltaTime, 1.0f); - InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, target, secondLerpTime); } m_NbItemsReceivedThisFrame = 0; return InterpolateState.CurrentValue; @@ -501,9 +555,9 @@ public T Update(float deltaTime, NetworkTime serverTime) /// /// Used for internal testing /// - internal T UpdateInternal(float deltaTime, NetworkTime serverTime) + internal T UpdateInternal(float deltaTime, NetworkTime serverTime, int ticksAgo = 1) { - return Update(deltaTime, serverTime.TimeTicksAgo(1).Time, serverTime.Time); + return Update(deltaTime, serverTime.TimeTicksAgo(ticksAgo).Time, serverTime.Time); } /// @@ -524,7 +578,7 @@ public void AddMeasurement(T newMeasurement, double sentTime) // Clear the interpolator Clear(); // Reset to the new value but don't automatically add the measurement (prevents recursion) - InternalReset(newMeasurement, sentTime, m_IsAngularValue, false); + InternalReset(newMeasurement, sentTime, false); m_LastMeasurementAddedTime = sentTime; m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime, m_BufferCount); // Next line keeps renderTime above m_StartTimeConsumed. Fixes pause/unpause issues @@ -533,7 +587,7 @@ public void AddMeasurement(T newMeasurement, double sentTime) return; } - // Drop measurements that are received out of order/late + // Drop measurements that are received out of order/late (i.e. user unreliable delta) if (sentTime > m_LastMeasurementAddedTime || m_BufferCount == 0) { m_BufferCount++; @@ -631,9 +685,12 @@ internal void ConvertTransformSpace(Transform transform, bool inLocalSpace) m_BufferQueue.Enqueue(entry); } InterpolateState.CurrentValue = OnConvertTransformSpace(transform, InterpolateState.CurrentValue, inLocalSpace); - var end = InterpolateState.Target.Value; - end.Item = OnConvertTransformSpace(transform, end.Item, inLocalSpace); - InterpolateState.Target = end; + if (InterpolateState.Target.HasValue) + { + var end = InterpolateState.Target.Value; + end.Item = OnConvertTransformSpace(transform, end.Item, inLocalSpace); + InterpolateState.Target = end; + } InLocalSpace = inLocalSpace; } } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs index 9d3765fb40..77583468a7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorFloat.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using UnityEngine; namespace Unity.Netcode @@ -9,34 +10,31 @@ namespace Unity.Netcode public class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator { /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override float InterpolateUnclamped(float start, float end, float time) { return Mathf.LerpUnclamped(start, end, time); } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override float Interpolate(float start, float end, float time) { return Mathf.Lerp(start, end, time); } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected override bool IsAproximately(float first, float second, float precision = 1E-07F) { return Mathf.Approximately(first, second); } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected override float SmoothDamp(float current, float target, ref float rateOfChange, float duration, float deltaTime, float maxSpeed = float.PositiveInfinity) { - if (m_IsAngularValue) - { - return Mathf.SmoothDampAngle(current, target, ref rateOfChange, duration, maxSpeed, deltaTime); - } - else - { - return Mathf.SmoothDamp(current, target, ref rateOfChange, duration, maxSpeed, deltaTime); - } + return Mathf.SmoothDamp(current, target, ref rateOfChange, duration, maxSpeed, deltaTime); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs index 8eb6d29bcc..5b8033a977 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorQuaternion.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using UnityEngine; namespace Unity.Netcode @@ -21,6 +22,7 @@ public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time) { if (IsSlerp) @@ -34,6 +36,7 @@ protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time) { if (IsSlerp) @@ -47,6 +50,7 @@ protected override Quaternion Interpolate(Quaternion start, Quaternion end, floa } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected override Quaternion SmoothDamp(Quaternion current, Quaternion target, ref Quaternion rateOfChange, float duration, float deltaTime, float maxSpeed = float.PositiveInfinity) { Vector3 currentEuler = current.eulerAngles; @@ -61,12 +65,17 @@ private protected override Quaternion SmoothDamp(Quaternion current, Quaternion } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected override bool IsAproximately(Quaternion first, Quaternion second, float precision) { - return (1.0f - Quaternion.Dot(first, second)) <= precision; + return Mathf.Abs(first.x - second.x) <= precision && + Mathf.Abs(first.y - second.y) <= precision && + Mathf.Abs(first.z - second.z) <= precision && + Mathf.Abs(first.w - second.w) <= precision; } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal override Quaternion OnConvertTransformSpace(Transform transform, Quaternion rotation, bool inLocalSpace) { if (inLocalSpace) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs index 2bb32d5402..aa5a739683 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolatorVector3.cs @@ -1,3 +1,5 @@ +using System; +using System.Runtime.CompilerServices; using UnityEngine; namespace Unity.Netcode @@ -13,6 +15,7 @@ public class BufferedLinearInterpolatorVector3 : BufferedLinearInterpolator public bool IsSlerp; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override Vector3 InterpolateUnclamped(Vector3 start, Vector3 end, float time) { if (IsSlerp) @@ -26,6 +29,7 @@ protected override Vector3 InterpolateUnclamped(Vector3 start, Vector3 end, floa } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override Vector3 Interpolate(Vector3 start, Vector3 end, float time) { if (IsSlerp) @@ -39,6 +43,7 @@ protected override Vector3 Interpolate(Vector3 start, Vector3 end, float time) } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal override Vector3 OnConvertTransformSpace(Transform transform, Vector3 position, bool inLocalSpace) { if (inLocalSpace) @@ -53,25 +58,19 @@ protected internal override Vector3 OnConvertTransformSpace(Transform transform, } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected override bool IsAproximately(Vector3 first, Vector3 second, float precision = 0.0001F) { - return Vector3.Distance(first, second) <= precision; + return Math.Round(Mathf.Abs(first.x - second.x), 2) <= precision && + Math.Round(Mathf.Abs(first.y - second.y), 2) <= precision && + Math.Round(Mathf.Abs(first.z - second.z), 2) <= precision; } /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected override Vector3 SmoothDamp(Vector3 current, Vector3 target, ref Vector3 rateOfChange, float duration, float deltaTime, float maxSpeed) { - if (m_IsAngularValue) - { - current.x = Mathf.SmoothDampAngle(current.x, target.x, ref rateOfChange.x, duration, maxSpeed, deltaTime); - current.y = Mathf.SmoothDampAngle(current.y, target.y, ref rateOfChange.y, duration, maxSpeed, deltaTime); - current.z = Mathf.SmoothDampAngle(current.z, target.z, ref rateOfChange.z, duration, maxSpeed, deltaTime); - return current; - } - else - { - return Vector3.SmoothDamp(current, target, ref rateOfChange, duration, maxSpeed, deltaTime); - } + return Vector3.SmoothDamp(current, target, ref rateOfChange, duration, maxSpeed, deltaTime); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index d0951c6cfb..aeb9687e7b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -16,7 +16,6 @@ namespace Unity.Netcode.Components [AddComponentMenu("Netcode/Network Transform")] public class NetworkTransform : NetworkBehaviour { - #if UNITY_EDITOR internal virtual bool HideInterpolateValue => false; @@ -936,26 +935,92 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade #region PROPERTIES AND GENERAL METHODS + /// + /// Pertains to Owner Authority and Interpolation
+ /// When enabled (default), 1 additional tick is added to the total number of ticks used to calculate the tick latency ("ticks ago") as a time. + /// This calculated time value is passed into the respective and used to determine if any pending + /// state updates in the queue should be processed. + /// The additional tick value is only applied when: + /// + /// The is using a authority mode. + /// The non-authority instance is a client (i.e. not host or server). + /// The network topology being used is . + /// + ///
+ /// + /// When calculating the total tick latency as time value, the is added to the + /// and if this property is enabled (and the conditions above are met) an additional tick is added to the final resultant value.
+ /// Note: The reason behind this additional tick latency value is due to the 2 RTT timespan when a client state update is sent to the host or server (1 RTT) + /// and the host or server relays this state update to all non-authority instances (1 RTT). + ///
+ public bool AutoOwnerAuthorityTickOffset = true; + /// /// The different interpolation types used with to help smooth interpolation results. + /// Interpolation types can be changed during runtime. /// public enum InterpolationTypes { /// - /// Uses lerping and yields a linear progression between two values. + /// Legacy Lerp (original NGO lerping model)
+ /// Uses a 1 to 2 phase lerp approach where:
+ /// + /// 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. + /// + ///
+ /// + /// For more information:
+ /// + /// + /// + /// + /// + /// + /// + /// + ///
+ LegacyLerp, + /// + /// Lerp (maintains time to target when under higher conditions)
+ /// Uses a 1 to 2 phase interpolation approach where:
+ /// + /// 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 1.0 minus the respective maximum interpolation time. + /// ///
/// /// For more information:
- /// -
- /// -
- /// -
+ /// + /// + /// + /// + /// + /// + /// + /// ///
Lerp, /// - /// Uses a smooth dampening approach for interpolating between two data points and adjusts based on rate of change. + /// Smooth Dampening (maintains time to target when under higher conditions)
+ /// Uses a 1 to 2 phase smooth dampening approach where:
+ /// + /// The first phase smooth dampens towards the current tick state update being processed by the accumulated delta time relative to the time to target. + /// The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the third phase at a rate of delta time divided by the respective max interpolation time. + /// ///
/// - /// Unlike , there are no additional values needed to be adjusted for this interpolation type. + /// Note: Smooth dampening is computationally more expensive than the and approaches.
+ /// It is recommended to turn lerp smoothing off or adjust the maximum interpolation time to a lower value if you want a more precise end result. + /// For more information:
+ /// + /// + /// + /// + /// + /// + /// + /// ///
SmoothDampening } @@ -964,76 +1029,118 @@ public enum InterpolationTypes /// The position interpolation type to use for the instance. ///
/// - /// - yields a traditional linear result.
- /// - adjusts based on the rate of change.
- /// - You can have mixed interpolation types between position, rotation, and scale on the same instance.
- /// - You can change the interpolation type during runtime, but changing between can result in a slight stutter if the object is in motion.
+ /// + /// Yields the original Netcode for GameObjects lerp result. + /// Uses the newer linear buffer queue consumption approach that maintains a consistent time to the next target. + /// Uses the newer linear buffer queue consumption approach and adjusts based on the rate of change. + /// + /// Things to consider:
+ /// + /// You can have mixed interpolation types between position, rotation, and scale on the same instance. + /// You can change the interpolation type during runtime, but changing between can result in a slight stutter if the object is in motion. + /// ///
[Tooltip("Lerping yields a traditional linear result where smooth dampening will adjust based on the rate of change. You can mix interpolation types for position, rotation, and scale.")] public InterpolationTypes PositionInterpolationType; + private InterpolationTypes m_PreviousPositionInterpolationType; /// /// The rotation interpolation type to use for the instance. /// /// - /// - yields a traditional linear result.
- /// - adjusts based on the rate of change.
- /// - You can have mixed interpolation types between position, rotation, and scale on the same instance.
- /// - You can change the interpolation type during runtime, but changing between can result in a slight stutter if the object is in motion.
+ /// + /// Yields the original Netcode for GameObjects lerp result. + /// Uses the newer linear buffer queue consumption approach that maintains a consistent time to the next target. + /// Uses the newer linear buffer queue consumption approach and adjusts based on the rate of change. + /// + /// Things to consider:
+ /// + /// You can have mixed interpolation types between position, rotation, and scale on the same instance. + /// You can change the interpolation type during runtime, but changing between can result in a slight stutter if the object is in motion. + /// ///
[Tooltip("Lerping yields a traditional linear result where smooth dampening will adjust based on the rate of change. You can mix interpolation types for position, rotation, and scale.")] public InterpolationTypes RotationInterpolationType; + private InterpolationTypes m_PreviousRotationInterpolationType; /// /// The scale interpolation type to use for the instance. /// /// - /// - yields a traditional linear result.
- /// - adjusts based on the rate of change.
- /// - You can have mixed interpolation types between position, rotation, and scale on the same instance.
- /// - You can change the interpolation type during runtime, but changing between can result in a slight stutter if the object is in motion.
+ /// + /// Yields the original Netcode for GameObjects lerp result. + /// Uses the newer linear buffer queue consumption approach that maintains a consistent time to the next target. + /// Uses the newer linear buffer queue consumption approach and adjusts based on the rate of change. + /// + /// Things to consider:
+ /// + /// You can have mixed interpolation types between position, rotation, and scale on the same instance. + /// You can change the interpolation type during runtime, but changing between can result in a slight stutter if the object is in motion. + /// ///
[Tooltip("Lerping yields a traditional linear result where smooth dampening will adjust based on the rate of change. You can mix interpolation types for position, rotation, and scale.")] public InterpolationTypes ScaleInterpolationType; + private InterpolationTypes m_PreviousScaleInterpolationType; /// - /// The position interoplation time divisor applied to the current delta time (dividend) where the quotient yields the time used for the second smoothing lerp. - /// - The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct).
- /// - The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value).
- /// - This value can be adjusted during runtime in the event you want to dynamically adjust it based on some other value (i.e. velocity or the like). + /// Controls position interpolation smoothing. ///
/// - /// - Only used When is enabled and using .
- /// - The quotient will be clamped to a value that ranges from 1.0f to the current delta time (i.e. or ) + /// When enabled, the will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the . ///
+ public bool PositionLerpSmoothing = true; + private bool m_PreviousPositionLerpSmoothing; + + /// + /// The position interoplation maximum interpolation time.
+ /// + /// The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct). + /// The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value). + /// This value can be adjusted during runtime in the event you want to dynamically adjust it based on some other value (i.e. linear velocity or the like). + /// + ///
[Tooltip("The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct). The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value).")] [Range(0.01f, 1.0f)] public float PositionMaxInterpolationTime = 0.1f; /// - /// The rotation interoplation time divisor applied to the current delta time (dividend) where the quotient yields the time used for the second smoothing lerp. - /// - The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct).
- /// - The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value).
- /// - This value can be adjusted during runtime in the event you want to dynamically adjust it based on some other value (i.e. velocity or the like). + /// Controls rotation interpolation smoothing. ///
/// - /// - Only used When is enabled and using .
- /// - The quotient will be clamped to a value that ranges from 1.0f to the current delta time (i.e. or ) + /// When enabled, the will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the . ///
+ public bool RotationLerpSmoothing = true; + private bool m_PreviousRotationLerpSmoothing; + + /// + /// The rotation interoplation maximum interpolation time.
+ /// + /// The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct). + /// The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value). + /// This value can be adjusted during runtime in the event you want to dynamically adjust it based on some other value (i.e. angular velocity). + /// + ///
[Tooltip("The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct). The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value).")] [Range(0.01f, 1.0f)] public float RotationMaxInterpolationTime = 0.1f; /// - /// The scale interoplation time divisor applied to the current delta time (dividend) where the quotient yields the time used for the second smoothing lerp. - /// - The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct).
- /// - The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value).
- /// - This value can be adjusted during runtime in the event you want to dynamically adjust it based on some other value (i.e. velocity or the like). + /// Controls scale interpolation smoothing. ///
/// - /// - Only used When is enabled and using .
- /// - The quotient will be clamped to a value that ranges from 1.0f to the current delta time (i.e. or ) + /// When enabled, the will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the . ///
+ public bool ScaleLerpSmoothing = true; + private bool m_PreviousScaleLerpSmoothing; + + /// + /// The scale interoplation maximum interpolation time.
+ /// + /// The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct). + /// The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value). + /// This value can be adjusted during runtime in the event you want to dynamically adjust it based on some other value. + /// + ///
[Tooltip("The higher the value the smoother, but can result in lost data points (i.e. quick changes in direct). The lower the value the more accurate/precise, but can result in slight stutter (i.e. due to jitter, latency, or a high threshold value).")] [Range(0.01f, 1.0f)] public float ScaleMaxInterpolationTime = 0.1f; @@ -1044,7 +1151,7 @@ public enum InterpolationTypes public enum AuthorityModes { /// - /// Server pushes transform state updates + /// Server pushes transform state updates. /// Server, /// @@ -1071,27 +1178,29 @@ public enum AuthorityModes /// This can help to reduce out of sync updates that can lead to slight jitter between a parent and its child/children. /// /// - /// - If this is set on a child and the parent does not have this set then the child will not be tick synchronized with its parent.
- /// - If the parent instance does not send any state updates, the children will still send state updates when exceeding axis delta threshold.
- /// - This does not need to be set on children to be applied. + /// + /// If this is set on a child and the parent does not have this set then the child will not be tick synchronized with its parent. + /// If the parent instance does not send any state updates, the children will still send state updates when exceeding axis delta threshold. + /// This does not need to be set on children to be applied. + /// ///
[Tooltip("When enabled, any parented children of this instance will send a state update when this instance sends a state update. If this instance doesn't send a state update, the children will still send state updates when reaching their axis specified threshold delta. Children do not have to have this setting enabled.")] public bool TickSyncChildren = false; /// - /// The default position change threshold value. + /// The default position change threshold value.
/// Any changes above this threshold will be replicated. ///
public const float PositionThresholdDefault = 0.001f; /// - /// The default rotation angle change threshold value. + /// The default rotation angle change threshold value.
/// Any changes above this threshold will be replicated. ///
public const float RotAngleThresholdDefault = 0.01f; /// - /// The default scale change threshold value. + /// The default scale change threshold value.
/// Any changes above this threshold will be replicated. ///
public const float ScaleThresholdDefault = 0.01f; @@ -1123,12 +1232,13 @@ public enum AuthorityModes /// are sent using a reliable fragmented sequenced network delivery. ///
/// - /// The following more critical state updates are still sent as reliable fragmented sequenced: - /// - The initial synchronization state update - /// - The teleporting state update. - /// - When using half float precision and the `NetworkDeltaPosition` delta exceeds the maximum delta forcing the axis in - /// question to be collapsed into the core base position, this state update will be sent as reliable fragmented sequenced. - /// + /// The following more critical state updates are still sent as reliable fragmented sequenced:
+ /// + /// The initial synchronization state update. + /// The teleporting state update. + /// When using half float precision and the `NetworkDeltaPosition` delta exceeds the maximum delta forcing the axis in + /// question to be collapsed into the core base position, this state update will be sent as reliable fragmented sequenced. + /// /// In order to preserve a continual consistency of axial values when unreliable delta messaging is enabled (due to the /// possibility of dropping packets), NetworkTransform instances will send 1 axial frame synchronization update per /// second (only for the axis marked to synchronize are sent as reliable fragmented sequenced) as long as a delta state @@ -1250,8 +1360,16 @@ internal bool SynchronizeScale /// The rotation threshold value that triggers a delta state update by the authoritative instance. /// /// - /// Minimum Value: 0.00001 - /// Maximum Value: 360.0 + /// + /// + /// Minimum Value + /// 0.00001 + /// + /// + /// Maximum Value + /// 360.0 + /// + /// /// [Range(0.00001f, 360.0f)] public float RotAngleThreshold = RotAngleThresholdDefault; @@ -1265,7 +1383,7 @@ internal bool SynchronizeScale public float ScaleThreshold = ScaleThresholdDefault; /// - /// Enable this on the authority side for quaternion synchronization + /// Enable this on the authority side for quaternion synchronization. /// /// /// This is synchronized by authority. During runtime, this should only be changed by the @@ -1281,7 +1399,7 @@ internal bool SynchronizeScale /// /// This has a lower precision than half float precision. Recommended only for low precision /// scenarios. provides better precision at roughly half - /// the cost of a full quaternion update. + /// the cost of a full quaternion update.
/// This is synchronized by authority. During runtime, this should only be changed by the /// authoritative side. Non-authoritative instances will be overridden by the next /// authoritative state update. @@ -1290,7 +1408,7 @@ internal bool SynchronizeScale public bool UseQuaternionCompression = false; /// - /// Enable this to use half float precision for position, rotation, and scale. + /// Enable this to use half float precision for position, rotation, and scale.
/// When enabled, delta position synchronization is used. ///
/// @@ -1375,19 +1493,27 @@ internal bool SynchronizeScale /// Helper method that returns the space relative position of the transform. /// /// - /// If InLocalSpace is then it returns the transform.localPosition - /// If InLocalSpace is then it returns the transform.position - /// When invoked on the non-authority side: - /// If is true then it will return the most + /// + /// If InLocalSpace is then it returns the transform.localPosition. + /// If InLocalSpace is then it returns the transform.position. + /// + /// + /// + /// When invoked on the non-authority side: + /// If is true then it will return the most /// current authority position from the most recent state update. This can be useful /// if interpolation is enabled and you need to determine the final target position. - /// When invoked on the authority side: - /// It will always return the space relative position. + /// + /// + /// When invoked on the authority side: + /// It will always return the space relative position. + /// + /// /// /// - /// Authority always returns the space relative transform position (whether true or false). - /// Non-authority: - /// When false (default): returns the space relative transform position + /// Authority always returns the space relative transform position (whether true or false).
+ /// Non-authority:
+ /// When false (default): returns the space relative transform position.
/// When true: returns the authority position from the most recent state update. /// /// @@ -1417,19 +1543,27 @@ public Vector3 GetSpaceRelativePosition(bool getCurrentState = false) /// Helper method that returns the space relative rotation of the transform. /// /// - /// If InLocalSpace is then it returns the transform.localRotation - /// If InLocalSpace is then it returns the transform.rotation - /// When invoked on the non-authority side: - /// If is true then it will return the most - /// current authority rotation from the most recent state update. This can be useful + /// + /// If InLocalSpace is then it returns the transform.localRotation. + /// If InLocalSpace is then it returns the transform.rotation. + /// + /// + /// + /// When invoked on the non-authority side: + /// If is true then it will return the most + /// current authority position from the most recent state update. This can be useful /// if interpolation is enabled and you need to determine the final target rotation. - /// When invoked on the authority side: - /// It will always return the space relative rotation. + /// + /// + /// When invoked on the authority side: + /// It will always return the space relative rotation. + /// + /// /// /// - /// Authority always returns the space relative transform rotation (whether true or false). - /// Non-authority: - /// When false (default): returns the space relative transform rotation + /// Authority always returns the space relative transform rotation (whether true or false).
+ /// Non-authority:
+ /// When false (default): returns the space relative transform rotation.
/// When true: returns the authority rotation from the most recent state update. /// /// @@ -1449,17 +1583,17 @@ public Quaternion GetSpaceRelativeRotation(bool getCurrentState = false) /// Helper method that returns the scale of the transform. /// /// - /// When invoked on the non-authority side: + /// When invoked on the non-authority side:
/// If is true then it will return the most /// current authority scale from the most recent state update. This can be useful - /// if interpolation is enabled and you need to determine the final target scale. - /// When invoked on the authority side: + /// if interpolation is enabled and you need to determine the final target scale.
+ /// When invoked on the authority side:
/// It will always return the space relative scale. ///
/// - /// Authority always returns the space relative transform scale (whether true or false). - /// Non-authority: - /// When false (default): returns the space relative transform scale + /// Authority always returns the space relative transform scale (whether true or false).
+ /// Non-authority:
+ /// When false (default): returns the space relative transform scale.
/// When true: returns the authority scale from the most recent state update. /// /// @@ -3075,9 +3209,18 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf /// for all transform elements being monitored by /// (i.e. Position, Scale, and Rotation) /// + /// + /// All of three max interpolation time properties will have this maximum interpolation bound value applied:
+ /// -
+ /// -
+ /// -
+ ///
/// Maximum time boundary that can be used in a frame when interpolating between two values public void SetMaxInterpolationBound(float maxInterpolationBound) { + PositionMaxInterpolationTime = maxInterpolationBound; + RotationMaxInterpolationTime = maxInterpolationBound; + ScaleMaxInterpolationTime = maxInterpolationBound; m_RotationInterpolator.MaxInterpolationBound = maxInterpolationBound; m_PositionInterpolator.MaxInterpolationBound = maxInterpolationBound; m_ScaleInterpolator.MaxInterpolationBound = maxInterpolationBound; @@ -3388,7 +3531,6 @@ private void InternalInitialization(bool isOwnershipChange = false) // Determine if this is the first NetworkTransform in the associated NetworkObject's list m_IsFirstNetworkTransform = NetworkObject.NetworkTransforms[0] == this; - if (m_CachedNetworkManager && m_CachedNetworkManager.DistributedAuthorityMode) { AuthorityMode = AuthorityModes.Owner; @@ -3456,6 +3598,14 @@ private void InternalInitialization(bool isOwnershipChange = false) } else { + // Always set these during initialization for non-authority so we can detect a change in interpolator types + m_PreviousPositionInterpolationType = PositionInterpolationType; + m_PreviousRotationInterpolationType = RotationInterpolationType; + m_PreviousScaleInterpolationType = ScaleInterpolationType; + m_PreviousPositionLerpSmoothing = PositionLerpSmoothing; + m_PreviousRotationLerpSmoothing = RotationLerpSmoothing; + m_PreviousScaleLerpSmoothing = ScaleLerpSmoothing; + // Non-authority needs to be added to updates for interpolation and applying state purposes m_CachedNetworkManager.NetworkTransformRegistration(NetworkObject, forUpdate, true); // Remove this instance from the tick update @@ -3829,18 +3979,95 @@ public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newSca #region UPDATES AND AUTHORITY CHECKS private NetworkTransformTickRegistration m_NetworkTransformTickRegistration; +#if DEBUG_LINEARBUFFER +#if UNITY_EDITOR + // For debugging purposes + public struct BufferEntry + { + public double TimeSent; + public Vector3 Position; + } + public struct NTPositionStats + { + public int FrameCount; + public int FixedFrameCount; + public int FixedUpdatesPerFrameCount; + public int TimeSynchCount; + public int Tick; + public double Time; + public double TicksAgoTime; + public int BufferCount; + public int TickMeasured; + public float LerpT; + public double DeltaTime; + public double MaxDeltaTime; + public double TimeSent; + public double TimeToTargetValue; + public Vector3 PreviousValue; + public Vector3 NextValue; + public Vector3 CurrentValue; + public Vector3 TargetValue; + + public List Buffer; + } + + public Dictionary> PositionStats = new Dictionary>(); + public bool GatherStats; + private int m_LastTimeSyncCount; + + public void ClearStats() + { + PositionStats.Clear(); + } + + public BufferedLinearInterpolatorVector3 GetPositionInterpolator() + { + return m_PositionInterpolator; + } + + public BufferedLinearInterpolatorQuaternion GetRotationInterpolator() + { + return m_RotationInterpolator; + } +#endif + public int GetPositionBufferCount() + { + return m_PositionInterpolator.m_BufferQueue.Count; + } + + public double GetPositionCurrentStateTimeToTarget() + { + return m_PositionInterpolator.InterpolateState.TimeToTargetValue; + } + + public double GetPositionLerpT() + { + return m_PositionInterpolator.InterpolateState.LerpT; + } + + public double GetPositionLastRemainingTime() + { + return m_PositionInterpolator.InterpolateState.LastRemainingTime; + } +#endif + + + // Non-Authority private void UpdateInterpolation() { AdjustForChangeInTransformSpace(); - - var cachedServerTime = m_CachedNetworkManager.ServerTime.Time; + var timeSystem = m_CachedNetworkManager.ServerTime; + var currentTime = timeSystem.Time; #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - var cachedDeltaTime = m_UseRigidbodyForMotion ? Time.fixedDeltaTime : Time.deltaTime; + var cachedDeltaTime = m_UseRigidbodyForMotion ? m_CachedNetworkManager.RealTimeProvider.FixedDeltaTime : m_CachedNetworkManager.RealTimeProvider.DeltaTime; #else - var cachedDeltaTime = Time.deltaTime; + var cachedDeltaTime = m_CachedNetworkManager.RealTimeProvider.DeltaTime; #endif - var tickLatency = m_CachedNetworkManager.NetworkTimeSystem.TickLatency; + // Optional user defined tick offset to be used to push the "render time" (the time that will be used to determine if a state update is available) + // back in order to provide more room for the interpolator to interpolate towards when latency conditions are impacting the frequency that state + // updates are received. + var tickLatency = Mathf.Max(1, m_CachedNetworkManager.NetworkTimeSystem.TickLatency + InterpolationBufferTickOffset); // If using an owner authoritative motion model if (!IsServerAuthoritative()) @@ -3858,59 +4085,162 @@ private void UpdateInterpolation() } } - var tickLatencyAsTime = m_CachedNetworkManager.LocalTime.TimeTicksAgo(tickLatency).Time; - // Smooth dampening specific: - // We clamp between tick rate and bit beyond the tick rate but not 2x tick rate (we predict 2x out) - var minDeltaTime = m_CachedNetworkManager.LocalTime.FixedDeltaTime; - // The 1.666667f value is a "magic" number tht lies between the FixedDeltaTime and 2 * the averaged - // frame update. Since smooth dampening is most useful for Rigidbody motion, the physics update - // frequency is roughly 60hz (59.x?) which 2x that value as frequency is typically close to 32-33ms. - // Look within the Interpolator.Update for smooth dampening to better understand the above. - var maxDeltaTime = (1.666667f * m_CachedNetworkManager.ServerTime.FixedDeltaTime); + // Get the tick latency (ticks ago) as time (in the past) to process state updates in the queue. + var tickLatencyAsTime = timeSystem.TimeTicksAgo(tickLatency).Time; + +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D + // If using rigid body for motion, then we need to increment + // our tick latency based on the number of times FixedUpdate + // is executed. + if (m_UseRigidbodyForMotion) + { + tickLatencyAsTime += m_FixedTimeFrameDelta; + currentTime += m_FixedTimeFrameDelta; + } +#endif + + // Smooth dampening and extrapolation specific: + // We clamp between the tick rate frequency and the tick latency x tick rate frequency + var minDeltaTime = timeSystem.FixedDeltaTimeAsDouble; + + // Maximum delta time is the maximum time we will lerp between values. If the time exceeds this due to extreme + // latency then the value's interpolation rate will be accelerated to reach the goal and continue interpolating + // the next state updates. + var maxDeltaTime = tickLatency * minDeltaTime; // Now only update the interpolators for the portions of the transform being synchronized if (SynchronizePosition) { - if (PositionInterpolationType == InterpolationTypes.Lerp) + if (PositionLerpSmoothing) { m_PositionInterpolator.MaximumInterpolationTime = PositionMaxInterpolationTime; - m_PositionInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, cachedServerTime); + } + m_PositionInterpolator.LerpSmoothEnabled = PositionLerpSmoothing; + + // If either of these two position interpolation related values have changed, then reset the current state being interpolated. + if (m_PreviousPositionInterpolationType != PositionInterpolationType || m_PreviousPositionLerpSmoothing != PositionLerpSmoothing) + { + m_PreviousPositionInterpolationType = PositionInterpolationType; + m_PreviousPositionLerpSmoothing = PositionLerpSmoothing; + m_PositionInterpolator.ResetCurrentState(); + } + + if (PositionInterpolationType == InterpolationTypes.LegacyLerp) + { + m_PositionInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, currentTime); } else { - m_PositionInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, minDeltaTime, maxDeltaTime); + m_PositionInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, minDeltaTime, maxDeltaTime, + PositionInterpolationType == InterpolationTypes.Lerp); } } if (SynchronizeRotation) { - if (RotationInterpolationType == InterpolationTypes.Lerp) + if (RotationLerpSmoothing) { m_RotationInterpolator.MaximumInterpolationTime = RotationMaxInterpolationTime; - // When using half precision Lerp towards the target rotation. - // When using full precision Slerp towards the target rotation. - /// - m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision; - m_RotationInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, cachedServerTime); + } + + m_RotationInterpolator.LerpSmoothEnabled = RotationLerpSmoothing; + + // If either of these two rotation interpolation related values have changed, then reset the current state being interpolated. + if (m_PreviousRotationInterpolationType != RotationInterpolationType || m_PreviousRotationLerpSmoothing != RotationLerpSmoothing) + { + m_PreviousRotationInterpolationType = RotationInterpolationType; + m_PreviousRotationLerpSmoothing = RotationLerpSmoothing; + m_RotationInterpolator.ResetCurrentState(); + } + // When using half precision Lerp towards the target rotation. + // When using full precision Slerp towards the target rotation. + /// + m_RotationInterpolator.IsSlerp = !UseHalfFloatPrecision; + if (RotationInterpolationType == InterpolationTypes.LegacyLerp) + { + m_RotationInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, currentTime); } else { - m_RotationInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, minDeltaTime, maxDeltaTime); + m_RotationInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, minDeltaTime, maxDeltaTime, + RotationInterpolationType == InterpolationTypes.Lerp); } } if (SynchronizeScale) { - if (ScaleInterpolationType == InterpolationTypes.Lerp) + if (ScaleLerpSmoothing) { m_ScaleInterpolator.MaximumInterpolationTime = ScaleMaxInterpolationTime; - m_ScaleInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, cachedServerTime); + } + + m_ScaleInterpolator.LerpSmoothEnabled = ScaleLerpSmoothing; + + // If either of these two rotation interpolation related values have changed, then reset the current state being interpolated. + if (m_PreviousScaleInterpolationType != ScaleInterpolationType || m_PreviousScaleLerpSmoothing != ScaleLerpSmoothing) + { + m_PreviousScaleInterpolationType = ScaleInterpolationType; + m_PreviousScaleLerpSmoothing = ScaleLerpSmoothing; + m_ScaleInterpolator.ResetCurrentState(); + } + + if (ScaleInterpolationType == InterpolationTypes.LegacyLerp) + { + m_ScaleInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, currentTime); } else { - m_ScaleInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, minDeltaTime, maxDeltaTime); + m_ScaleInterpolator.Update(cachedDeltaTime, tickLatencyAsTime, minDeltaTime, maxDeltaTime, + ScaleInterpolationType == InterpolationTypes.Lerp); } } + +#if DEBUG_LINEARBUFFER && UNITY_EDITOR + // For debugging purposes + if (GatherStats) + { + if (!m_PositionInterpolator.InterpolateState.Target.HasValue) + { + return; + } + var posStats = new NTPositionStats() + { + FrameCount = m_FrameCount, + FixedFrameCount = m_FixedFrameCount, + FixedUpdatesPerFrameCount = m_FixedUpdatesPerFrameCount, + TimeSynchCount = m_CachedNetworkManager.NetworkTimeSystem.SyncCount, + Tick = timeSystem.Tick, + Time = timeSystem.Time, + TicksAgoTime = tickLatencyAsTime, + BufferCount = m_PositionInterpolator.m_BufferQueue.Count, + TickMeasured = (int)Math.Round(m_PositionInterpolator.InterpolateState.Target.Value.TimeSent / timeSystem.FixedDeltaTimeAsDouble, MidpointRounding.AwayFromZero), + LerpT = m_PositionInterpolator.InterpolateState.LerpT, + DeltaTime = m_PositionInterpolator.InterpolateState.DeltaTime, + MaxDeltaTime = m_PositionInterpolator.InterpolateState.MaxDeltaTime, + TimeSent = m_PositionInterpolator.InterpolateState.Target.Value.TimeSent, + TimeToTargetValue = m_PositionInterpolator.InterpolateState.TimeToTargetValue, + PreviousValue = m_PositionInterpolator.InterpolateState.PreviousValue, + NextValue = m_PositionInterpolator.InterpolateState.NextValue, + CurrentValue = m_PositionInterpolator.InterpolateState.CurrentValue, + TargetValue = m_PositionInterpolator.InterpolateState.Target.Value.Item, + Buffer = new List(), + }; + + foreach (var entry in m_PositionInterpolator.m_BufferQueue) + { + posStats.Buffer.Add(new BufferEntry() + { + TimeSent = entry.TimeSent, + Position = entry.Item, + }); + } + if (!PositionStats.ContainsKey(posStats.TickMeasured)) + { + PositionStats.Add(posStats.TickMeasured, new List()); + } + PositionStats[posStats.TickMeasured].Add(posStats); + } +#endif } /// @@ -3936,12 +4266,47 @@ public virtual void OnUpdate() UpdateInterpolation(); } - // Apply the current authoritative state ApplyAuthoritativeState(); } #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D +#if DEBUG_LINEARBUFFER && UNITY_EDITOR + // For debugging purposes + private int m_FrameCount = 0; + private int m_FixedFrameCount = 0; + private int m_FixedUpdatesPerFrameCount = 0; +#endif + + // This is used during fixed update in case there are multiple fixed update passes without any frame pass. + private float m_FixedTimeFrameDelta; + + // The fixed time (static time step) value for the current frame (in the event it is changed at runtime). + private float m_DeltaFixedUpdateCached; + + /// + /// Resets the total fixed update time passed. + /// This handles dealing with multiple passes within FixedUpdate and interpolation. + /// + internal void ResetFixedTimeDelta() + { + // If not spawned or this instance has authority, exit early + if (!m_UseRigidbodyForMotion || !IsSpawned || CanCommitToTransform) + { + return; + } + + // Get the current fixed delta time (used in fixed upate) + m_DeltaFixedUpdateCached = m_CachedNetworkManager.RealTimeProvider.FixedDeltaTime; + // Reset the total fixed update time (increased each time physics invokes FixedUpdate within the same frame) + m_FixedTimeFrameDelta = 0.0f; +#if DEBUG_LINEARBUFFER && UNITY_EDITOR + // For debugging purposes + m_FrameCount++; + m_FixedUpdatesPerFrameCount = 0; +#endif + } + /// /// When paired with a NetworkRigidbody and NetworkRigidbody.UseRigidBodyForMotion is enabled, /// this will be invoked during . @@ -3956,6 +4321,11 @@ public virtual void OnFixedUpdate() m_NetworkRigidbodyInternal.WakeIfSleeping(); +#if DEBUG_LINEARBUFFER && UNITY_EDITOR + // For debugging purposes + m_FixedFrameCount++; + m_FixedUpdatesPerFrameCount++; +#endif // Update interpolation when enabled if (Interpolate) @@ -3965,6 +4335,10 @@ public virtual void OnFixedUpdate() // Apply the current authoritative state ApplyAuthoritativeState(); + + // Increment the time passed based on our current fixed update rate in case + // FixedUpdate is invoked more than once within a single frame. + m_FixedTimeFrameDelta += m_DeltaFixedUpdateCached; } #endif @@ -4118,12 +4492,23 @@ private void UpdateTransformState() #region NETWORK TICK REGISTRATOIN AND HANDLING private static Dictionary s_NetworkTickRegistration = new Dictionary(); - + /// + /// Adjusts the over-all tick offset (i.e. how many ticks ago) and how wide of a maximum delta time will be used for the + /// various . + /// + /// + /// Note: You can adjust this value during runtime. Increasing this value will set non-authority instances that much further + /// behind the authority instance but will increase the number of state updates to be processed. Increasing this can be useful + /// under higher latency conditions.
+ /// The default value is 1 tick (plus the tick latency). When running on a local network, reducing this to 0 is recommended.
+ /// + ///
+ public static int InterpolationBufferTickOffset = 0; internal static float GetTickLatency(NetworkManager networkManager) { if (networkManager.IsListening) { - return (float)(networkManager.NetworkTimeSystem.TickLatency + networkManager.LocalTime.TickOffset); + return (float)(networkManager.NetworkTimeSystem.TickLatency + InterpolationBufferTickOffset + networkManager.LocalTime.TickOffset); } return 0; } @@ -4145,7 +4530,7 @@ internal static float GetTickLatencyInSeconds(NetworkManager networkManager) { if (networkManager.IsListening) { - return (float)networkManager.LocalTime.TimeTicksAgo(networkManager.NetworkTimeSystem.TickLatency).Time; + return (float)networkManager.LocalTime.TimeTicksAgo(networkManager.NetworkTimeSystem.TickLatency + InterpolationBufferTickOffset).Time; } return 0f; } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index a35fcfec75..eed0bc497c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -333,6 +333,25 @@ public void NetworkUpdate(NetworkUpdateStage updateStage) MessageManager.CleanupDisconnectedClients(); AnticipationSystem.ProcessReanticipation(); +#if COM_UNITY_MODULES_PHYSICS + foreach (var networkObjectEntry in NetworkTransformFixedUpdate) + { + // if not active or not spawned then skip + if (!networkObjectEntry.Value.gameObject.activeInHierarchy || !networkObjectEntry.Value.IsSpawned) + { + continue; + } + + foreach (var networkTransformEntry in networkObjectEntry.Value.NetworkTransforms) + { + // only update if enabled + if (networkTransformEntry.enabled) + { + networkTransformEntry.ResetFixedTimeDelta(); + } + } + } +#endif } break; #if COM_UNITY_MODULES_PHYSICS diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/TimeSyncMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/TimeSyncMessage.cs index 97b0fcc5e7..e3ab1dfe32 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/TimeSyncMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/TimeSyncMessage.cs @@ -26,6 +26,7 @@ public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; var time = new NetworkTime(networkManager.NetworkTickSystem.TickRate, Tick); + networkManager.NetworkTimeSystem.SyncCount++; networkManager.NetworkTimeSystem.Sync(time.Time, networkManager.NetworkConfig.NetworkTransport.GetCurrentRtt(context.SenderId) / 1000d); } } diff --git a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs index c3fc9e8b67..652b59cc09 100644 --- a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs +++ b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs @@ -82,10 +82,16 @@ public class NetworkTimeSystem /// The averaged latency in network ticks between a client and server. ///
/// - /// For a distributed authority network topology, this latency is between - /// the client and the distributed authority service instance. + /// For a distributed authority network topology, this latency is between the client and the + /// distributed authority service instance.
+ /// Note: uses this value plus an additional global + /// offset when interpolation + /// is enabled.
+ /// To see the current tick latency:
+ /// -
+ /// -
///
- public int TickLatency = 2; + public int TickLatency = 1; internal double LastSyncedServerTimeSec { get; private set; } internal double LastSyncedRttSec { get; private set; } @@ -257,7 +263,7 @@ public void Reset(double serverTimeSec, double rttSec) Sync(serverTimeSec, rttSec); Advance(0); } - + internal int SyncCount; /// /// Synchronizes the time system with up-to-date network statistics but does not change any time values or advance the time. /// diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs index a6f0f3a4cb..b518f26583 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -38,6 +38,7 @@ public enum SceneManagementState } private StringBuilder m_InternalErrorLog = new StringBuilder(); + internal StringBuilder VerboseDebugLog = new StringBuilder(); /// /// Registered list of all NetworkObjects spawned. @@ -257,6 +258,7 @@ protected void VerboseDebug(string msg) { if (m_EnableVerboseDebug) { + VerboseDebugLog.AppendLine(msg); Debug.Log(msg); } } @@ -345,6 +347,7 @@ protected virtual void OnInlineSetup() [UnitySetUp] public IEnumerator SetUp() { + VerboseDebugLog.Clear(); VerboseDebug($"Entering {nameof(SetUp)}"); NetcodeLogAssert = new NetcodeLogAssert(); if (m_EnableTimeTravel) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs index 703ed7433e..7d9635dd76 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformBase.cs @@ -631,14 +631,13 @@ protected bool PositionsMatchesValue(Vector3 positionToMatch) var nonAuthorityPosition = m_NonAuthoritativeTransform.transform.position; var auhtorityIsEqual = Approximately(authorityPosition, positionToMatch); var nonauthorityIsEqual = Approximately(nonAuthorityPosition, positionToMatch); - if (!auhtorityIsEqual) { - VerboseDebug($"Authority position {authorityPosition} != position to match: {positionToMatch}!"); + VerboseDebug($"Authority ({m_AuthoritativeTransform.name}) position {authorityPosition} != position to match: {positionToMatch}!"); } if (!nonauthorityIsEqual) { - VerboseDebug($"NonAuthority position {nonAuthorityPosition} != position to match: {positionToMatch}!"); + VerboseDebug($"NonAuthority ({m_NonAuthoritativeTransform.name}) position {nonAuthorityPosition} != position to match: {positionToMatch}!"); } return auhtorityIsEqual && nonauthorityIsEqual; } @@ -782,6 +781,7 @@ internal class NetworkTransformTestComponent : NetworkTransform protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) { + Debug.Log($"[Auth]{name} State Pushed."); StatePushed = true; AuthorityLastSentState = networkTransformState; AuthorityPushedTransformState?.Invoke(ref networkTransformState); @@ -792,10 +792,21 @@ protected override void OnAuthorityPushTransformState(ref NetworkTransformState public bool StateUpdated { get; internal set; } protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) { + Debug.Log($"[Non-Auth]{name} State Updated."); StateUpdated = true; base.OnNetworkTransformStateUpdated(ref oldState, ref newState); } + protected string GetVector3Values(ref Vector3 vector3) + { + return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})"; + } + + protected string GetVector3Values(Vector3 vector3) + { + return GetVector3Values(ref vector3); + } + protected override bool OnIsServerAuthoritative() { return ServerAuthority; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs index efb1ce1244..92c47522c1 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformGeneral.cs @@ -4,12 +4,20 @@ namespace Unity.Netcode.RuntimeTests { - [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, NetworkTransform.InterpolationTypes.Lerp)] - [TestFixture(HostOrServer.Host, Authority.ServerAuthority, NetworkTransform.InterpolationTypes.Lerp)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, NetworkTransform.InterpolationTypes.LegacyLerp)] [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, NetworkTransform.InterpolationTypes.SmoothDampening)] [TestFixture(HostOrServer.Host, Authority.ServerAuthority, NetworkTransform.InterpolationTypes.SmoothDampening)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, NetworkTransform.InterpolationTypes.Lerp)] + [TestFixture(HostOrServer.Host, Authority.ServerAuthority, NetworkTransform.InterpolationTypes.Lerp)] internal class NetworkTransformGeneral : NetworkTransformBase { + public enum SmoothLerpSettings + { + SmoothLerp, + NormalLerp + } + public NetworkTransformGeneral(HostOrServer testWithHost, Authority authority, NetworkTransform.InterpolationTypes interpolationType) : base(testWithHost, authority, RotationCompression.None, Rotation.Euler, Precision.Full) { @@ -279,39 +287,42 @@ public void TestMultipleExplicitSetStates([Values] Interpolation interpolation) /// This also tests that the original server authoritative model with client-owner driven NetworkTransforms is preserved. /// [Test] - public void NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation) + public void NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolation, [Values] SmoothLerpSettings smoothLerp) { var interpolate = interpolation == Interpolation.EnableInterpolate; + var usingSmoothLerp = (smoothLerp == SmoothLerpSettings.SmoothLerp) && interpolate; + var waitForDelay = usingSmoothLerp ? 1000 : 500; + m_NonAuthoritativeTransform.PositionLerpSmoothing = usingSmoothLerp; + m_NonAuthoritativeTransform.RotationLerpSmoothing = usingSmoothLerp; + m_NonAuthoritativeTransform.ScaleLerpSmoothing = usingSmoothLerp; + m_AuthoritativeTransform.Interpolate = interpolate; m_NonAuthoritativeTransform.Interpolate = interpolate; m_NonAuthoritativeTransform.RotAngleThreshold = m_AuthoritativeTransform.RotAngleThreshold = 0.1f; m_EnableVerboseDebug = true; - - m_AuthoritativeTransform.Teleport(Vector3.zero, Quaternion.identity, Vector3.one); - var success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionRotationScaleMatches(Vector3.zero, Quaternion.identity.eulerAngles, Vector3.one), 800); - Assert.True(success, $"Timed out waiting for initialization to be applied!"); - + VerboseDebug($"Target Frame Rate: {Application.targetFrameRate}"); // Test one parameter at a time first - var newPosition = new Vector3(55f, 35f, 65f); + var newPosition = usingSmoothLerp ? new Vector3(15f, -12f, 10f) : new Vector3(55f, -24f, 20f); var newRotation = Quaternion.Euler(1, 2, 3); var newScale = new Vector3(2.0f, 2.0f, 2.0f); m_NonAuthoritativeTransform.SetState(newPosition, null, null, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatchesValue(newPosition), 800); - Assert.True(success, $"Timed out waiting for non-authoritative position state request to be applied!"); + var success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatchesValue(newPosition), waitForDelay); + Assert.True(success, $"Timed out waiting for non-authoritative position state request to be applied!\n {VerboseDebugLog}"); Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); m_NonAuthoritativeTransform.SetState(null, newRotation, null, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationMatchesValue(newRotation.eulerAngles), 800); - Assert.True(success, $"Timed out waiting for non-authoritative rotation state request to be applied!"); - Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); - Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); + success = WaitForConditionOrTimeOutWithTimeTravel(() => RotationMatchesValue(newRotation.eulerAngles), waitForDelay); + Assert.True(success, $"Timed out waiting for non-authoritative rotation state request to be applied!\n {VerboseDebugLog}"); + Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), $"Authoritative rotation does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), $"Non-Authoritative rotation does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), $"Non-Authoritative rotation does not match!\n {VerboseDebugLog}"); m_NonAuthoritativeTransform.SetState(null, null, newScale, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleMatchesValue(newScale), 800); - Assert.True(success, $"Timed out waiting for non-authoritative scale state request to be applied!"); - Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); - Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); + success = WaitForConditionOrTimeOutWithTimeTravel(() => ScaleMatchesValue(newScale), waitForDelay); + Assert.True(success, $"Timed out waiting for non-authoritative scale state request to be applied!\n {VerboseDebugLog}"); + Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), $"Authoritative scale does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), $"Non-Authoritative scale does not match!\n {VerboseDebugLog}"); // Test all parameters at once newPosition = new Vector3(-10f, 95f, -25f); @@ -319,14 +330,14 @@ public void NonAuthorityOwnerSettingStateTest([Values] Interpolation interpolati newScale = new Vector3(0.5f, 0.5f, 0.5f); m_NonAuthoritativeTransform.SetState(newPosition, newRotation, newScale, interpolate); - success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionRotationScaleMatches(newPosition, newRotation.eulerAngles, newScale), 800); - Assert.True(success, $"Timed out waiting for non-authoritative position, rotation, and scale state request to be applied!"); - Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), "Authoritative position does not match!"); - Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), "Non-Authoritative position does not match!"); - Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), "Authoritative rotation does not match!"); - Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), "Non-Authoritative rotation does not match!"); - Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), "Authoritative scale does not match!"); - Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), "Non-Authoritative scale does not match!"); + success = WaitForConditionOrTimeOutWithTimeTravel(() => PositionRotationScaleMatches(newPosition, newRotation.eulerAngles, newScale), waitForDelay); + Assert.True(success, $"Timed out waiting for non-authoritative position, rotation, and scale state request to be applied!\n {VerboseDebugLog}"); + Assert.True(Approximately(newPosition, m_AuthoritativeTransform.transform.position), $"Authoritative position does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newPosition, m_NonAuthoritativeTransform.transform.position), $"Non-Authoritative position does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newRotation.eulerAngles, m_AuthoritativeTransform.transform.rotation.eulerAngles), $"Authoritative rotation does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newRotation.eulerAngles, m_NonAuthoritativeTransform.transform.rotation.eulerAngles), $"Non-Authoritative rotation does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newScale, m_AuthoritativeTransform.transform.localScale), $"Authoritative scale does not match!\n {VerboseDebugLog}"); + Assert.True(Approximately(newScale, m_NonAuthoritativeTransform.transform.localScale), $"Non-Authoritative scale does not match!\n {VerboseDebugLog}"); } } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index 6418e0f23f..d4f22c11c5 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -10,9 +10,16 @@ namespace Unity.Netcode.RuntimeTests /// server and host operating modes and will test both authoritative /// models for each operating mode. /// + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.SmoothDampening)] #if !MULTIPLAYER_TOOLS + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.Lerp)] @@ -26,9 +33,16 @@ namespace Unity.Netcode.RuntimeTests [TestFixture(HostOrServer.DAHost, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.SmoothDampening)] #endif + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.SmoothDampening)] #if !MULTIPLAYER_TOOLS + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.Lerp)] @@ -41,9 +55,16 @@ namespace Unity.Netcode.RuntimeTests [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.SmoothDampening)] [TestFixture(HostOrServer.Server, Authority.ServerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.SmoothDampening)] #endif + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Full, NetworkTransform.InterpolationTypes.SmoothDampening)] #if !MULTIPLAYER_TOOLS + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.QuaternionCompress, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.LegacyLerp)] + [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Euler, Precision.Half, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Full, NetworkTransform.InterpolationTypes.Lerp)] [TestFixture(HostOrServer.Host, Authority.OwnerAuthority, RotationCompression.None, Rotation.Quaternion, Precision.Half, NetworkTransform.InterpolationTypes.Lerp)] @@ -471,7 +492,7 @@ public void ParentedNetworkTransformTest([Values] Interpolation interpolation, [ /// delta update, and it runs through 8 delta updates per unique test. ///
[Test] - public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState, [Values] Axis axis) + public void MultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState, [Values] Axis axis) { m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local; bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ; @@ -486,6 +507,30 @@ public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace test // when interpolation is enabled. m_AuthoritativeTransform.Interpolate = axisCount == 3 ? true : false; + // Lerp smoothing skews values based on our tests and how we had to originally adjust for the way we handled the original Lerp approach and how that + // consumed state updates from the buffer. + // With the two new interpolation types, they will process until close to the final value before moving on to the next. + // Lerp--> Will skip to next state before finishing the current state (i.e. loss of precision but arrives to the final value at the end of multiple updates faster) + // LerpAhead & SmoothDampening --> + // Will not skip to the next state update until approximately at the end of the current state (higher precision longer time to final value) + // How this impacts this test: + // It was re-written to use TimeTravel which has a limit of 60 update iterations per "WaitforCondition" which if you are interpolating between two large values + // it can take a few more iterations with lerp smoothing enabled. Lerp smoothing is purely a visual thing and will eventually end up at its final destination + // upon processing the last state update. However, this test should be only to verify the functionality of the actual lerping between values without the added + // delay of smoothing the final result. So, instead of having one timeout value for the two new interpolation types and the default for the original I am opting + // for the disabling of lerp smoothing while this particular test runs as it really is only validating that each interpolator type will interpolate to the right + // value within a given period of time which is simulated using the time travel approach. + // With smooth lerping enabled, the two new interpolation types will come very close to the correct value but will not reach the 2nd or 3rd pass values set because + // this test uses the adjusted approximation checks that prematurely determines the target values (position, rotation, and scale) have been reached and as such + // sends a new state update that will sit in the buffer for 3-4 frames before the two new interpolation types are done with the current state update. This will + // eventually lead to a time deficit that will offset the processing of the next state update such that the default time travel timeout (60 updates) will timeout + // and the test will fail. This only happens with 3 axis since that is the only time interpolation was enabled for this particular test. + // As such, just disabling smooth lerping for all 3 seemed like the better approach as the maximum interpolation time out period for smooth lerping is now + // adjustable by users (i.e. they can adjust how much lerp smoothing is applied based on their project's needs). + m_NonAuthoritativeTransform.PositionLerpSmoothing = false; + m_NonAuthoritativeTransform.RotationLerpSmoothing = false; + m_NonAuthoritativeTransform.ScaleLerpSmoothing = false; + m_CurrentAxis = axis; m_AuthoritativeTransform.SyncPositionX = axisX; @@ -520,12 +565,18 @@ public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace test 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 - TimeTravelAdvanceTick(); + // The default is interpolate, so we only need to check for the updated state when + // we turn off interpolation. + if (!m_AuthoritativeTransform.Interpolate) + { + // Reset our state updated and state pushed + m_NonAuthoritativeTransform.StateUpdated = false; + m_AuthoritativeTransform.StatePushed = false; + // Wait for both authority and non-authority to update their respective flags so we know the change to interpolation has been received. + success = WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed && m_NonAuthoritativeTransform.StateUpdated); + Assert.True(success, "Failed to wait for interpolation changed state update!"); + } + var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations; // Move and rotate within the same tick, validate the non-authoritative instance updates @@ -574,6 +625,7 @@ public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace test if (!success) { m_EnableVerboseDebug = true; + VerboseDebug($"Failed on iteration: {i}"); success = PositionRotationScaleMatches(); m_EnableVerboseDebug = false; } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs index 4e0cea8e53..840204d4a5 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TransformInterpolationTests.cs @@ -14,7 +14,7 @@ internal class TransformInterpolationObject : NetworkTransform public static bool TestComplete = false; // Set the minimum threshold which we will use as our margin of error #if UNITY_EDITOR - public const float MinThreshold = 0.005f; + public const float MinThreshold = 0.00555555f; #else // Add additional room for error on console tests public const float MinThreshold = 0.009999f; diff --git a/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs b/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs index 60cbf38082..dc5b2b4e9d 100644 --- a/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs @@ -34,6 +34,21 @@ namespace TestProject.RuntimeTests [TestFixture(Interpolation.NoInterpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] [TestFixture(Interpolation.NoInterpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + + // Lerp, extrapolate, and blend pass + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Half, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.NormalSynchronize)] + [TestFixture(NetworkTransform.InterpolationTypes.Lerp, Interpolation.Interpolation, Precision.Compressed, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.NormalSynchronize)] + // Smooth dampening interpolation pass [TestFixture(NetworkTransform.InterpolationTypes.SmoothDampening, Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Server, NestedTickSynchronization.TickSynchronized)] [TestFixture(NetworkTransform.InterpolationTypes.SmoothDampening, Interpolation.Interpolation, Precision.Full, NetworkTransform.AuthorityModes.Owner, NestedTickSynchronization.TickSynchronized)] @@ -106,7 +121,7 @@ public NestedNetworkTransformTests(NetworkTransform.InterpolationTypes interpola } public NestedNetworkTransformTests(Interpolation interpolation, Precision precision, NetworkTransform.AuthorityModes authoritativeModel, NestedTickSynchronization nestedTickSynchronization) : - this(NetworkTransform.InterpolationTypes.Lerp, interpolation, precision, authoritativeModel, nestedTickSynchronization) + this(NetworkTransform.InterpolationTypes.LegacyLerp, interpolation, precision, authoritativeModel, nestedTickSynchronization) { } public NestedNetworkTransformTests()