Skip to content

Commit 36f66ec

Browse files
test - update
Moving the NetworkDeltaPositionTests into its own file.
1 parent 1bbdfdb commit 36f66ec

3 files changed

Lines changed: 391 additions & 383 deletions

File tree

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
using NUnit.Framework;
2+
using Unity.Collections;
3+
using Unity.Mathematics;
4+
using Unity.Netcode.Components;
5+
using UnityEngine;
6+
7+
namespace Unity.Netcode.RuntimeTests
8+
{
9+
/// <summary>
10+
/// Branch coverage for <see cref="NetworkDeltaPosition"/>'s encoding math.
11+
/// </summary>
12+
/// <remarks>
13+
/// Separate from <see cref="NetworkTransformHalfFloatPrecisionTests"/> because none of this needs a
14+
/// session, and that fixture would run it twice over two topologies.
15+
/// <br /><br />
16+
/// A value that is exactly representable as a half float carries no rounding loss, so a test built on
17+
/// one cannot observe the behavior checked here and will pass against broken code. Keep the constants
18+
/// below off the lattice, and derive expected encodings with <see cref="math.half(float)"/> rather than
19+
/// writing them out as literals.
20+
/// </remarks>
21+
internal class NetworkDeltaPositionTests
22+
{
23+
private const int k_Tick = 100;
24+
25+
// Lossy as a half float, and two of them still fit under the collapse threshold.
26+
private const float k_LossyStep = 0.7f;
27+
28+
// Past the threshold and exactly representable, so the collapse cannot hinge on rounding.
29+
private const float k_CollapsingStep = NetworkDeltaPosition.MaxDeltaBeforeAdjustment + 0.5f;
30+
31+
// Off the half float lattice on every axis, so each conversion leaves rounding loss behind.
32+
private static readonly Vector3 k_Base = new Vector3(30.0007f, -12.0003f, 5.0009f);
33+
34+
private static Vector3 Offset(float amount)
35+
{
36+
return k_Base + new Vector3(amount, amount, amount);
37+
}
38+
39+
// The transmitted form, so comparisons are against what actually goes on the wire.
40+
private static ushort[] Encoded(NetworkDeltaPosition deltaPosition)
41+
{
42+
return new[]
43+
{
44+
deltaPosition.HalfVector3.Axis.x.value,
45+
deltaPosition.HalfVector3.Axis.y.value,
46+
deltaPosition.HalfVector3.Axis.z.value,
47+
};
48+
}
49+
50+
[Test]
51+
public void ConstructorOverloadsProduceTheSameInitialState()
52+
{
53+
var position = k_Base;
54+
var allAxes = math.bool3(true);
55+
56+
var instances = new[]
57+
{
58+
new NetworkDeltaPosition(position, k_Tick),
59+
new NetworkDeltaPosition(position, k_Tick, allAxes),
60+
new NetworkDeltaPosition(position.x, position.y, position.z, k_Tick),
61+
new NetworkDeltaPosition(position.x, position.y, position.z, k_Tick, allAxes),
62+
};
63+
64+
foreach (var instance in instances)
65+
{
66+
Assert.AreEqual(position, instance.GetCurrentBasePosition(), "The base position should be where the object started.");
67+
Assert.AreEqual(Vector3.zero, instance.GetDeltaPosition(), "Nothing has moved yet, so there is no delta.");
68+
Assert.AreEqual(Vector3.zero, instance.PrecisionLossDelta, "No conversion has lost anything yet.");
69+
Assert.AreEqual(k_Tick, instance.NetworkTick, "The construction tick should be recorded.");
70+
Assert.IsFalse(instance.CollapsedDeltaIntoBase, "A zero delta cannot have collapsed.");
71+
Assert.IsFalse(instance.SynchronizeBase, "The base is only synchronized explicitly.");
72+
Assert.AreEqual(allAxes, instance.HalfVector3.AxisToSynchronize, "All axes should be synchronized by default.");
73+
}
74+
}
75+
76+
[Test]
77+
public void AccessorsReportTheUnderlyingState()
78+
{
79+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick);
80+
var moved = Offset(k_LossyStep);
81+
deltaPosition.UpdateFrom(ref moved, k_Tick + 1);
82+
83+
Assert.AreEqual(deltaPosition.CurrentBasePosition, deltaPosition.GetCurrentBasePosition());
84+
Assert.AreEqual(deltaPosition.DeltaPosition, deltaPosition.GetDeltaPosition());
85+
Assert.AreEqual(deltaPosition.HalfDeltaConvertedBack, deltaPosition.GetConvertedDelta());
86+
Assert.AreEqual(deltaPosition.CurrentBasePosition + deltaPosition.DeltaPosition, deltaPosition.GetFullPosition());
87+
88+
Assert.AreNotEqual(deltaPosition.GetDeltaPosition().x, deltaPosition.GetConvertedDelta().x,
89+
"The converted delta is the lossy one and should not match the full precision delta.");
90+
}
91+
92+
[Test]
93+
public void MovingFoldsThePreviousRoundingLossBackIn()
94+
{
95+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick);
96+
97+
var firstMove = Offset(k_LossyStep);
98+
deltaPosition.UpdateFrom(ref firstMove, k_Tick + 1);
99+
100+
var carriedLoss = deltaPosition.PrecisionLossDelta;
101+
Assert.AreNotEqual(0.0f, carriedLoss.x, "A step off the lattice has to leave rounding loss behind.");
102+
103+
var basePosition = deltaPosition.GetCurrentBasePosition();
104+
var secondMove = Offset(k_LossyStep * 2.0f);
105+
deltaPosition.UpdateFrom(ref secondMove, k_Tick + 2);
106+
107+
Assert.IsFalse(deltaPosition.CollapsedDeltaIntoBase,
108+
"Both steps together have to stay under the collapse threshold, or the delta asserted on below is reset to zero.");
109+
110+
// Folding the loss in is what keeps the average position accurate instead of drifting by a
111+
// fraction of a step per send.
112+
var rawDelta = secondMove.x - basePosition.x;
113+
Assert.AreEqual(rawDelta + carriedLoss.x, deltaPosition.GetDeltaPosition().x, 1e-7f,
114+
"The delta being sent should have the carried rounding loss added to it.");
115+
Assert.AreNotEqual(math.half(rawDelta).value, deltaPosition.HalfVector3.Axis.x.value,
116+
"Folding the loss in has to change the transmitted value, or it would have no effect.");
117+
Assert.AreNotEqual(carriedLoss.x, deltaPosition.PrecisionLossDelta.x,
118+
"The carried loss should be recomputed from the conversion that just happened.");
119+
}
120+
121+
[Test]
122+
public void StandingStillDoesNotChangeWhatIsSent()
123+
{
124+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick);
125+
126+
// Arrive off the lattice, which is where a settling object ends up.
127+
var arrived = Offset(k_LossyStep);
128+
deltaPosition.UpdateFrom(ref arrived, k_Tick + 1);
129+
130+
var encodedOnArrival = Encoded(deltaPosition);
131+
var lossOnArrival = deltaPosition.PrecisionLossDelta;
132+
Assert.AreNotEqual(0.0f, lossOnArrival.x, "The arrival conversion has to leave rounding loss behind.");
133+
134+
// Folding the loss back in while stationary is what made resting objects jitter.
135+
for (var tick = k_Tick + 2; tick <= k_Tick + 5; tick++)
136+
{
137+
deltaPosition.UpdateFrom(ref arrived, tick);
138+
139+
Assert.AreEqual(encodedOnArrival, Encoded(deltaPosition),
140+
$"The transmitted delta changed on tick {tick} while the position did not move.");
141+
Assert.AreEqual(lossOnArrival, deltaPosition.PrecisionLossDelta,
142+
$"The carried loss should be untouched on tick {tick} so it still applies once movement resumes.");
143+
}
144+
}
145+
146+
[Test]
147+
public void DeltaCollapsesIntoTheBaseAtTheThreshold()
148+
{
149+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick);
150+
var originalBase = deltaPosition.GetCurrentBasePosition();
151+
152+
var moved = Offset(k_CollapsingStep);
153+
deltaPosition.UpdateFrom(ref moved, k_Tick + 1);
154+
155+
Assert.IsTrue(deltaPosition.CollapsedDeltaIntoBase, "A delta at the threshold should have been folded into the base.");
156+
Assert.AreEqual(0.0f, deltaPosition.GetDeltaPosition().x, "The delta should be reset once it is folded in.");
157+
Assert.AreEqual(0.0f, deltaPosition.GetConvertedDelta().x, "The converted delta should be reset along with it.");
158+
Assert.AreNotEqual(originalBase.x, deltaPosition.GetCurrentBasePosition().x, "The base should have absorbed the delta.");
159+
Assert.AreEqual(moved.x, deltaPosition.GetFullPosition().x, 1e-3f,
160+
"Folding the delta into the base must not move the object it describes.");
161+
}
162+
163+
[Test]
164+
public void ADeltaUnderTheThresholdIsLeftAsADelta()
165+
{
166+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick);
167+
var originalBase = deltaPosition.GetCurrentBasePosition();
168+
169+
var moved = Offset(k_LossyStep);
170+
deltaPosition.UpdateFrom(ref moved, k_Tick + 1);
171+
172+
Assert.IsFalse(deltaPosition.CollapsedDeltaIntoBase, "A delta under the threshold should stay a delta.");
173+
Assert.AreEqual(originalBase, deltaPosition.GetCurrentBasePosition(), "The base should not move while the delta is small.");
174+
Assert.AreNotEqual(0.0f, deltaPosition.GetDeltaPosition().x, "The delta should hold the movement.");
175+
}
176+
177+
[Test]
178+
public void UnsynchronizedAxesAreLeftUntouched()
179+
{
180+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick, math.bool3(true, false, false));
181+
182+
var moved = Offset(k_LossyStep);
183+
deltaPosition.UpdateFrom(ref moved, k_Tick + 1);
184+
185+
Assert.AreNotEqual(0.0f, deltaPosition.GetDeltaPosition().x, "The synchronized axis should track the movement.");
186+
Assert.AreEqual(0.0f, deltaPosition.GetDeltaPosition().y, "An unsynchronized axis should not produce a delta.");
187+
Assert.AreEqual(0.0f, deltaPosition.GetDeltaPosition().z, "An unsynchronized axis should not produce a delta.");
188+
189+
// A stale reference here would break the comparison if the axis is synchronized later.
190+
Assert.AreEqual(moved.x, deltaPosition.PreviousPosition.x, "The synchronized axis should record where it was sent from.");
191+
Assert.AreEqual(k_Base.y, deltaPosition.PreviousPosition.y, "An unsynchronized axis should keep its original reference.");
192+
Assert.AreEqual(k_Base.z, deltaPosition.PreviousPosition.z, "An unsynchronized axis should keep its original reference.");
193+
}
194+
195+
[Test]
196+
public void DecodingOnTheSameTickDoesNotReadTheEncodedAxes()
197+
{
198+
var deltaPosition = new NetworkDeltaPosition(k_Base, k_Tick);
199+
var moved = Offset(k_LossyStep);
200+
deltaPosition.UpdateFrom(ref moved, k_Tick + 1);
201+
202+
var expected = deltaPosition.GetFullPosition();
203+
204+
// Overwriting the encoded axes proves this path returns the already-decoded value rather than
205+
// decoding again, which would apply the same delta twice.
206+
deltaPosition.HalfVector3.Axis = math.half3(new float3(1.9f, 1.9f, 1.9f));
207+
208+
Assert.AreEqual(expected, deltaPosition.ToVector3(k_Tick + 1),
209+
"Decoding the tick that was just written should return the position already held.");
210+
}
211+
212+
[Test]
213+
public void DecodingANewTickAppliesTheDelta()
214+
{
215+
var authority = new NetworkDeltaPosition(k_Base, k_Tick);
216+
var moved = Offset(k_LossyStep);
217+
authority.UpdateFrom(ref moved, k_Tick + 1);
218+
219+
var receiver = new NetworkDeltaPosition(k_Base, k_Tick)
220+
{
221+
HalfVector3 = authority.HalfVector3,
222+
};
223+
224+
var decoded = receiver.ToVector3(k_Tick + 1);
225+
226+
Assert.AreEqual(authority.GetConvertedDelta().x, receiver.GetDeltaPosition().x,
227+
"The receiver should decode the same delta the authority encoded.");
228+
Assert.AreEqual(k_Base.x + authority.GetConvertedDelta().x, decoded.x, 1e-4f,
229+
"The decoded position should be the base plus the transmitted delta.");
230+
}
231+
232+
[Test]
233+
public void DecodingCollapsesIntoTheBaseAtTheThreshold()
234+
{
235+
var authority = new NetworkDeltaPosition(k_Base, k_Tick);
236+
var moved = Offset(k_CollapsingStep);
237+
authority.UpdateFrom(ref moved, k_Tick + 1);
238+
239+
// The send side folds the delta into its own base but leaves the encoded axes holding it, so the
240+
// receiving side has to perform the same fold to end up on the same base.
241+
var receiver = new NetworkDeltaPosition(k_Base, k_Tick)
242+
{
243+
HalfVector3 = authority.HalfVector3,
244+
};
245+
246+
var decoded = receiver.ToVector3(k_Tick + 1);
247+
248+
Assert.AreEqual(0.0f, receiver.GetDeltaPosition().x, "The delta should be reset once it is folded into the base.");
249+
Assert.AreEqual(0, receiver.HalfVector3.Axis.x.value, "The encoded axis should be cleared along with it.");
250+
Assert.AreEqual(authority.GetCurrentBasePosition().x, receiver.GetCurrentBasePosition().x, 1e-4f,
251+
"Both sides must end up on the same base position or they will disagree from here on.");
252+
Assert.AreEqual(moved.x, decoded.x, 1e-3f, "Folding the delta into the base must not move the object.");
253+
}
254+
255+
[Test]
256+
public void DecodingIgnoresUnsynchronizedAxes()
257+
{
258+
var axesToSynchronize = math.bool3(true, false, false);
259+
var authority = new NetworkDeltaPosition(k_Base, k_Tick, axesToSynchronize);
260+
var moved = Offset(k_LossyStep);
261+
authority.UpdateFrom(ref moved, k_Tick + 1);
262+
263+
var receiver = new NetworkDeltaPosition(k_Base, k_Tick, axesToSynchronize)
264+
{
265+
HalfVector3 = authority.HalfVector3,
266+
};
267+
268+
var decoded = receiver.ToVector3(k_Tick + 1);
269+
270+
Assert.AreNotEqual(k_Base.x, decoded.x, "The synchronized axis should have moved.");
271+
Assert.AreEqual(k_Base.y, decoded.y, "An unsynchronized axis should stay at the base value.");
272+
Assert.AreEqual(k_Base.z, decoded.z, "An unsynchronized axis should stay at the base value.");
273+
}
274+
275+
[Test]
276+
public void HalfDeltaRoundTripsWhenTheBaseIsNotSynchronized()
277+
{
278+
var source = new NetworkDeltaPosition(k_Base, k_Tick);
279+
var moved = Offset(k_LossyStep);
280+
source.UpdateFrom(ref moved, k_Tick + 1);
281+
282+
var result = RoundTrip(source, synchronizeBase: false);
283+
284+
Assert.AreEqual(Encoded(source), Encoded(result), "The encoded axes should survive the round trip.");
285+
286+
// Only the half float axes go on the wire here, so the receiver keeps whatever base it had.
287+
Assert.AreEqual(Vector3.zero, result.GetCurrentBasePosition(), "The base should not be transmitted in this mode.");
288+
}
289+
290+
[Test]
291+
public void FullPrecisionRoundTripsWhenTheBaseIsSynchronized()
292+
{
293+
var source = new NetworkDeltaPosition(k_Base, k_Tick);
294+
var moved = Offset(k_LossyStep);
295+
source.UpdateFrom(ref moved, k_Tick + 1);
296+
297+
var result = RoundTrip(source, synchronizeBase: true);
298+
299+
// Synchronizing sends both values at full precision, so this path has to be lossless.
300+
Assert.AreEqual(source.GetDeltaPosition(), result.GetDeltaPosition(), "The delta should round trip exactly.");
301+
Assert.AreEqual(source.GetCurrentBasePosition(), result.GetCurrentBasePosition(), "The base should round trip exactly.");
302+
}
303+
304+
[Test]
305+
public void QuantumIsTheSmallestChangeTheEncodingCanSee()
306+
{
307+
// Exactly representable, so "one step away" is unambiguous.
308+
foreach (var value in new[] { 0.5f, 1.0f, -1.0f, 2.0f, 1024.0f })
309+
{
310+
var quantum = NetworkDeltaPosition.HalfPrecisionQuantum(value);
311+
Assert.Greater(quantum, 0.0f, $"The step size at {value} should be positive.");
312+
313+
Assert.AreNotEqual(math.half(value).value, math.half(value + quantum).value,
314+
$"A full step from {value} should encode differently, or it is not the step size.");
315+
Assert.AreEqual(math.half(value).value, math.half(value + (quantum * 0.25f)).value,
316+
$"A quarter step from {value} should encode identically, or the step size is too large.");
317+
}
318+
}
319+
320+
[Test]
321+
public void QuantumDropsTheSignBecauseTheLatticeIsSymmetric()
322+
{
323+
foreach (var value in new[] { 0.5f, 1.0f, 300.0f, 1024.0f })
324+
{
325+
Assert.AreEqual(NetworkDeltaPosition.HalfPrecisionQuantum(value),
326+
NetworkDeltaPosition.HalfPrecisionQuantum(-value),
327+
$"The step size at {value} and {-value} should be the same.");
328+
}
329+
}
330+
331+
[TestCase(65504.0f, TestName = "QuantumIsGuarded_AtLargestFiniteHalf")]
332+
[TestCase(-65504.0f, TestName = "QuantumIsGuarded_AtNegativeLargestFiniteHalf")]
333+
[TestCase(70000.0f, TestName = "QuantumIsGuarded_WhenRoundingToInfinity")]
334+
[TestCase(float.PositiveInfinity, TestName = "QuantumIsGuarded_AtPositiveInfinity")]
335+
[TestCase(float.NegativeInfinity, TestName = "QuantumIsGuarded_AtNegativeInfinity")]
336+
[TestCase(float.NaN, TestName = "QuantumIsGuarded_AtNaN")]
337+
public void QuantumIsGuardedAtTheTopOfTheRange(float value)
338+
{
339+
Assert.AreEqual(NetworkDeltaPosition.MaxDeltaBeforeAdjustment,
340+
NetworkDeltaPosition.HalfPrecisionQuantum(value),
341+
$"{value} is at or past the largest finite half float and should fall back to the maximum delta.");
342+
}
343+
344+
[Test]
345+
public void QuantumIsNeverNonFiniteOrZero()
346+
{
347+
// Why the guard exists: an infinite step size would make the "has it moved?" comparison in
348+
// UpdateFrom false for every input, silently stopping the rounding loss from being applied.
349+
var unguarded = Mathf.HalfToFloat(0x7BFF + 1) - Mathf.HalfToFloat(0x7BFF);
350+
Assert.IsTrue(float.IsInfinity(unguarded) || float.IsNaN(unguarded),
351+
"The unguarded computation at the top of the range should be non-finite, which is why the guard exists.");
352+
353+
var values = new[]
354+
{
355+
0.0f, float.Epsilon, 1e-7f, 0.5f, 1.0f, 100.0f, 65503.0f, 65504.0f, -65504.0f, 70000.0f,
356+
float.PositiveInfinity, float.NegativeInfinity, float.NaN,
357+
};
358+
359+
foreach (var value in values)
360+
{
361+
var quantum = NetworkDeltaPosition.HalfPrecisionQuantum(value);
362+
Assert.IsFalse(float.IsNaN(quantum) || float.IsInfinity(quantum), $"The step size at {value} should be finite.");
363+
Assert.Greater(quantum, 0.0f, $"The step size at {value} should be positive.");
364+
}
365+
}
366+
367+
private static NetworkDeltaPosition RoundTrip(NetworkDeltaPosition source, bool synchronizeBase)
368+
{
369+
source.SynchronizeBase = synchronizeBase;
370+
371+
using var writer = new FastBufferWriter(256, Allocator.Temp);
372+
var writeSerializer = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(writer));
373+
source.NetworkSerialize(writeSerializer);
374+
375+
// Starts from a different state, so a value that failed to arrive shows up as a mismatch.
376+
var result = new NetworkDeltaPosition(Vector3.zero, 0)
377+
{
378+
SynchronizeBase = synchronizeBase,
379+
HalfVector3 = { AxisToSynchronize = source.HalfVector3.AxisToSynchronize },
380+
};
381+
382+
using var reader = new FastBufferReader(writer, Allocator.Temp);
383+
var readSerializer = new BufferSerializer<BufferSerializerReader>(new BufferSerializerReader(reader));
384+
result.NetworkSerialize(readSerializer);
385+
386+
return result;
387+
}
388+
}
389+
}

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