mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
Prediction: InsertCorrection moved into CorrectHistory, with complete step by step test coverage for the algorithm
This commit is contained in:
parent
383aff2698
commit
79c4298db9
@ -406,18 +406,14 @@ void OnReceivedState(double timestamp, RigidbodyState state)
|
|||||||
// helps to compare with the interpolated/applied correction locally.
|
// helps to compare with the interpolated/applied correction locally.
|
||||||
Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime);
|
Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime);
|
||||||
|
|
||||||
// insert the corrected state and adjust the next state's delta.
|
// insert the correction and correct the history on top of it.
|
||||||
// this is because deltas are always relative to the previous
|
// returns the final recomputed state after rewinding.
|
||||||
// state. and since we inserted another, we need to readjust.
|
RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, stateHistoryLimit, state, before, after, afterIndex);
|
||||||
Prediction.InsertCorrection(stateHistory, stateHistoryLimit, state, before, after);
|
|
||||||
|
|
||||||
// insert the corrected state and correct all reapply the deltas after it.
|
|
||||||
RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, state, afterIndex);
|
|
||||||
int correctedAmount = stateHistory.Count - afterIndex;
|
|
||||||
|
|
||||||
// log, draw & apply the final position.
|
// log, draw & apply the final position.
|
||||||
// always do this here, not when iterating above, in case we aren't iterating.
|
// always do this here, not when iterating above, in case we aren't iterating.
|
||||||
// for example, on same machine with near zero latency.
|
// for example, on same machine with near zero latency.
|
||||||
|
// int correctedAmount = stateHistory.Count - afterIndex;
|
||||||
// Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}");
|
// Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}");
|
||||||
Debug.DrawLine(rb.position, recomputed.position, Color.green, lineTime);
|
Debug.DrawLine(rb.position, recomputed.position, Color.green, lineTime);
|
||||||
ApplyState(recomputed.position, recomputed.rotation, recomputed.velocity);
|
ApplyState(recomputed.position, recomputed.rotation, recomputed.velocity);
|
||||||
|
@ -31,14 +31,6 @@ public RigidbodyState(
|
|||||||
this.velocity = velocity;
|
this.velocity = velocity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjust the deltas after inserting a correction between this one and the previous one.
|
|
||||||
public void AdjustDeltas(float multiplier)
|
|
||||||
{
|
|
||||||
positionDelta = Vector3.Lerp(Vector3.zero, positionDelta, multiplier);
|
|
||||||
// TODO if we have have a rotation delta, then scale it here too
|
|
||||||
velocityDelta = Vector3.Lerp(Vector3.zero, velocityDelta, multiplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t)
|
public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t)
|
||||||
{
|
{
|
||||||
return new RigidbodyState
|
return new RigidbodyState
|
||||||
|
@ -16,14 +16,6 @@ public interface PredictedState
|
|||||||
|
|
||||||
Vector3 velocity { get; set; }
|
Vector3 velocity { get; set; }
|
||||||
Vector3 velocityDelta { get; set; }
|
Vector3 velocityDelta { get; set; }
|
||||||
|
|
||||||
// predicted states should have absolute and delta values, for example:
|
|
||||||
// Vector3 position;
|
|
||||||
// Vector3 positionDelta; // from last to here
|
|
||||||
// when inserting a correction between this one and the one before,
|
|
||||||
// we need to adjust the delta:
|
|
||||||
// positionDelta *= multiplier;
|
|
||||||
void AdjustDeltas(float multiplier);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Prediction
|
public static class Prediction
|
||||||
@ -90,17 +82,16 @@ public static bool Sample<T>(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// when receiving a correction from the server, we want to insert it
|
// inserts a server state into the client's history.
|
||||||
// into the client's state history.
|
// readjust the deltas of the states after the inserted one.
|
||||||
// -> if there's already a state at timestamp, replace
|
// returns the corrected final position.
|
||||||
// -> otherwise insert and adjust the next state's delta
|
public static T CorrectHistory<T>(
|
||||||
// TODO test coverage
|
|
||||||
public static void InsertCorrection<T>(
|
|
||||||
SortedList<double, T> stateHistory,
|
SortedList<double, T> stateHistory,
|
||||||
int stateHistoryLimit,
|
int stateHistoryLimit,
|
||||||
T corrected, // corrected state with timestamp
|
T corrected, // corrected state with timestamp
|
||||||
T before, // state in history before the correction
|
T before, // state in history before the correction
|
||||||
T after) // state in history after the correction
|
T after, // state in history after the correction
|
||||||
|
int afterIndex) // index of the 'after' value so we don't need to find it again here
|
||||||
where T: PredictedState
|
where T: PredictedState
|
||||||
{
|
{
|
||||||
// respect the limit
|
// respect the limit
|
||||||
@ -142,24 +133,15 @@ public static void InsertCorrection<T>(
|
|||||||
double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25
|
double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25
|
||||||
|
|
||||||
// recalculate 'after.delta' with the multiplier
|
// recalculate 'after.delta' with the multiplier
|
||||||
after.AdjustDeltas((float)multiplier);
|
after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier);
|
||||||
|
after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); // TODO rotation too?
|
||||||
|
|
||||||
// write the adjusted 'after' value into the history buffer
|
// changes aren't saved until we overwrite them in the history
|
||||||
stateHistory[after.timestamp] = after;
|
stateHistory[after.timestamp] = after;
|
||||||
}
|
|
||||||
|
|
||||||
// client may need to correct parts of the history after receiving server state.
|
// second step: readjust all absolute values by rewinding client's delta moves on top of it.
|
||||||
// CorrectHistory inserts the entries from [i..n] based on 'corrected'.
|
|
||||||
// in other words, readjusts the deltas that the client moved since then.
|
|
||||||
public static T CorrectHistory<T>(
|
|
||||||
SortedList<double, T> stateHistory,
|
|
||||||
T corrected, // corrected state with timestamp
|
|
||||||
int startIndex) // first state after the inserted correction
|
|
||||||
where T: PredictedState
|
|
||||||
{
|
|
||||||
// start iterating right after the inserted state at startIndex
|
|
||||||
T last = corrected;
|
T last = corrected;
|
||||||
for (int i = startIndex; i < stateHistory.Count; ++i)
|
for (int i = afterIndex; i < stateHistory.Count; ++i)
|
||||||
{
|
{
|
||||||
double key = stateHistory.Keys[i];
|
double key = stateHistory.Keys[i];
|
||||||
T entry = stateHistory.Values[i];
|
T entry = stateHistory.Values[i];
|
||||||
@ -175,7 +157,7 @@ public static T CorrectHistory<T>(
|
|||||||
last = entry;
|
last = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the recomputed state after all deltas were applied to the correction
|
// third step: return the final recomputed state.
|
||||||
return last;
|
return last;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Mirror.Tests
|
namespace Mirror.Tests
|
||||||
{
|
{
|
||||||
public class PredictionTests
|
public class PredictionTests
|
||||||
{
|
{
|
||||||
|
struct TestState : PredictedState
|
||||||
|
{
|
||||||
|
public double timestamp { get; set; }
|
||||||
|
public Vector3 position { get; set; }
|
||||||
|
public Vector3 positionDelta { get; set; }
|
||||||
|
public Vector3 velocity { get; set; }
|
||||||
|
public Vector3 velocityDelta { get; set; }
|
||||||
|
|
||||||
|
public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vector3 velocity, Vector3 velocityDelta)
|
||||||
|
{
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.position = position;
|
||||||
|
this.positionDelta = positionDelta;
|
||||||
|
this.velocity = velocity;
|
||||||
|
this.velocityDelta = velocityDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Sample_Empty()
|
public void Sample_Empty()
|
||||||
{
|
{
|
||||||
@ -89,5 +108,93 @@ public void Sample_MultipleEntries()
|
|||||||
Assert.That(afterIndex, Is.EqualTo(2));
|
Assert.That(afterIndex, Is.EqualTo(2));
|
||||||
Assert.That(t, Is.EqualTo(0.0));
|
Assert.That(t, Is.EqualTo(0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
[Test]
|
||||||
|
public void CorrectHistory()
|
||||||
|
{
|
||||||
|
// prepare a straight forward history
|
||||||
|
SortedList<double, TestState> history = new SortedList<double, TestState>();
|
||||||
|
|
||||||
|
// (0,0,0) with delta (0,0,0) from previous:
|
||||||
|
history.Add(0, new TestState(0, new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0)));
|
||||||
|
|
||||||
|
// (1,0,0) with delta (1,0,0) from previous:
|
||||||
|
history.Add(1, new TestState(1, new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0)));
|
||||||
|
|
||||||
|
// (2,0,0) with delta (1,0,0) from previous:
|
||||||
|
history.Add(2, new TestState(2, new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0)));
|
||||||
|
|
||||||
|
// (3,0,0) with delta (1,0,0) from previous:
|
||||||
|
history.Add(3, new TestState(3, new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0)));
|
||||||
|
|
||||||
|
// client receives a correction from server between t=1 and t=2.
|
||||||
|
// exactly t=1.5 where position should be 1.5, server says it's +0.1 = 1.6
|
||||||
|
// deltas are zero because that's how PredictedBody.Serialize sends them, alwasy at zero.
|
||||||
|
TestState correction = new TestState(1.5, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero);
|
||||||
|
|
||||||
|
// Sample() will find that the value before correction is at t=1 and after at t=2.
|
||||||
|
Assert.That(Prediction.Sample(history, correction.timestamp, out TestState before, out TestState after, out int afterIndex, out double t), Is.True);
|
||||||
|
Assert.That(before.timestamp, Is.EqualTo(1));
|
||||||
|
Assert.That(after.timestamp, Is.EqualTo(2));
|
||||||
|
Assert.That(afterIndex, Is.EqualTo(2));
|
||||||
|
Assert.That(t, Is.EqualTo(0.5));
|
||||||
|
|
||||||
|
// ... this is where we would interpolate (before, after, 0.5) and
|
||||||
|
// compare to decide if we need to correct.
|
||||||
|
// assume we decided that a correction is necessary ...
|
||||||
|
|
||||||
|
// correct history with the received server state
|
||||||
|
const int historyLimit = 32;
|
||||||
|
Prediction.CorrectHistory(history, historyLimit, correction, before, after, afterIndex);
|
||||||
|
|
||||||
|
// there should be 4 initial + 1 corrected = 5 entries now
|
||||||
|
Assert.That(history.Count, Is.EqualTo(5));
|
||||||
|
|
||||||
|
// first entry at t=0 should be unchanged, since we corrected after that one.
|
||||||
|
Assert.That(history.Keys[0], Is.EqualTo(0));
|
||||||
|
Assert.That(history.Values[0].position.x, Is.EqualTo(0));
|
||||||
|
Assert.That(history.Values[0].positionDelta.x, Is.EqualTo(0));
|
||||||
|
Assert.That(history.Values[0].velocity.x, Is.EqualTo(0));
|
||||||
|
Assert.That(history.Values[0].velocityDelta.x, Is.EqualTo(0));
|
||||||
|
|
||||||
|
// second entry at t=1 should be unchanged, since we corrected after that one.
|
||||||
|
Assert.That(history.Keys[1], Is.EqualTo(1));
|
||||||
|
Assert.That(history.Values[1].position.x, Is.EqualTo(1));
|
||||||
|
Assert.That(history.Values[1].positionDelta.x, Is.EqualTo(1));
|
||||||
|
Assert.That(history.Values[1].velocity.x, Is.EqualTo(1));
|
||||||
|
Assert.That(history.Values[1].velocityDelta.x, Is.EqualTo(1));
|
||||||
|
|
||||||
|
// third entry at t=1.5 should be the received state.
|
||||||
|
// absolute values should be the correction, without any deltas since
|
||||||
|
// server doesn't send those and we don't need them.
|
||||||
|
Assert.That(history.Keys[2], Is.EqualTo(1.5));
|
||||||
|
Assert.That(history.Values[2].position.x, Is.EqualTo(1.6f).Within(0.001f));
|
||||||
|
Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0));
|
||||||
|
Assert.That(history.Values[2].velocity.x, Is.EqualTo(1.6f).Within(0.001f));
|
||||||
|
Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0));
|
||||||
|
|
||||||
|
// fourth entry at t=2:
|
||||||
|
// delta was from t=1.0 @ 1 to t=2.0 @ 2 = 1.0
|
||||||
|
// we inserted at t=1.5 which is half way between t=1 and t=2.
|
||||||
|
// the delta at t=1.5 would've been 0.5.
|
||||||
|
// => the inserted position is at t=1.6
|
||||||
|
// => add the relative delta of 0.5 = 2.1
|
||||||
|
Assert.That(history.Keys[3], Is.EqualTo(2.0));
|
||||||
|
Assert.That(history.Values[3].position.x, Is.EqualTo(2.1).Within(0.001f));
|
||||||
|
Assert.That(history.Values[3].positionDelta.x, Is.EqualTo(0.5).Within(0.001f));
|
||||||
|
Assert.That(history.Values[3].velocity.x, Is.EqualTo(2.1).Within(0.001f));
|
||||||
|
Assert.That(history.Values[3].velocityDelta.x, Is.EqualTo(0.5).Within(0.001f));
|
||||||
|
|
||||||
|
// fifth entry at t=3:
|
||||||
|
// client moved by a delta of 1 here, and that remains unchanged.
|
||||||
|
// absolute position was 3.0 but if we apply the delta of 1 to the one before at 2.1,
|
||||||
|
// we get the new position of 3.1
|
||||||
|
Assert.That(history.Keys[4], Is.EqualTo(3.0));
|
||||||
|
Assert.That(history.Values[4].position.x, Is.EqualTo(3.1).Within(0.001f));
|
||||||
|
Assert.That(history.Values[4].positionDelta.x, Is.EqualTo(1.0).Within(0.001f));
|
||||||
|
Assert.That(history.Values[4].velocity.x, Is.EqualTo(3.1).Within(0.001f));
|
||||||
|
Assert.That(history.Values[4].velocityDelta.x, Is.EqualTo(1.0).Within(0.001f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user