This repository has been archived on 2025-04-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
com.unity.netcode.gameobjects/Components/Interpolator/BufferedLinearInterpolator.cs
Unity Technologies add668dfd2 com.unity.netcode.gameobjects@1.0.0-pre.8
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [1.0.0-pre.8] - 2022-04-27

### Changed

- `unmanaged` structs are no longer universally accepted as RPC parameters because some structs (i.e., structs with pointers in them, such as `NativeList<T>`) can't be supported by the default memcpy struct serializer. Structs that are intended to be serialized across the network must add `INetworkSerializeByMemcpy` to the interface list (i.e., `struct Foo : INetworkSerializeByMemcpy`). This interface is empty and just serves to mark the struct as compatible with memcpy serialization. For external structs you can't edit, you can pass them to RPCs by wrapping them in `ForceNetworkSerializeByMemcpy<T>`. (#1901)

### Removed
- Removed `SIPTransport` (#1870)

- Removed `ClientNetworkTransform` from the package samples and moved to Boss Room's Utilities package which can be found [here](https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop/blob/main/Packages/com.unity.multiplayer.samples.coop/Utilities/Net/ClientAuthority/ClientNetworkTransform.cs).

### Fixed

- Fixed `NetworkTransform` generating false positive rotation delta checks when rolling over between 0 and 360 degrees. (#1890)
- Fixed client throwing an exception if it has messages in the outbound queue when processing the `NetworkEvent.Disconnect` event and is using UTP. (#1884)
- Fixed issue during client synchronization if 'ValidateSceneBeforeLoading' returned false it would halt the client synchronization process resulting in a client that was approved but not synchronized or fully connected with the server. (#1883)
- Fixed an issue where UNetTransport.StartServer would return success even if the underlying transport failed to start (#854)
- Passing generic types to RPCs no longer causes a native crash (#1901)
- Fixed an issue where calling `Shutdown` on a `NetworkManager` that was already shut down would cause an immediate shutdown the next time it was started (basically the fix makes `Shutdown` idempotent). (#1877)
2022-04-27 00:00:00 +00:00

295 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Netcode
{
/// <summary>
/// Solves for incoming values that are jittered
/// Partially solves for message loss. Unclamped lerping helps hide this, but not completely
/// </summary>
public abstract class BufferedLinearInterpolator<T> where T : struct
{
private struct BufferedItem
{
public T Item;
public double TimeSent;
public BufferedItem(T item, double timeSent)
{
Item = item;
TimeSent = timeSent;
}
}
/// <summary>
/// Theres two factors affecting interpolation: buffering (set in NetworkManagers NetworkTimeSystem) and interpolation time, which is the amount of time itll take to reach the target. This is to affect the second one.
/// </summary>
public float MaximumInterpolationTime = 0.1f;
private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator
private T m_InterpStartValue;
private T m_CurrentInterpValue;
private T m_InterpEndValue;
private double m_EndTimeConsumed;
private double m_StartTimeConsumed;
private readonly List<BufferedItem> m_Buffer = new List<BufferedItem>(k_BufferCountLimit);
// Buffer consumption scenarios
// Perfect case consumption
// | 1 | 2 | 3 |
// | 2 | 3 | 4 | consume 1
// | 3 | 4 | 5 | consume 2
// | 4 | 5 | 6 | consume 3
// | 5 | 6 | 7 | consume 4
// jittered case
// | 1 | 2 | 3 |
// | 2 | 3 | | consume 1
// | 3 | | | consume 2
// | 4 | 5 | 6 | consume 3
// | 5 | 6 | 7 | consume 4
// bursted case (assuming max count is 5)
// | 1 | 2 | 3 |
// | 2 | 3 | | consume 1
// | 3 | | | consume 2
// | | | | consume 3
// | | | |
// | 4 | 5 | 6 | 7 | 8 | --> consume all and teleport to last value <8> --> this is the nuclear option, ideally this example would consume 4 and 5
// instead of jumping to 8, but since in OnValueChange we don't yet have an updated server time (updated in pre-update) to know which value
// we should keep and which we should drop, we don't have enough information to do this. Another thing would be to not have the burst in the first place.
// Constant absolute value for max buffer count instead of dynamic time based value. This is in case we have very low tick rates, so
// that we don't have a very small buffer because of this.
private const int k_BufferCountLimit = 100;
private BufferedItem m_LastBufferedItemReceived;
private int m_NbItemsReceivedThisFrame;
private int m_LifetimeConsumedCount;
private bool InvalidState => m_Buffer.Count == 0 && m_LifetimeConsumedCount == 0;
/// <summary>
/// Resets Interpolator to initial state
/// </summary>
public void Clear()
{
m_Buffer.Clear();
m_EndTimeConsumed = 0.0d;
m_StartTimeConsumed = 0.0d;
}
/// <summary>
/// Teleports current interpolation value to targetValue.
/// </summary>
public void ResetTo(T targetValue, double serverTime)
{
m_LifetimeConsumedCount = 1;
m_InterpStartValue = targetValue;
m_InterpEndValue = targetValue;
m_CurrentInterpValue = targetValue;
m_Buffer.Clear();
m_EndTimeConsumed = 0.0d;
m_StartTimeConsumed = 0.0d;
Update(0, serverTime, serverTime);
}
// todo if I have value 1, 2, 3 and I'm treating 1 to 3, I shouldn't interpolate between 1 and 3, I should interpolate from 1 to 2, then from 2 to 3 to get the best path
private void TryConsumeFromBuffer(double renderTime, double serverTime)
{
int consumedCount = 0;
// only consume if we're ready
// this operation was measured as one of our most expensive, and we should put some thought into this.
// NetworkTransform has (currently) 7 buffered linear interpolators (3 position, 3 scale, 1 rot), and
// each has its own independent buffer and 'm_endTimeConsume'. That means every frame I have to do 7x
// these checks vs. if we tracked these values in a unified way
if (renderTime >= m_EndTimeConsumed)
{
BufferedItem? itemToInterpolateTo = null;
// assumes we're using sequenced messages for netvar syncing
// buffer contains oldest values first, iterating from end to start to remove elements from list while iterating
// calling m_Buffer.Count shows up hot in the profiler.
for (int i = m_Buffer.Count - 1; i >= 0; i--) // todo stretch: consume ahead if we see we're missing values due to packet loss
{
var bufferedValue = m_Buffer[i];
// Consume when ready and interpolate to last value we can consume. This can consume multiple values from the buffer
if (bufferedValue.TimeSent <= serverTime)
{
if (!itemToInterpolateTo.HasValue || bufferedValue.TimeSent > itemToInterpolateTo.Value.TimeSent)
{
if (m_LifetimeConsumedCount == 0)
{
// if interpolator not initialized, teleport to first value when available
m_StartTimeConsumed = bufferedValue.TimeSent;
m_InterpStartValue = bufferedValue.Item;
}
else if (consumedCount == 0)
{
// Interpolating to new value, end becomes start. We then look in our buffer for a new end.
m_StartTimeConsumed = m_EndTimeConsumed;
m_InterpStartValue = m_InterpEndValue;
}
if (bufferedValue.TimeSent > m_EndTimeConsumed)
{
itemToInterpolateTo = bufferedValue;
m_EndTimeConsumed = bufferedValue.TimeSent;
m_InterpEndValue = bufferedValue.Item;
}
}
m_Buffer.RemoveAt(i);
consumedCount++;
m_LifetimeConsumedCount++;
}
}
}
}
/// <summary>
/// Convenience version of 'Update' mainly for testing
/// the reason we don't want to always call this version is so that on the calling side we can compute
/// the renderTime once for the many things being interpolated (and the many interpolators per object)
/// </summary>
/// <param name="deltaTime">time since call</param>
/// <param name="serverTime">current server time</param>
public T Update(float deltaTime, NetworkTime serverTime)
{
return Update(deltaTime, serverTime.TimeTicksAgo(1).Time, serverTime.Time);
}
/// <summary>
/// Call to update the state of the interpolators before reading out
/// </summary>
/// <param name="deltaTime">time since last call</param>
/// <param name="renderTime">our current time</param>
/// <param name="serverTime">current server time</param>
public T Update(float deltaTime, double renderTime, double serverTime)
{
TryConsumeFromBuffer(renderTime, serverTime);
if (InvalidState)
{
throw new InvalidOperationException("trying to update interpolator when no data has been added to it yet");
}
// Interpolation example to understand the math below
// 4 4.5 6 6.5
// | | | |
// A render B Server
if (m_LifetimeConsumedCount >= 1) // shouldn't interpolate between default values, let's wait to receive data first, should only interpolate between real measurements
{
float t = 1.0f;
double range = m_EndTimeConsumed - m_StartTimeConsumed;
if (range > k_SmallValue)
{
t = (float)((renderTime - m_StartTimeConsumed) / range);
if (t < 0.0f)
{
// There is no mechanism to guarantee renderTime to not be before m_StartTimeConsumed
// This clamps t to a minimum of 0 and fixes issues with longer frames and pauses
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogError($"renderTime was before m_StartTimeConsumed. This should never happen. {nameof(renderTime)} is {renderTime}, {nameof(m_StartTimeConsumed)} is {m_StartTimeConsumed}");
}
t = 0.0f;
}
if (t > 3.0f) // max extrapolation
{
// TODO this causes issues with teleport, investigate
// todo make this configurable
t = 1.0f;
}
}
var target = InterpolateUnclamped(m_InterpStartValue, m_InterpEndValue, t);
m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / MaximumInterpolationTime); // second interpolate to smooth out extrapolation jumps
}
m_NbItemsReceivedThisFrame = 0;
return m_CurrentInterpValue;
}
/// <summary>
/// Add measurements to be used during interpolation. These will be buffered before being made available to be displayed as "latest value".
/// </summary>
public void AddMeasurement(T newMeasurement, double sentTime)
{
m_NbItemsReceivedThisFrame++;
// This situation can happen after a game is paused. When starting to receive again, the server will have sent a bunch of messages in the meantime
// instead of going through thousands of value updates just to get a big teleport, we're giving up on interpolation and teleporting to the latest value
if (m_NbItemsReceivedThisFrame > k_BufferCountLimit)
{
if (m_LastBufferedItemReceived.TimeSent < sentTime)
{
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
ResetTo(newMeasurement, sentTime);
// Next line keeps renderTime above m_StartTimeConsumed. Fixes pause/unpause issues
m_Buffer.Add(m_LastBufferedItemReceived);
}
return;
}
if (sentTime > m_EndTimeConsumed || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now
{
m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime);
m_Buffer.Add(m_LastBufferedItemReceived);
}
}
/// <summary>
/// Gets latest value from the interpolator. This is updated every update as time goes by.
/// </summary>
public T GetInterpolatedValue()
{
return m_CurrentInterpValue;
}
/// <summary>
/// Method to override and adapted to the generic type. This assumes interpolation for that value will be clamped.
/// </summary>
protected abstract T Interpolate(T start, T end, float time);
/// <summary>
/// Method to override and adapted to the generic type. This assumes interpolation for that value will not be clamped.
/// </summary>
protected abstract T InterpolateUnclamped(T start, T end, float time);
}
public class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator<float>
{
protected override float InterpolateUnclamped(float start, float end, float time)
{
return Mathf.LerpUnclamped(start, end, time);
}
protected override float Interpolate(float start, float end, float time)
{
return Mathf.Lerp(start, end, time);
}
}
public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion>
{
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
{
return Quaternion.SlerpUnclamped(start, end, time);
}
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
{
return Quaternion.SlerpUnclamped(start, end, time);
}
}
}