Prediction: merge InsertCorrection and CorrectHistory, which is more logical and more obvious to test

This commit is contained in:
mischa 2024-01-11 12:29:22 +01:00
parent 383aff2698
commit f30767e68d
4 changed files with 128 additions and 48 deletions

View File

@ -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);

View File

@ -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

View File

@ -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 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;
}
}

View File

@ -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,96 @@ 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));
// Assert.That(history.Values[3].position.x, Is.EqualTo(2.5).Within(0.001f));
// Assert.That(history.Values[3].velocity.x, Is.EqualTo(0).Within(0.001f));
// correct history after the insertion, starting corrections at index=3 (0, 1, 1.5, 2, 3)
// Prediction.CorrectHistory(history, existing, 2);
//
// Assert.That(history.Values[0].position.x, Is.EqualTo(0));
// Assert.That(history.Values[1].position.x, Is.EqualTo(1.1f).Within(0.001f));
// Assert.That(history.Values[2].position.x, Is.EqualTo(3.3f).Within(0.001f));
// Assert.That(history.Values[3].position.x, Is.EqualTo(5.5f).Within(0.001f));
}
}
}