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.
|
||||
Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime);
|
||||
|
||||
// insert the corrected state and adjust the next state's delta.
|
||||
// this is because deltas are always relative to the previous
|
||||
// state. and since we inserted another, we need to readjust.
|
||||
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;
|
||||
// insert the correction and correct the history on top of it.
|
||||
// returns the final recomputed state after rewinding.
|
||||
RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, stateHistoryLimit, state, before, after, afterIndex);
|
||||
|
||||
// log, draw & apply the final position.
|
||||
// always do this here, not when iterating above, in case we aren't iterating.
|
||||
// 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.DrawLine(rb.position, recomputed.position, Color.green, lineTime);
|
||||
ApplyState(recomputed.position, recomputed.rotation, recomputed.velocity);
|
||||
|
@ -31,14 +31,6 @@ public RigidbodyState(
|
||||
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)
|
||||
{
|
||||
return new RigidbodyState
|
||||
|
@ -16,14 +16,6 @@ public interface PredictedState
|
||||
|
||||
Vector3 velocity { 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
|
||||
@ -90,17 +82,16 @@ public static bool Sample<T>(
|
||||
return false;
|
||||
}
|
||||
|
||||
// when receiving a correction from the server, we want to insert it
|
||||
// into the client's state history.
|
||||
// -> if there's already a state at timestamp, replace
|
||||
// -> otherwise insert and adjust the next state's delta
|
||||
// TODO test coverage
|
||||
public static void InsertCorrection<T>(
|
||||
// inserts a server state into the client's history.
|
||||
// readjust the deltas of the states after the inserted one.
|
||||
// returns the corrected final position.
|
||||
public static T CorrectHistory<T>(
|
||||
SortedList<double, T> stateHistory,
|
||||
int stateHistoryLimit,
|
||||
T corrected, // corrected state with timestamp
|
||||
T before, // state in history before the correction
|
||||
T after) // state in history after the correction
|
||||
T corrected, // corrected state with timestamp
|
||||
T before, // state in history before 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
|
||||
{
|
||||
// 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
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// client may need to correct parts of the history after receiving server state.
|
||||
// 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
|
||||
// second step: readjust all absolute values by rewinding client's delta moves on top of it.
|
||||
T last = corrected;
|
||||
for (int i = startIndex; i < stateHistory.Count; ++i)
|
||||
for (int i = afterIndex; i < stateHistory.Count; ++i)
|
||||
{
|
||||
double key = stateHistory.Keys[i];
|
||||
T entry = stateHistory.Values[i];
|
||||
@ -175,7 +157,7 @@ public static T CorrectHistory<T>(
|
||||
last = entry;
|
||||
}
|
||||
|
||||
// return the recomputed state after all deltas were applied to the correction
|
||||
// third step: return the final recomputed state.
|
||||
return last;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Tests
|
||||
{
|
||||
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]
|
||||
public void Sample_Empty()
|
||||
{
|
||||
@ -89,5 +108,93 @@ public void Sample_MultipleEntries()
|
||||
Assert.That(afterIndex, Is.EqualTo(2));
|
||||
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