Skip to content

Commit b658b7e

Browse files
test: Add NetworkTransform interpolation render time regression test
Adds an integration test that measures how far behind the server clock the state a non-authority NetworkTransform is interpolating towards was sent. Only states sent at or before the render time are eligible to be interpolated towards, and the render time is the server clock minus the tick latency, so that measurement can never be less than the tick latency. It currently is, and goes negative, meaning the interpolator is chasing a state that the server clock says has not happened yet. An in-process integration test has effectively no round trip time, so the test first widens the client's local time buffer to separate LocalTime and ServerTime by a known amount and waits for that separation to take hold. Without it the two clocks sit close enough together that the test would pass regardless of which one the render time is derived from. This commit contains the test only, so it can be run against an unfixed tree.
1 parent 0f87cad commit b658b7e

2 files changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using NUnit.Framework;
4+
using Unity.Netcode.Components;
5+
using Unity.Netcode.TestHelpers.Runtime;
6+
using UnityEngine;
7+
using UnityEngine.TestTools;
8+
9+
namespace Unity.Netcode.RuntimeTests
10+
{
11+
/// <summary>
12+
/// Validates that the render time a non-authority instance interpolates towards is derived from the same
13+
/// clock that the state updates it is interpolating between are stamped on.
14+
/// </summary>
15+
/// <remarks>
16+
/// A <see cref="NetworkTransform"/> state's SentTime is derived from its NetworkTick, which is a server
17+
/// tick, so the render time has to be measured from ServerTime. Measuring it from LocalTime mixes two
18+
/// clocks: LocalTime leads ServerTime, so subtracting the tick latency from LocalTime lands the render time
19+
/// back at approximately ServerTime rather than a whole tick latency behind it. The interpolator is then
20+
/// asked to render a point in time at (or ahead of) the newest state that can possibly exist, so it has
21+
/// nothing left to interpolate towards.
22+
///
23+
/// What this test measures is how far behind ServerTime the state currently being interpolated towards was
24+
/// sent. Because the target is selected against the render time, this has to be at least the tick latency:
25+
/// the render time is ServerTime minus the tick latency, and only states sent at or before the render time
26+
/// are eligible. Deriving the render time from LocalTime instead eats into that margin by however far the
27+
/// two clocks are apart, and can push the target past ServerTime entirely (a negative value below, meaning
28+
/// the interpolator is chasing a state that the server clock says has not happened yet).
29+
/// </remarks>
30+
[TestFixture(HostOrServer.Host, NetworkTransform.InterpolationTypes.Lerp)]
31+
[TestFixture(HostOrServer.Host, NetworkTransform.InterpolationTypes.SmoothDampening)]
32+
internal class NetworkTransformInterpolationRenderTimeTests : IntegrationTestWithApproximation
33+
{
34+
protected override int NumberOfClients => 1;
35+
36+
// How far LocalTime is pushed ahead of ServerTime, in ticks. An in-process integration test has
37+
// effectively no round trip time and the separation between the two clocks is
38+
// (half RTT + LocalBufferSec + ServerBufferSec), so without widening the local buffer the two clocks
39+
// sit close enough together that which one is used barely shows. This is deliberately large enough to
40+
// exceed NetworkTimeSystem's hard reset threshold (0.2s) so the offset snaps rather than converging at
41+
// the default adjustment ratio of 0.01s per second, which would take over ten seconds.
42+
private const int k_LocalBufferTicks = 12;
43+
44+
// The separation the clocks must actually reach before any measurement is taken.
45+
private const double k_RequiredLeadTicks = 8.0d;
46+
47+
// Ticks of authority motion after the clocks have separated, so the interpolator reaches steady state.
48+
private const int k_WarmUpTicks = 20;
49+
50+
// The number of rendered frames sampled once the warm up has completed.
51+
private const int k_SampledFrames = 90;
52+
53+
// The distance the authority moves each tick. Large enough that every tick produces a state update
54+
// rather than being filtered out by the position threshold.
55+
private const float k_DistancePerTick = 1.37f;
56+
57+
private readonly NetworkTransform.InterpolationTypes m_InterpolationType;
58+
59+
private GameObject m_TestPrefab;
60+
private NetworkManager m_AuthorityNetworkManager;
61+
private NetworkTransform m_AuthorityInstance;
62+
private Vector3 m_Direction;
63+
private int m_TickCount;
64+
65+
public NetworkTransformInterpolationRenderTimeTests(HostOrServer hostOrServer, NetworkTransform.InterpolationTypes interpolationType) : base(hostOrServer)
66+
{
67+
m_InterpolationType = interpolationType;
68+
}
69+
70+
// TODO: [CmbServiceTests] ServerTime's meaning under a CMB service session has not been verified.
71+
protected override bool UseCMBService()
72+
{
73+
return false;
74+
}
75+
76+
protected override void OnServerAndClientsCreated()
77+
{
78+
m_TestPrefab = CreateNetworkObjectPrefab("RenderTimeTestObj");
79+
var networkTransform = m_TestPrefab.AddComponent<NetworkTransform>();
80+
networkTransform.PositionInterpolationType = m_InterpolationType;
81+
base.OnServerAndClientsCreated();
82+
}
83+
84+
private static double GetTickInterval(NetworkManager networkManager)
85+
{
86+
return 1.0d / networkManager.NetworkTickSystem.TickRate;
87+
}
88+
89+
/// <summary>
90+
/// How far LocalTime currently leads ServerTime, expressed in ticks.
91+
/// </summary>
92+
private static double GetClockLeadInTicks(NetworkManager networkManager)
93+
{
94+
return (networkManager.LocalTime.Time - networkManager.ServerTime.Time) / GetTickInterval(networkManager);
95+
}
96+
97+
/// <summary>
98+
/// Moves the authority instance once per tick so that a state update is generated every tick.
99+
/// </summary>
100+
private void OnNetworkTick()
101+
{
102+
m_TickCount++;
103+
m_AuthorityInstance.transform.position += m_Direction * k_DistancePerTick;
104+
}
105+
106+
private bool AllClientsSpawnedInstance()
107+
{
108+
foreach (var networkManager in m_NetworkManagers)
109+
{
110+
if (networkManager == m_AuthorityNetworkManager)
111+
{
112+
continue;
113+
}
114+
115+
if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObject.NetworkObjectId))
116+
{
117+
return false;
118+
}
119+
}
120+
return true;
121+
}
122+
123+
private List<NetworkTransform> GetNonAuthorityInstances()
124+
{
125+
var instances = new List<NetworkTransform>();
126+
foreach (var networkManager in m_NetworkManagers)
127+
{
128+
if (networkManager == m_AuthorityNetworkManager)
129+
{
130+
continue;
131+
}
132+
133+
var spawnedObject = networkManager.SpawnManager.SpawnedObjects[m_AuthorityInstance.NetworkObject.NetworkObjectId];
134+
instances.Add(spawnedObject.GetComponent<NetworkTransform>());
135+
}
136+
return instances;
137+
}
138+
139+
[UnityTest]
140+
public IEnumerator RenderTimeTrailsTheServerClock()
141+
{
142+
m_AuthorityNetworkManager = GetAuthorityNetworkManager();
143+
m_AuthorityInstance = SpawnObject(m_TestPrefab, m_AuthorityNetworkManager).GetComponent<NetworkTransform>();
144+
145+
yield return WaitForConditionOrTimeOut(AllClientsSpawnedInstance);
146+
AssertOnTimeout($"Not all clients spawned {m_AuthorityInstance.name}!");
147+
148+
var nonAuthorityInstances = GetNonAuthorityInstances();
149+
Assert.IsNotEmpty(nonAuthorityInstances, "There were no non-authority instances to measure!");
150+
151+
// Separate the two clocks by a known amount so that which one the render time is derived from is
152+
// actually distinguishable.
153+
foreach (var instance in nonAuthorityInstances)
154+
{
155+
var networkManager = instance.NetworkManager;
156+
networkManager.NetworkTimeSystem.LocalBufferSec = k_LocalBufferTicks * GetTickInterval(networkManager);
157+
}
158+
159+
// Start continuous motion on the authority.
160+
m_Direction = GetRandomVector3(-10, 10).normalized;
161+
m_TickCount = 0;
162+
m_AuthorityNetworkManager.NetworkTickSystem.Tick += OnNetworkTick;
163+
164+
// The offset only moves when the client next receives a time sync, so wait for the separation to
165+
// actually take hold rather than assuming it has.
166+
yield return WaitForConditionOrTimeOut(() =>
167+
{
168+
foreach (var instance in nonAuthorityInstances)
169+
{
170+
if (GetClockLeadInTicks(instance.NetworkManager) < k_RequiredLeadTicks)
171+
{
172+
return false;
173+
}
174+
}
175+
return true;
176+
});
177+
AssertOnTimeout($"The client clocks never separated by {k_RequiredLeadTicks} ticks, so this test " +
178+
$"cannot tell the two clocks apart and would pass regardless of which one is used.");
179+
180+
// Let the interpolator settle at the new separation before measuring.
181+
var warmUpTarget = m_TickCount + k_WarmUpTicks;
182+
yield return WaitForConditionOrTimeOut(() => m_TickCount >= warmUpTarget);
183+
AssertOnTimeout("Timed out waiting for the authority to keep moving!");
184+
185+
// Sample how far behind ServerTime the state being interpolated towards was sent.
186+
var totalTargetLagTicks = new Dictionary<NetworkTransform, double>();
187+
var totalBuffered = new Dictionary<NetworkTransform, int>();
188+
var samples = new Dictionary<NetworkTransform, int>();
189+
foreach (var instance in nonAuthorityInstances)
190+
{
191+
totalTargetLagTicks.Add(instance, 0.0d);
192+
totalBuffered.Add(instance, 0);
193+
samples.Add(instance, 0);
194+
}
195+
196+
for (int frame = 0; frame < k_SampledFrames; frame++)
197+
{
198+
foreach (var instance in nonAuthorityInstances)
199+
{
200+
var interpolator = instance.GetPositionInterpolator();
201+
if (!interpolator.InterpolateState.Target.HasValue)
202+
{
203+
continue;
204+
}
205+
206+
var networkManager = instance.NetworkManager;
207+
var targetLag = networkManager.ServerTime.Time - interpolator.InterpolateState.Target.Value.TimeSent;
208+
totalTargetLagTicks[instance] += targetLag / GetTickInterval(networkManager);
209+
totalBuffered[instance] += interpolator.m_BufferQueue.Count;
210+
samples[instance]++;
211+
}
212+
yield return null;
213+
}
214+
215+
m_AuthorityNetworkManager.NetworkTickSystem.Tick -= OnNetworkTick;
216+
217+
foreach (var instance in nonAuthorityInstances)
218+
{
219+
Assert.Greater(samples[instance], 0, $"{instance.name} never had a state to interpolate towards!");
220+
221+
var networkManager = instance.NetworkManager;
222+
var meanTargetLagTicks = totalTargetLagTicks[instance] / samples[instance];
223+
var meanBuffered = totalBuffered[instance] / (float)samples[instance];
224+
var tickLatency = networkManager.NetworkTimeSystem.TickLatency;
225+
226+
// Only states sent at or before the render time are eligible to be interpolated towards, and the
227+
// render time is the server clock minus the tick latency, so the target can never be newer than
228+
// that. Anything less means the render time was taken from a clock that runs ahead of the one
229+
// the states are stamped on.
230+
Assert.GreaterOrEqual(meanTargetLagTicks, tickLatency,
231+
$"[{m_InterpolationType}] {instance.name} was interpolating towards a state sent " +
232+
$"{meanTargetLagTicks:F3} ticks behind the server clock, but the render time is the server " +
233+
$"clock minus a tick latency of {tickLatency}, so it should never be less than that. " +
234+
$"(clock lead {GetClockLeadInTicks(networkManager):F3} ticks, mean buffered {meanBuffered:F3}). " +
235+
$"The render time is being derived from a clock that leads the one state updates are stamped on.");
236+
}
237+
}
238+
}
239+
}

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformInterpolationRenderTimeTests.cs.meta

Lines changed: 2 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)