Skip to content

Commit 5513c90

Browse files
feat: interpolation for network transform (Unity-Technologies#1060)
Adding buffered interpolation to network transform following this RFC Unity-Technologies/com.unity.multiplayer.rfcs#28 This will smooth out transform updates over jittery networks and help alleviate some packet loss. Jira: MTT-876
1 parent 0654eaf commit 5513c90

25 files changed

+28449
-135
lines changed

com.unity.netcode.gameobjects/Components/Interpolator.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using UnityEngine;
4+
5+
namespace Unity.Netcode
6+
{
7+
/// <summary>
8+
/// Solves for incoming values that are jittered
9+
/// Partially solves for message loss. Unclamped lerping helps hide this, but not completely
10+
/// </summary>
11+
/// <typeparam name="T"></typeparam>
12+
internal abstract class BufferedLinearInterpolator<T> where T : struct
13+
{
14+
// interface for mock testing, abstracting away external systems
15+
internal interface IInterpolatorTime
16+
{
17+
double BufferedServerTime { get; }
18+
double BufferedServerFixedTime { get; }
19+
int TickRate { get; }
20+
}
21+
22+
private class InterpolatorTime : IInterpolatorTime
23+
{
24+
public double BufferedServerTime => NetworkManager.Singleton.ServerTime.Time;
25+
public double BufferedServerFixedTime => NetworkManager.Singleton.ServerTime.FixedTime;
26+
public int TickRate => NetworkManager.Singleton.ServerTime.TickRate;
27+
}
28+
29+
internal IInterpolatorTime InterpolatorTimeProxy = new InterpolatorTime();
30+
31+
private struct BufferedItem
32+
{
33+
public T Item;
34+
public NetworkTime TimeSent;
35+
}
36+
37+
38+
/// <summary>
39+
/// Override this if you want configurable buffering, right now using ServerTick's own global buffering
40+
/// </summary>
41+
private double ServerTimeBeingHandledForBuffering => InterpolatorTimeProxy.BufferedServerTime;
42+
43+
private double RenderTime => InterpolatorTimeProxy.BufferedServerTime - 1f / InterpolatorTimeProxy.TickRate;
44+
45+
private T m_InterpStartValue;
46+
private T m_CurrentInterpValue;
47+
private T m_InterpEndValue;
48+
49+
private NetworkTime m_EndTimeConsumed;
50+
private NetworkTime m_StartTimeConsumed;
51+
52+
private readonly List<BufferedItem> m_Buffer = new List<BufferedItem>();
53+
private const int k_BufferSizeLimit = 100;
54+
55+
private int m_LifetimeConsumedCount;
56+
57+
public void ResetTo(T targetValue)
58+
{
59+
m_LifetimeConsumedCount = 1;
60+
m_InterpStartValue = targetValue;
61+
m_InterpEndValue = targetValue;
62+
m_CurrentInterpValue = targetValue;
63+
m_Buffer.Clear();
64+
m_EndTimeConsumed = new NetworkTime(InterpolatorTimeProxy.TickRate, 0);
65+
m_StartTimeConsumed = new NetworkTime(InterpolatorTimeProxy.TickRate, 0);
66+
67+
Update(0);
68+
}
69+
70+
71+
// 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
72+
private void TryConsumeFromBuffer()
73+
{
74+
int consumedCount = 0;
75+
// only consume if we're ready
76+
if (RenderTime >= m_EndTimeConsumed.Time)
77+
{
78+
// buffer is sorted so older (smaller) time values are at the end.
79+
for (int i = m_Buffer.Count - 1; i >= 0; i--) // todo stretch: consume ahead if we see we're missing values
80+
{
81+
var bufferedValue = m_Buffer[i];
82+
// Consume when ready. This can consume multiple times
83+
if (bufferedValue.TimeSent.Time <= ServerTimeBeingHandledForBuffering)
84+
{
85+
if (m_LifetimeConsumedCount == 0)
86+
{
87+
m_StartTimeConsumed = bufferedValue.TimeSent;
88+
m_InterpStartValue = bufferedValue.Item;
89+
}
90+
else if (consumedCount == 0)
91+
{
92+
m_StartTimeConsumed = m_EndTimeConsumed;
93+
m_InterpStartValue = m_InterpEndValue;
94+
}
95+
96+
m_EndTimeConsumed = bufferedValue.TimeSent;
97+
m_InterpEndValue = bufferedValue.Item;
98+
m_Buffer.RemoveAt(i);
99+
consumedCount++;
100+
m_LifetimeConsumedCount++;
101+
}
102+
}
103+
}
104+
}
105+
106+
public T Update(float deltaTime)
107+
{
108+
TryConsumeFromBuffer();
109+
110+
if (m_LifetimeConsumedCount == 0 && m_Buffer.Count == 0)
111+
{
112+
throw new InvalidOperationException("trying to update interpolator when no data has been added to it yet");
113+
}
114+
115+
// Interpolation example to understand the math below
116+
// 4 4.5 6 6.5
117+
// | | | |
118+
// A render B Server
119+
120+
if (m_LifetimeConsumedCount >= 1) // shouldn't interpolate between default values, let's wait to receive data first, should only interpolate between real measurements
121+
{
122+
double range = m_EndTimeConsumed.Time - m_StartTimeConsumed.Time;
123+
float t;
124+
if (range == 0)
125+
{
126+
t = 1;
127+
}
128+
else
129+
{
130+
t = (float)((RenderTime - m_StartTimeConsumed.Time) / range);
131+
}
132+
133+
if (t > 3) // max extrapolation
134+
{
135+
// TODO this causes issues with teleport, investigate
136+
// todo make this configurable
137+
t = 1;
138+
}
139+
140+
if (Debug.isDebugBuild)
141+
{
142+
Debug.Assert(t >= 0, $"t must be bigger than or equal to 0. range {range}, RenderTime {RenderTime}, Start time {m_StartTimeConsumed.Time}, end time {m_EndTimeConsumed.Time}");
143+
}
144+
145+
var target = InterpolateUnclamped(m_InterpStartValue, m_InterpEndValue, t);
146+
float maxInterpTime = 0.1f;
147+
m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / maxInterpTime); // second interpolate to smooth out extrapolation jumps
148+
}
149+
150+
return m_CurrentInterpValue;
151+
}
152+
153+
public void AddMeasurement(T newMeasurement, NetworkTime sentTime)
154+
{
155+
if (m_Buffer.Count >= k_BufferSizeLimit)
156+
{
157+
Debug.LogWarning("Going over buffer size limit while adding new interpolation values, interpolation buffering isn't consuming fast enough, removing oldest value now.");
158+
m_Buffer.RemoveAt(m_Buffer.Count - 1);
159+
}
160+
161+
if (sentTime.Time > m_EndTimeConsumed.Time || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now
162+
{
163+
m_Buffer.Add(new BufferedItem { Item = newMeasurement, TimeSent = sentTime });
164+
m_Buffer.Sort((item1, item2) => item2.TimeSent.Time.CompareTo(item1.TimeSent.Time));
165+
}
166+
}
167+
168+
public T GetInterpolatedValue()
169+
{
170+
return m_CurrentInterpValue;
171+
}
172+
173+
protected abstract T Interpolate(T start, T end, float time);
174+
protected abstract T InterpolateUnclamped(T start, T end, float time);
175+
}
176+
177+
internal class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator<float>
178+
{
179+
protected override float InterpolateUnclamped(float start, float end, float time)
180+
{
181+
return Mathf.LerpUnclamped(start, end, time);
182+
}
183+
184+
protected override float Interpolate(float start, float end, float time)
185+
{
186+
return Mathf.Lerp(start, end, time);
187+
}
188+
}
189+
190+
internal class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator<Quaternion>
191+
{
192+
protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time)
193+
{
194+
return Quaternion.SlerpUnclamped(start, end, time);
195+
}
196+
197+
protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time)
198+
{
199+
return Quaternion.SlerpUnclamped(start, end, time);
200+
}
201+
}
202+
}

com.unity.netcode.gameobjects/Components/Interpolator/BufferedLinearInterpolator.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)