Skip to content

Commit 405caa8

Browse files
Merge develop-3.x.x into chore/unified-hybrid-tests-enabled-environment-var
2 parents a379634 + 6279d27 commit 405caa8

6 files changed

Lines changed: 257 additions & 14 deletions

File tree

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ Additional documentation and release notes are available at [Multiplayer Documen
1010

1111
### Added
1212

13-
1413
### Changed
1514

1615
- All editor assembly definitions are renamed with `Unity.Netcode.GameObjects.x` variants
@@ -21,21 +20,19 @@ Additional documentation and release notes are available at [Multiplayer Documen
2120

2221
### Deprecated
2322

24-
2523
### Removed
2624

27-
2825
### Fixed
2926

3027
- Fixed issue where scenes additively loaded before a session started were tracked as loaded on the server but had no scene handle entries, which caused `NetworkSceneManager.UnloadScene` to log an error and leave the scene registered as loaded even though it unloaded on all peers. (#4146)
28+
- Issue where `NetworkTransform` interpolated towards a point in time taken from the local clock rather than the server clock that state updates are stamped on, which starved the interpolator on clients and reduced interpolation to snapping between state updates. (#4135)
29+
- Issue where `NetworkTransform.GetTickLatencyInSeconds` returned an absolute network timestamp that grew for as long as the session ran, rather than the tick latency as a duration in seconds that it is documented to return. (#4135)
3130
- Issue with not being able to spawn initially disabled in-scene placed objects. (#4093)
3231
- Issue with pre-instantiated network prefab instances being marked as in-scene placed. Now pre-instantiated network prefabs are dynamically spawned. (#4093)
3332
- Issue where a user could spawn runtime created `NetworkObject` that has a GlobalObjectIdHash of zero. These are not valid instances and will no longer be allowed to spawn. (#4093)
3433

35-
3634
### Security
3735

38-
3936
### Obsolete
4037

4138

com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4259,14 +4259,13 @@ internal BufferedLinearInterpolatorQuaternion GetRotationInterpolator()
42594259
// Non-Authority
42604260
private void UpdateInterpolation()
42614261
{
4262-
// Use the local time because:
4263-
// Client-Server:
4264-
// Local time is server time on a host or server.
4265-
// Local time on clients takes latency into consideration.
4266-
// Distributed authority:
4267-
// Local time is used by the authority.
4268-
// Local time on non-authority takes latency into consid]eration.
4269-
var timeSystem = m_CachedNetworkManager.LocalTime;
4262+
// Use the server time, since that is the clock the states being interpolated between are stamped on
4263+
// (a state's SentTime is derived from its NetworkTick). Deriving the render time from LocalTime
4264+
// subtracts the tick latency from a clock that already leads ServerTime by roughly that much, which
4265+
// leaves the render time at or ahead of the newest state that can exist and starves the interpolator.
4266+
// Measuring from ServerTime is also self correcting, as the tick latency grows with the round trip
4267+
// time. This is a no-op on a host or server, where both clocks are the same.
4268+
var timeSystem = m_CachedNetworkManager.ServerTime;
42704269
var currentTime = timeSystem.Time;
42714270
#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
42724271
var cachedDeltaTime = m_UseRigidbodyForMotion ? m_CachedNetworkManager.RealTimeProvider.FixedDeltaTime : m_CachedNetworkManager.RealTimeProvider.DeltaTime;
@@ -4730,7 +4729,10 @@ internal static float GetTickLatencyInSeconds(NetworkManager networkManager)
47304729
{
47314730
if (networkManager.IsListening)
47324731
{
4733-
return (float)networkManager.LocalTime.TimeTicksAgo(networkManager.NetworkTimeSystem.TickLatency + InterpolationBufferTickOffset).Time;
4732+
// The number of ticks the interpolators run behind, as a duration. This is not a point in time:
4733+
// it does not grow as the session runs.
4734+
var ticksBehind = networkManager.NetworkTimeSystem.TickLatency + InterpolationBufferTickOffset;
4735+
return (float)(ticksBehind * networkManager.ServerTime.FixedDeltaTimeAsDouble);
47344736
}
47354737
return 0f;
47364738
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using System.Collections;
2+
using NUnit.Framework;
3+
using Unity.Netcode.Components;
4+
using Unity.Netcode.TestHelpers.Runtime;
5+
using UnityEngine;
6+
using UnityEngine.TestTools;
7+
8+
namespace Unity.Netcode.RuntimeTests
9+
{
10+
/// <summary>
11+
/// Validates that the render time a non-authority instance interpolates towards is derived from the same
12+
/// clock that the state updates it is interpolating between are stamped on.
13+
/// </summary>
14+
/// <remarks>
15+
/// Measures how far behind ServerTime the state being interpolated towards was sent. The render time is
16+
/// ServerTime minus the tick latency and only states sent at or before it are eligible, so that measurement
17+
/// can never be less than the tick latency. Deriving the render time from LocalTime eats into that margin by
18+
/// however far the two clocks are apart, and can push the target past ServerTime entirely.
19+
/// </remarks>
20+
[TestFixture(NetworkTransform.InterpolationTypes.Lerp)]
21+
[TestFixture(NetworkTransform.InterpolationTypes.SmoothDampening)]
22+
internal class NetworkTransformInterpolationRenderTimeTests : IntegrationTestWithApproximation
23+
{
24+
protected override int NumberOfClients => 1;
25+
26+
// How far LocalTime is pushed ahead of ServerTime, in ticks. An in-process test has no round trip time
27+
// to separate the two clocks, and this is large enough to exceed NetworkTimeSystem's hard reset
28+
// threshold so the offset snaps instead of converging at its default adjustment ratio.
29+
private const int k_LocalBufferTicks = 12;
30+
31+
// The separation the clocks must actually reach before any measurement is taken.
32+
private const double k_RequiredLeadTicks = 8.0d;
33+
34+
// Ticks of authority motion after the clocks have separated, so the interpolator reaches steady state.
35+
private const int k_WarmUpTicks = 20;
36+
37+
private const int k_SampledFrames = 90;
38+
39+
// Far enough each tick that every tick produces a state update rather than being filtered out by the
40+
// position threshold.
41+
private const float k_DistancePerTick = 1.37f;
42+
43+
private readonly NetworkTransform.InterpolationTypes m_InterpolationType;
44+
45+
private GameObject m_TestPrefab;
46+
private NetworkManager m_AuthorityNetworkManager;
47+
private NetworkTransform m_AuthorityInstance;
48+
private Vector3 m_Direction;
49+
50+
public NetworkTransformInterpolationRenderTimeTests(NetworkTransform.InterpolationTypes interpolationType)
51+
{
52+
m_InterpolationType = interpolationType;
53+
}
54+
55+
protected override void OnServerAndClientsCreated()
56+
{
57+
m_TestPrefab = CreateNetworkObjectPrefab("RenderTimeTestObj");
58+
var networkTransform = m_TestPrefab.AddComponent<NetworkTransform>();
59+
networkTransform.PositionInterpolationType = m_InterpolationType;
60+
base.OnServerAndClientsCreated();
61+
}
62+
63+
private static double GetTickInterval(NetworkManager networkManager)
64+
{
65+
return 1.0d / networkManager.NetworkTickSystem.TickRate;
66+
}
67+
68+
/// <summary>
69+
/// How far LocalTime currently leads ServerTime, expressed in ticks.
70+
/// </summary>
71+
private static double GetClockLeadInTicks(NetworkManager networkManager)
72+
{
73+
return (networkManager.LocalTime.Time - networkManager.ServerTime.Time) / GetTickInterval(networkManager);
74+
}
75+
76+
/// <summary>
77+
/// Moves the authority instance once per tick so that a state update is generated every tick.
78+
/// </summary>
79+
private void OnNetworkTick()
80+
{
81+
m_AuthorityInstance.transform.position += m_Direction * k_DistancePerTick;
82+
}
83+
84+
[UnityTest]
85+
public IEnumerator RenderTimeTrailsTheServerClock()
86+
{
87+
m_AuthorityNetworkManager = GetAuthorityNetworkManager();
88+
m_AuthorityInstance = SpawnObject(m_TestPrefab, m_AuthorityNetworkManager).GetComponent<NetworkTransform>();
89+
90+
yield return WaitForSpawnedOnAllOrTimeOut(m_AuthorityInstance.NetworkObject);
91+
AssertOnTimeout($"Not all clients spawned {m_AuthorityInstance.name}!");
92+
93+
var nonAuthority = GetNonAuthorityNetworkManager();
94+
var nonAuthorityInstance = nonAuthority.SpawnManager.SpawnedObjects[m_AuthorityInstance.NetworkObject.NetworkObjectId].GetComponent<NetworkTransform>();
95+
96+
// Separate the two clocks by a known amount so that which one the render time is derived from is
97+
// actually distinguishable.
98+
nonAuthority.NetworkTimeSystem.LocalBufferSec = k_LocalBufferTicks * GetTickInterval(nonAuthority);
99+
100+
// Start continuous motion on the authority.
101+
m_Direction = GetRandomVector3(-10, 10).normalized;
102+
m_AuthorityNetworkManager.NetworkTickSystem.Tick += OnNetworkTick;
103+
104+
// The offset only moves when the client next receives a time sync, so wait for the separation to
105+
// actually take hold rather than assuming it has.
106+
yield return WaitForConditionOrTimeOut(() => GetClockLeadInTicks(nonAuthority) >= k_RequiredLeadTicks);
107+
AssertOnTimeout($"The nonAuthority clock never fell {k_RequiredLeadTicks} ticks behind, so this test " +
108+
"cannot tell the two clocks apart and would pass regardless of which one is used.");
109+
110+
// Let the interpolator settle at the new separation before measuring.
111+
yield return WaitForTicks(m_AuthorityNetworkManager, k_WarmUpTicks);
112+
113+
// Sample how far behind ServerTime the state being interpolated towards was sent.
114+
var interpolator = nonAuthorityInstance.GetPositionInterpolator();
115+
var totalTargetLagTicks = 0.0d;
116+
var totalBuffered = 0;
117+
var samples = 0;
118+
for (int frame = 0; frame < k_SampledFrames; frame++)
119+
{
120+
if (interpolator.InterpolateState.Target.HasValue)
121+
{
122+
var targetLag = nonAuthority.ServerTime.Time - interpolator.InterpolateState.Target.Value.TimeSent;
123+
totalTargetLagTicks += targetLag / GetTickInterval(nonAuthority);
124+
totalBuffered += interpolator.m_BufferQueue.Count;
125+
samples++;
126+
}
127+
yield return null;
128+
}
129+
130+
m_AuthorityNetworkManager.NetworkTickSystem.Tick -= OnNetworkTick;
131+
132+
Assert.Greater(samples, 0, $"{nonAuthorityInstance.name} never had a state to interpolate towards!");
133+
134+
var meanTargetLagTicks = totalTargetLagTicks / samples;
135+
var meanBuffered = totalBuffered / (float)samples;
136+
var tickLatency = nonAuthority.NetworkTimeSystem.TickLatency;
137+
138+
// Anything less than the tick latency means the render time came from a clock that leads the one
139+
// the states are stamped on.
140+
Assert.GreaterOrEqual(meanTargetLagTicks, tickLatency,
141+
$"[{m_InterpolationType}] {nonAuthorityInstance.name} was interpolating towards a state sent " +
142+
$"{meanTargetLagTicks:F3} ticks behind the server clock, but the render time is the server " +
143+
$"clock minus a tick latency of {tickLatency}, so it should never be less than that. " +
144+
$"(clock lead {GetClockLeadInTicks(nonAuthority):F3} ticks, mean buffered {meanBuffered:F3})");
145+
}
146+
}
147+
}

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.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Collections;
2+
using NUnit.Framework;
3+
using Unity.Netcode.Components;
4+
using Unity.Netcode.TestHelpers.Runtime;
5+
using UnityEngine.TestTools;
6+
7+
namespace Unity.Netcode.RuntimeTests
8+
{
9+
/// <summary>
10+
/// Validates that <see cref="NetworkTransform.GetTickLatencyInSeconds()"/> returns what it is documented to
11+
/// return: the tick latency as a duration in seconds.
12+
/// </summary>
13+
/// <remarks>
14+
/// It previously returned <c>TimeTicksAgo(...).Time</c>, which is an absolute network timestamp rather than a
15+
/// duration, so the value grew for as long as the session ran.
16+
/// </remarks>
17+
internal class NetworkTransformTickLatencyTests : NetcodeIntegrationTest
18+
{
19+
protected override int NumberOfClients => 1;
20+
21+
// Ticks of additional buffering applied part way through the test to confirm the returned duration
22+
// tracks the tick latency it is derived from.
23+
private const int k_AddedBufferTicks = 3;
24+
25+
// Seconds of tolerance when comparing against the expected duration.
26+
private const float k_Tolerance = 0.0005f;
27+
28+
// The number of samples taken while the session runs, to confirm the value does not drift with time.
29+
private const int k_Samples = 30;
30+
31+
private int m_OriginalBufferTickOffset;
32+
33+
protected override IEnumerator OnSetup()
34+
{
35+
m_OriginalBufferTickOffset = NetworkTransform.InterpolationBufferTickOffset;
36+
return base.OnSetup();
37+
}
38+
39+
protected override IEnumerator OnTearDown()
40+
{
41+
// This is static, so leaving it modified would leak into every test that runs afterwards.
42+
NetworkTransform.InterpolationBufferTickOffset = m_OriginalBufferTickOffset;
43+
return base.OnTearDown();
44+
}
45+
46+
[UnityTest]
47+
public IEnumerator GetTickLatencyInSecondsReturnsADurationNotATimestamp()
48+
{
49+
var client = GetNonAuthorityNetworkManager();
50+
var tickInterval = (float)client.ServerTime.FixedDeltaTimeAsDouble;
51+
52+
// A timestamp climbs by roughly a second per second, so sample while the session clock advances
53+
// and hold the value to only moving when the tick latency it is derived from moves. That latency
54+
// is adaptive and can legitimately change mid-run.
55+
var previousTicksBehind = -1;
56+
var previousValue = 0f;
57+
for (int i = 0; i < k_Samples; i++)
58+
{
59+
var ticksBehind = client.NetworkTimeSystem.TickLatency + NetworkTransform.InterpolationBufferTickOffset;
60+
var latency = NetworkTransform.GetTickLatencyInSeconds(client);
61+
62+
Assert.Greater(latency, 0f, "A latency of zero or less is not a duration this can be measured against.");
63+
if (ticksBehind == previousTicksBehind)
64+
{
65+
Assert.AreEqual(previousValue, latency, k_Tolerance,
66+
$"The reported latency moved from {previousValue}s to {latency}s while the tick latency " +
67+
$"stayed at {ticksBehind} ticks, so it is tracking elapsed time rather than latency.");
68+
}
69+
70+
previousTicksBehind = ticksBehind;
71+
previousValue = latency;
72+
yield return null;
73+
}
74+
75+
// Buffering more ticks has to lengthen the reported duration by exactly those ticks.
76+
var latencyBefore = client.NetworkTimeSystem.TickLatency;
77+
var before = NetworkTransform.GetTickLatencyInSeconds(client);
78+
NetworkTransform.InterpolationBufferTickOffset = m_OriginalBufferTickOffset + k_AddedBufferTicks;
79+
yield return null;
80+
81+
var latencyAfter = client.NetworkTimeSystem.TickLatency;
82+
var after = NetworkTransform.GetTickLatencyInSeconds(client);
83+
84+
// The adaptive tick latency may also have moved in between, so only the buffering is held to an
85+
// exact figure.
86+
var expectedIncrease = (k_AddedBufferTicks + (latencyAfter - latencyBefore)) * tickInterval;
87+
Assert.AreEqual(expectedIncrease, after - before, k_Tolerance,
88+
$"Adding {k_AddedBufferTicks} ticks of buffering changed the reported latency by " +
89+
$"{after - before}s when a tick is {tickInterval}s, so it should have changed by " +
90+
$"{expectedIncrease}s (tick latency went from {latencyBefore} to {latencyAfter}).");
91+
}
92+
}
93+
}

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTickLatencyTests.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)