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 60e2dabef4 com.unity.netcode.gameobjects@1.0.0-pre.7
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.7] - 2022-04-01

### Added

- Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828)
- Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823)
- Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762)
- `UnityTransport` settings can now be set programmatically. (#1845)
- `FastBufferWriter` and Reader IsInitialized property. (#1859)

### Changed

- Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849)

### Removed

- Removed `SnapshotSystem` (#1852)
- Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812)
- Removed `com.unity.collections` dependency from the package (#1849)

### Fixed
- Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850)
- Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847)
- Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841)
- Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838)
- Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836)
- Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828)
- Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821)
- Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811)
- Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809)
- Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808)
- Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801)
- Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780)
- Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779)
- Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777)
- Fixed parenting warning printing for false positives (#1855)
2022-04-01 00:00:00 +00:00

278 lines
12 KiB
C#

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>
/// <typeparam name="T"></typeparam>
internal 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;
}
}
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;
public void Clear()
{
m_Buffer.Clear();
m_EndTimeConsumed = 0.0d;
m_StartTimeConsumed = 0.0d;
}
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);
float maxInterpTime = 0.1f;
m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / maxInterpTime); // second interpolate to smooth out extrapolation jumps
}
m_NbItemsReceivedThisFrame = 0;
return m_CurrentInterpValue;
}
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);
}
}
public T GetInterpolatedValue()
{
return m_CurrentInterpValue;
}
protected abstract T Interpolate(T start, T end, float time);
protected abstract T InterpolateUnclamped(T start, T end, float time);
}
internal 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);
}
}
internal 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);
}
}
}