725 lines
35 KiB
C#
725 lines
35 KiB
C#
|
|
using System;
|
|
using NUnit.Framework;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Mirror.Tests
|
|
{
|
|
// a simple snapshot with timestamp & interpolation
|
|
struct SimpleSnapshot : Snapshot
|
|
{
|
|
public double remoteTimestamp { get; set; }
|
|
public double localTimestamp { get; set; }
|
|
public double value;
|
|
|
|
public SimpleSnapshot(double remoteTimestamp, double localTimestamp, double value)
|
|
{
|
|
this.remoteTimestamp = remoteTimestamp;
|
|
this.localTimestamp = localTimestamp;
|
|
this.value = value;
|
|
}
|
|
|
|
public static SimpleSnapshot Interpolate(SimpleSnapshot from, SimpleSnapshot to, double t) =>
|
|
new SimpleSnapshot(
|
|
// interpolated snapshot is applied directly. don't need timestamps.
|
|
0, 0,
|
|
// lerp unclamped in case we ever need to extrapolate.
|
|
// atm SnapshotInterpolation never does.
|
|
Mathd.LerpUnclamped(from.value, to.value, t));
|
|
}
|
|
|
|
public class SnapshotInterpolationTests
|
|
{
|
|
// buffer for convenience so we don't have to create it manually each time
|
|
SortedList<double, SimpleSnapshot> buffer;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
buffer = new SortedList<double, SimpleSnapshot>();
|
|
}
|
|
|
|
[Test]
|
|
public void InsertIfNewEnough()
|
|
{
|
|
// inserting a first value should always work
|
|
SimpleSnapshot first = new SimpleSnapshot(1, 1, 0);
|
|
SnapshotInterpolation.InsertIfNewEnough(first, buffer);
|
|
Assert.That(buffer.Count, Is.EqualTo(1));
|
|
|
|
// insert before first should not work
|
|
SimpleSnapshot before = new SimpleSnapshot(0.5, 0.5, 0);
|
|
SnapshotInterpolation.InsertIfNewEnough(before, buffer);
|
|
Assert.That(buffer.Count, Is.EqualTo(1));
|
|
|
|
// insert after first should work
|
|
SimpleSnapshot second = new SimpleSnapshot(2, 2, 0);
|
|
SnapshotInterpolation.InsertIfNewEnough(second, buffer);
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
Assert.That(buffer.Values[0], Is.EqualTo(first));
|
|
Assert.That(buffer.Values[1], Is.EqualTo(second));
|
|
|
|
// insert after second should work
|
|
SimpleSnapshot after = new SimpleSnapshot(2.5, 2.5, 0);
|
|
SnapshotInterpolation.InsertIfNewEnough(after, buffer);
|
|
Assert.That(buffer.Count, Is.EqualTo(3));
|
|
Assert.That(buffer.Values[0], Is.EqualTo(first));
|
|
Assert.That(buffer.Values[1], Is.EqualTo(second));
|
|
Assert.That(buffer.Values[2], Is.EqualTo(after));
|
|
}
|
|
|
|
// the 'ACB' problem:
|
|
// if we have a snapshot A at t=0 and C at t=2,
|
|
// we start interpolating between them.
|
|
// if suddenly B at t=1 comes in unexpectely,
|
|
// we should NOT suddenly steer towards B.
|
|
// => inserting between the first two snapshot should never be allowed
|
|
// in order to avoid all kinds of edge cases.
|
|
[Test]
|
|
public void InsertIfNewEnough_ACB_Problem()
|
|
{
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(1, 1, 0);
|
|
SimpleSnapshot c = new SimpleSnapshot(2, 2, 0);
|
|
|
|
// insert A and C
|
|
SnapshotInterpolation.InsertIfNewEnough(a, buffer);
|
|
SnapshotInterpolation.InsertIfNewEnough(c, buffer);
|
|
|
|
// trying to insert B between the first two snapshots should fail
|
|
SnapshotInterpolation.InsertIfNewEnough(b, buffer);
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
Assert.That(buffer.Values[0], Is.EqualTo(a));
|
|
Assert.That(buffer.Values[1], Is.EqualTo(c));
|
|
}
|
|
|
|
// the 'first is lagging' problem:
|
|
// server sends A, B.
|
|
// A is lagging behind by 2000ms for whatever reason.
|
|
// we get B first.
|
|
// B should remain the first snapshot, the lagging A should be dropped
|
|
[Test]
|
|
public void InsertIfNewEnough_FirstIsLagging_Problem()
|
|
{
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(1, 1, 0);
|
|
|
|
// insert B. A is still delayed.
|
|
SnapshotInterpolation.InsertIfNewEnough(b, buffer);
|
|
|
|
// now the delayed A comes in.
|
|
// timestamp is before B though.
|
|
// but it should still be dropped.
|
|
SnapshotInterpolation.InsertIfNewEnough(a, buffer);
|
|
Assert.That(buffer.Count, Is.EqualTo(1));
|
|
Assert.That(buffer.Values[0], Is.EqualTo(b));
|
|
}
|
|
|
|
[Test]
|
|
public void HasAmountOlderThan_NotEnough()
|
|
{
|
|
// only add two
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(1, 1, 0);
|
|
buffer.Add(a.remoteTimestamp, a);
|
|
buffer.Add(b.remoteTimestamp, b);
|
|
|
|
// shouldn't have more old enough than two
|
|
// because we don't have more than two
|
|
Assert.That(SnapshotInterpolation.HasAmountOlderThan(buffer, 0, 3), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void HasAmountOlderThan_EnoughButNotOldEnough()
|
|
{
|
|
// add three
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(1, 1, 0);
|
|
SimpleSnapshot c = new SimpleSnapshot(2, 2, 0);
|
|
buffer.Add(a.remoteTimestamp, a);
|
|
buffer.Add(b.remoteTimestamp, b);
|
|
buffer.Add(c.remoteTimestamp, c);
|
|
|
|
// check at time = 1.9, where third one would not be old enough.
|
|
Assert.That(SnapshotInterpolation.HasAmountOlderThan(buffer, 1.9, 3), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void HasAmountOlderThan_EnoughAndOldEnough()
|
|
{
|
|
// add three
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(1, 1, 0);
|
|
SimpleSnapshot c = new SimpleSnapshot(2, 2, 0);
|
|
buffer.Add(a.remoteTimestamp, a);
|
|
buffer.Add(b.remoteTimestamp, b);
|
|
buffer.Add(c.remoteTimestamp, c);
|
|
|
|
// check at time = 2.1, where third one would be old enough.
|
|
Assert.That(SnapshotInterpolation.HasAmountOlderThan(buffer, 2.1, 3), Is.True);
|
|
}
|
|
|
|
// UDP messages might arrive twice sometimes.
|
|
// make sure InsertIfNewEnough can handle it.
|
|
[Test]
|
|
public void InsertIfNewEnough_Duplicate()
|
|
{
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(1, 1, 0);
|
|
SimpleSnapshot c = new SimpleSnapshot(2, 2, 0);
|
|
|
|
// add two valid snapshots first.
|
|
// we can't add 'duplicates' before 3rd and 4th anyway.
|
|
SnapshotInterpolation.InsertIfNewEnough(a, buffer);
|
|
SnapshotInterpolation.InsertIfNewEnough(b, buffer);
|
|
|
|
// insert C which is newer than B.
|
|
// then insert it again because it arrive twice.
|
|
SnapshotInterpolation.InsertIfNewEnough(c, buffer);
|
|
SnapshotInterpolation.InsertIfNewEnough(c, buffer);
|
|
|
|
// count should still be 3.
|
|
Assert.That(buffer.Count, Is.EqualTo(3));
|
|
}
|
|
|
|
[Test]
|
|
public void CalculateCatchup_Empty()
|
|
{
|
|
// make sure nothing happens with buffer size = 0
|
|
Assert.That(SnapshotInterpolation.CalculateCatchup(buffer, 0, 10), Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void CalculateCatchup_None()
|
|
{
|
|
// add one
|
|
buffer.Add(0, default);
|
|
|
|
// catch-up starts at threshold = 1. so nothing.
|
|
Assert.That(SnapshotInterpolation.CalculateCatchup(buffer, 1, 10), Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void GetFirstSecondAndDelta()
|
|
{
|
|
// add three
|
|
SimpleSnapshot a = new SimpleSnapshot(0, 1, 0);
|
|
SimpleSnapshot b = new SimpleSnapshot(2, 3, 0);
|
|
SimpleSnapshot c = new SimpleSnapshot(10, 20, 0);
|
|
buffer.Add(a.remoteTimestamp, a);
|
|
buffer.Add(b.remoteTimestamp, b);
|
|
buffer.Add(c.remoteTimestamp, c);
|
|
|
|
SnapshotInterpolation.GetFirstSecondAndDelta(buffer, out SimpleSnapshot first, out SimpleSnapshot second, out double delta);
|
|
Assert.That(first, Is.EqualTo(a));
|
|
Assert.That(second, Is.EqualTo(b));
|
|
Assert.That(delta, Is.EqualTo(b.remoteTimestamp - a.remoteTimestamp));
|
|
}
|
|
|
|
[Test]
|
|
public void CalculateCatchup_Multiple()
|
|
{
|
|
// add three
|
|
buffer.Add(0, default);
|
|
buffer.Add(1, default);
|
|
buffer.Add(2, default);
|
|
|
|
// catch-up starts at threshold = 1. so two are multiplied by 10.
|
|
Assert.That(SnapshotInterpolation.CalculateCatchup(buffer, 1, 10), Is.EqualTo(20));
|
|
}
|
|
|
|
// first step: with empty buffer and defaults, nothing should happen
|
|
[Test]
|
|
public void Compute_Step1_DefaultDoesNothing()
|
|
{
|
|
// compute with defaults
|
|
double localTime = 0;
|
|
double deltaTime = 0;
|
|
double interpolationTime = 0;
|
|
float bufferTime = 0;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should not spit out any snapshot to apply
|
|
Assert.That(result, Is.False);
|
|
// no interpolation should have happened yet
|
|
Assert.That(interpolationTime, Is.EqualTo(0));
|
|
// buffer should still be untouched
|
|
Assert.That(buffer.Count, Is.EqualTo(0));
|
|
}
|
|
|
|
// third step: compute should always wait until the first two snapshots
|
|
// are older than the time we buffer ('bufferTime')
|
|
// => test for both snapshots not old enough
|
|
[Test]
|
|
public void Compute_Step3_WaitsUntilBufferTime()
|
|
{
|
|
// add two snapshots that are barely not old enough
|
|
// (localTime - bufferTime)
|
|
// IMPORTANT: use a 'definitely old enough' remoteTime to make sure
|
|
// that compute() actually checks LOCAL, not REMOTE time!
|
|
SimpleSnapshot first = new SimpleSnapshot(0.1, 0.1, 0);
|
|
SimpleSnapshot second = new SimpleSnapshot(0.9, 1.1, 0);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
double localTime = 3;
|
|
double deltaTime = 0.5;
|
|
double interpolationTime = 0;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should not spit out any snapshot to apply
|
|
Assert.That(result, Is.False);
|
|
// no interpolation should happen yet (not old enough)
|
|
Assert.That(interpolationTime, Is.EqualTo(0));
|
|
// buffer should be untouched
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
}
|
|
|
|
// third step: compute should always wait until the first two snapshots
|
|
// are older than the time we buffer ('bufferTime')
|
|
// => test for only one snapshot which is old enough
|
|
[Test]
|
|
public void Compute_Step3_WaitsForSecondSnapshot()
|
|
{
|
|
// add a snapshot at t = 0
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 0);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
|
|
// compute at localTime = 2 with bufferTime = 1
|
|
// so the threshold is anything < t=1
|
|
double localTime = 2;
|
|
double deltaTime = 0;
|
|
double interpolationTime = 0;
|
|
float bufferTime = 1;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should not spit out any snapshot to apply
|
|
Assert.That(result, Is.False);
|
|
// no interpolation should happen yet (not enough snapshots)
|
|
Assert.That(interpolationTime, Is.EqualTo(0));
|
|
// buffer should be untouched
|
|
Assert.That(buffer.Count, Is.EqualTo(1));
|
|
}
|
|
|
|
// fourth step: compute should begin if we have two old enough snapshots
|
|
[Test]
|
|
public void Compute_Step4_InterpolateWithTwoOldEnoughSnapshots()
|
|
{
|
|
// add two old enough snapshots
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 1);
|
|
// IMPORTANT: second snapshot delta is != 1 so we can be sure that
|
|
// interpolationTime result is actual time, not 't' ratio.
|
|
// for a delta of 1, absolute and relative values would
|
|
// return the same results.
|
|
SimpleSnapshot second = new SimpleSnapshot(2, 2, 2);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
double localTime = 4;
|
|
double deltaTime = 1.5;
|
|
double interpolationTime = 0;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started just now, from 0.
|
|
// and deltaTime is 1.5, so we should be at 1.5 now.
|
|
Assert.That(interpolationTime, Is.EqualTo(1.5));
|
|
// buffer should be untouched, we are still interpolating between the two
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
// interpolationTime is at 1.5, so 3/4 between first & second.
|
|
// computed snapshot should be interpolated at 3/4ths.
|
|
Assert.That(computed.value, Is.EqualTo(1.75).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fourth step: compute should begin if we have two old enough snapshots
|
|
// => test with 3 snapshots to make sure the third one
|
|
// isn't touched while t between [0,1]
|
|
[Test]
|
|
public void Compute_Step4_InterpolateWithThreeOldEnoughSnapshots()
|
|
{
|
|
// add three old enough snapshots.
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 1);
|
|
SimpleSnapshot second = new SimpleSnapshot(1, 1, 2);
|
|
SimpleSnapshot third = new SimpleSnapshot(2, 2, 2);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
buffer.Add(third.remoteTimestamp, third);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
double localTime = 4;
|
|
double deltaTime = 0.5;
|
|
double interpolationTime = 0;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started just now, from 0.
|
|
// and deltaTime is 0.5, so we should be at 0.5 now.
|
|
Assert.That(interpolationTime, Is.EqualTo(0.5));
|
|
// buffer should be untouched, we are still interpolating between
|
|
// the first two. third should still be there.
|
|
Assert.That(buffer.Count, Is.EqualTo(3));
|
|
// computed snapshot should be interpolated in the middle
|
|
Assert.That(computed.value, Is.EqualTo(1.5).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fourth step: simulate interpolation after a long time of no updates.
|
|
// for example, a mobile user might put the app in the
|
|
// background for a minute.
|
|
[Test]
|
|
public void Compute_Step4_InterpolateAfterLongPause()
|
|
{
|
|
// add two immediate, and one that arrives 100s later
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 0);
|
|
SimpleSnapshot second = new SimpleSnapshot(1, 1, 1);
|
|
SimpleSnapshot third = new SimpleSnapshot(101, 2, 101);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
buffer.Add(third.remoteTimestamp, third);
|
|
|
|
// compute where we are half way between first and second,
|
|
// and now are updated 1 minute later.
|
|
double localTime = 103; // 1011+bufferTime so third snapshot is old enough
|
|
double deltaTime = 98.5; // 99s - interpolation time
|
|
double interpolationTime = 0.5; // half way between first and second
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started at 0.5, right between first & second.
|
|
// we received another snapshot at t=101.
|
|
// delta = 98.5 seconds
|
|
// => interpolationTime = 99
|
|
// => overshoots second goal, so we move to third goal and subtract 1
|
|
// => so we should be at 98 now
|
|
Assert.That(interpolationTime, Is.EqualTo(98));
|
|
// we moved to the next snapshot. so only 2 should be in buffer now.
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
// delta between second and third is 100.
|
|
// interpolationTime is at 98
|
|
// interpolationTime is relative to second.time
|
|
// => InverseLerp(1, 101, 1 + 98) = 0.98
|
|
// which is at 98% of the value
|
|
// => Lerp(1, 101, 0.98): 101-1 is 100. 98% are 98. relative to '1'
|
|
// makes it 99.
|
|
Assert.That(computed.value, Is.EqualTo(99).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fourth step: catchup should be considered if buffer gets too large
|
|
[Test]
|
|
public void Compute_Step4_InterpolateWithCatchup()
|
|
{
|
|
// add two old enough snapshots
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 1);
|
|
SimpleSnapshot second = new SimpleSnapshot(1, 1, 2);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
|
|
// start applying 25% catchup per excess when > 2.
|
|
int catchupThreshold = 2;
|
|
float catchupMultiplier = 0.25f;
|
|
|
|
// two excess snapshots to make sure that multiplier is accumulated
|
|
SimpleSnapshot excess1 = new SimpleSnapshot(2, 2, 3);
|
|
SimpleSnapshot excess2 = new SimpleSnapshot(3, 3, 4);
|
|
buffer.Add(excess1.remoteTimestamp, excess1);
|
|
buffer.Add(excess2.remoteTimestamp, excess2);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
double localTime = 3;
|
|
double deltaTime = 0.5;
|
|
double interpolationTime = 0;
|
|
float bufferTime = 2;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started just now, from 0.
|
|
// and deltaTime is 0.5 + 50% catchup, so we should be at 0.75 now
|
|
Assert.That(interpolationTime, Is.EqualTo(0.75));
|
|
// buffer should be untouched, we are still interpolating between
|
|
// the first two.
|
|
Assert.That(buffer.Count, Is.EqualTo(4));
|
|
// computed snapshot should be interpolated in 3/4 because
|
|
// interpolationTime is at 3/4
|
|
Assert.That(computed.value, Is.EqualTo(1.75).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fifth step: interpolation time overshoots the end while waiting for
|
|
// more snapshots.
|
|
//
|
|
// IMPORTANT: we should NOT extrapolate & predict while waiting for more
|
|
// snapshots as this would introduce a whole range of issues:
|
|
// * player might be extrapolated WAY out if we wait for long
|
|
// * player might be extrapolated behind walls
|
|
// * once we receive a new snapshot, we would interpolate
|
|
// not from the last valid position, but from the
|
|
// extrapolated position. this could be ANYWHERE. the
|
|
// player might get stuck in walls, etc.
|
|
// => we are NOT doing client side prediction & rollback here
|
|
// => we are simply interpolating with known, valid positions
|
|
//
|
|
// NOTE: to reproduce the issue in a real example:
|
|
// * open mirror benchmark example
|
|
// * editor=host 1000+ monsters & deep profiling for LOW FPS
|
|
// * build=client
|
|
// * move around client
|
|
// * see it all over the place in editor because it extrapolates,
|
|
// ends up at the wrong start positions and gets worse from
|
|
// there.
|
|
//
|
|
// video: https://gyazo.com/8de68f0a821449d7b9a8424e2c9e3ff8
|
|
// (or see Mirror/Docs/Screenshots/NT Snap. Interp./extrapolation issues)
|
|
[Test]
|
|
public void Compute_Step5_OvershootWithoutEnoughSnapshots()
|
|
{
|
|
// add two old enough snapshots
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 1);
|
|
SimpleSnapshot second = new SimpleSnapshot(1, 1, 2);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
// -> interpolation time is already at '1' at the end.
|
|
// -> compute will add 0.5 deltaTime
|
|
// -> so we should NOT overshoot aka extrapolate beyond second snap.
|
|
double localTime = 3;
|
|
double deltaTime = 0.5;
|
|
double interpolationTime = 1;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started at the end = 1
|
|
// and deltaTime is 0.5, so it's at 1.5 internally.
|
|
//
|
|
// BUT there's NO reason to overshoot interpolationTime if there's
|
|
// no other snapshots to move to.
|
|
// interpolationTime overshoot is only for smooth transitions WHILE
|
|
// moving.
|
|
// for example, if we keep overshooting to 100, then we would
|
|
// instantly skip the next 20 snapshots.
|
|
// => so it should be capped at second.remoteTime
|
|
Assert.That(interpolationTime, Is.EqualTo(1));
|
|
// buffer should be untouched, we are still interpolating between the two
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
// computed snapshot should NOT extrapolate beyond second snap.
|
|
Assert.That(computed.value, Is.EqualTo(2).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fifth step: interpolation time overshoots the end while having more
|
|
// snapshots available.
|
|
// BUT: the next snapshot isn't old enough yet.
|
|
// we shouldn't move there until old enough.
|
|
// for the same reason we don't move to first, second
|
|
// until they are old enough.
|
|
// => always need to be 'bufferTime' old.
|
|
[Test]
|
|
public void Compute_Step5_OvershootWithEnoughSnapshots_NextIsntOldEnough()
|
|
{
|
|
// add two old enough snapshots
|
|
// (localTime - bufferTime)
|
|
//
|
|
// IMPORTANT: second.time needs to be != second.time-first.time
|
|
// to guarantee that we cap interpolationTime (which is
|
|
// RELATIVE from 0..delta) at delta, not at second.time.
|
|
// this was a bug before.
|
|
SimpleSnapshot first = new SimpleSnapshot(1, 1, 1);
|
|
SimpleSnapshot second = new SimpleSnapshot(2, 2, 2);
|
|
// IMPORTANT: third snapshot needs to be:
|
|
// - a different time delta
|
|
// to test if overflow is correct if deltas are different.
|
|
// it's not obvious if we ever use t ratio between [0,1] where an
|
|
// overflow of 0.1 between A,B could speed up B,C interpolation if
|
|
// that's not the same delta, since t is a ratio.
|
|
// - a different value delta to check if it really _interpolates_,
|
|
// not just extrapolates further after A,B
|
|
SimpleSnapshot third = new SimpleSnapshot(4, 4, 4);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
buffer.Add(third.remoteTimestamp, third);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
// -> interpolation time is already at '1' at the end.
|
|
// -> compute will add 0.5 deltaTime
|
|
// -> so we overshoot beyond the second one and move to the next
|
|
//
|
|
// localTime is at 4
|
|
// third snapshot localTime is at 4.
|
|
// bufferTime is 2, so it is NOT old enough and we should wait!
|
|
double localTime = 4;
|
|
double deltaTime = 0.5;
|
|
double interpolationTime = 1;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should still spit out a result between first & second.
|
|
Assert.That(result, Is.True);
|
|
// interpolation started at the end = 1
|
|
// and deltaTime is 0.5, so we were at 1.5 internally.
|
|
//
|
|
// BUT there's NO reason to overshoot interpolationTime while we
|
|
// wait for the next snapshot which isn't old enough.
|
|
// we stopped movement anyway.
|
|
// interpolationTime overshoot is only for smooth transitions WHILE
|
|
// moving.
|
|
// for example, if we overshoot to 100 while waiting, then we would
|
|
// instantly skip the next 20 snapshots.
|
|
// => so it should be capped at max
|
|
// => which is always 0..delta, NOT first.time .. second.time!!
|
|
Assert.That(interpolationTime, Is.EqualTo(1));
|
|
// buffer should be untouched. shouldn't have moved to third yet.
|
|
Assert.That(buffer.Count, Is.EqualTo(3));
|
|
// computed snapshot should be all the way at second snapshot.
|
|
Assert.That(computed.value, Is.EqualTo(2).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fifth step: interpolation time overshoots the end while having more
|
|
// snapshots available.
|
|
[Test]
|
|
public void Compute_Step5_OvershootWithEnoughSnapshots_MovesToNextSnapshotIfOldEnough()
|
|
{
|
|
// add two old enough snapshots
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 1);
|
|
SimpleSnapshot second = new SimpleSnapshot(1, 1, 2);
|
|
// IMPORTANT: third snapshot needs to be:
|
|
// - a different time delta
|
|
// to test if overflow is correct if deltas are different.
|
|
// it's not obvious if we ever use t ratio between [0,1] where an
|
|
// overflow of 0.1 between A,B could speed up B,C interpolation if
|
|
// that's not the same delta, since t is a ratio.
|
|
// - a different value delta to check if it really _interpolates_,
|
|
// not just extrapolates further after A,B
|
|
SimpleSnapshot third = new SimpleSnapshot(3, 3, 4);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
buffer.Add(third.remoteTimestamp, third);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
// -> interpolation time is already at '1' at the end.
|
|
// -> compute will add 0.5 deltaTime
|
|
// -> so we overshoot beyond the second one and move to the next
|
|
//
|
|
// localTime is 5. third snapshot localTime is at 3.
|
|
// bufferTime is 2.
|
|
// so third is exactly old enough and we should move there.
|
|
double localTime = 5;
|
|
double deltaTime = 0.5;
|
|
double interpolationTime = 1;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started at the end = 1
|
|
// and deltaTime is 0.5, so we were at 1.5 internally.
|
|
// we have more snapshots, so we jump to the next and subtract '1'
|
|
// 1 + 0.5 = 1.5 => -1 => 0.5
|
|
Assert.That(interpolationTime, Is.EqualTo(0.5));
|
|
// buffer's first entry should have been removed
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
// computed snapshot should be 1/4 way between second and third
|
|
// because delta is 2 and interpolationTime is at 0.5 which is 1/4
|
|
Assert.That(computed.value, Is.EqualTo(2.5).Within(Mathf.Epsilon));
|
|
}
|
|
|
|
// fifth step: interpolation time overshoots 2x the end while having
|
|
// >= 2 more snapshots available. it should correctly jump
|
|
// ahead the first pending one to the second one.
|
|
[Test]
|
|
public void Compute_Step5_OvershootWithEnoughSnapshots_2x_MovesToSecondNextSnapshot()
|
|
{
|
|
// add two old enough snapshots
|
|
// (localTime - bufferTime)
|
|
SimpleSnapshot first = new SimpleSnapshot(0, 0, 1);
|
|
SimpleSnapshot second = new SimpleSnapshot(1, 1, 2);
|
|
// IMPORTANT: third snapshot needs to be:
|
|
// - a different time delta
|
|
// to test if overflow is correct if deltas are different.
|
|
// it's not obvious if we ever use t ratio between [0,1] where an
|
|
// overflow of 0.1 between A,B could speed up B,C interpolation if
|
|
// that's not the same delta, since t is a ratio.
|
|
// - a different value delta to check if it really _interpolates_,
|
|
// not just extrapolates further after A,B
|
|
SimpleSnapshot third = new SimpleSnapshot(3, 3, 4);
|
|
SimpleSnapshot fourth = new SimpleSnapshot(5, 5, 6);
|
|
buffer.Add(first.remoteTimestamp, first);
|
|
buffer.Add(second.remoteTimestamp, second);
|
|
buffer.Add(third.remoteTimestamp, third);
|
|
buffer.Add(fourth.remoteTimestamp, fourth);
|
|
|
|
// compute with initialized remoteTime and buffer time of 2 seconds
|
|
// and a delta time to be sure that we move along it no matter what.
|
|
// -> interpolation time is already at '1' at the end.
|
|
// -> compute will add 1.5 deltaTime
|
|
// -> so we should overshoot beyond second and third even
|
|
//
|
|
// localTime is 7. fourth snapshot localTime is at 5.
|
|
// bufferTime is 2.
|
|
// so fourth is exactly old enough and we should move there.
|
|
double localTime = 7;
|
|
double deltaTime = 2.5;
|
|
double interpolationTime = 1;
|
|
float bufferTime = 2;
|
|
int catchupThreshold = Int32.MaxValue;
|
|
float catchupMultiplier = 0;
|
|
bool result = SnapshotInterpolation.Compute(localTime, deltaTime, ref interpolationTime, bufferTime, buffer, catchupThreshold, catchupMultiplier, SimpleSnapshot.Interpolate, out SimpleSnapshot computed);
|
|
|
|
// should spit out the interpolated snapshot
|
|
Assert.That(result, Is.True);
|
|
// interpolation started at the end = 1
|
|
// and deltaTime is 2.5, so we were at 4.5 internally.
|
|
// we have more snapshots, so we:
|
|
// * jump to third, subtract delta of 1-0 = 1 => 2.5
|
|
// * jump to fourth, subtract delta of 3-1 = 2 => 0.5
|
|
// * end up at 0.5 again, between third and fourth
|
|
Assert.That(interpolationTime, Is.EqualTo(0.5));
|
|
// buffer's first entry should have been removed
|
|
Assert.That(buffer.Count, Is.EqualTo(2));
|
|
// computed snapshot should be 1/4 way between second and third
|
|
// because delta is 2 and interpolationTime is at 0.5 which is 1/4
|
|
Assert.That(computed.value, Is.EqualTo(4.5).Within(Mathf.Epsilon));
|
|
}
|
|
}
|
|
}
|