mirror of
https://github.com/DerTyp7/defrain-shooter-unity.git
synced 2025-10-30 13:07:10 +01:00
CHANGED TO MIRROR
This commit is contained in:
20
Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs
Normal file
20
Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Snapshot interface so we can reuse it for all kinds of systems.
|
||||
// for example, NetworkTransform, NetworkRigidbody, CharacterController etc.
|
||||
// NOTE: we use '<T>' and 'where T : Snapshot' to avoid boxing.
|
||||
// List<Snapshot> would cause allocations through boxing.
|
||||
namespace Mirror
|
||||
{
|
||||
public interface Snapshot
|
||||
{
|
||||
// snapshots have two timestamps:
|
||||
// -> the remote timestamp (when it was sent by the remote)
|
||||
// used to interpolate.
|
||||
// -> the local timestamp (when we received it)
|
||||
// used to know if the first two snapshots are old enough to start.
|
||||
//
|
||||
// IMPORTANT: the timestamp does _NOT_ need to be sent over the
|
||||
// network. simply get it from batching.
|
||||
double remoteTimestamp { get; set; }
|
||||
double localTimestamp { get; set; }
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta
Normal file
11
Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12afea28fdb94154868a0a3b7a9df55b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,302 @@
|
||||
// snapshot interpolation algorithms only,
|
||||
// independent from Unity/NetworkTransform/MonoBehaviour/Mirror/etc.
|
||||
// the goal is to remove all the magic from it.
|
||||
// => a standalone snapshot interpolation algorithm
|
||||
// => that can be simulated with unit tests easily
|
||||
//
|
||||
// BOXING: in C#, uses <T> does not box! passing the interface would box!
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class SnapshotInterpolation
|
||||
{
|
||||
// insert into snapshot buffer if newer than first entry
|
||||
// this should ALWAYS be used when inserting into a snapshot buffer!
|
||||
public static void InsertIfNewEnough<T>(T snapshot, SortedList<double, T> buffer)
|
||||
where T : Snapshot
|
||||
{
|
||||
// we need to drop any snapshot which is older ('<=')
|
||||
// the snapshots we are already working with.
|
||||
double timestamp = snapshot.remoteTimestamp;
|
||||
|
||||
// if size == 1, then only add snapshots that are newer.
|
||||
// for example, a snapshot before the first one might have been
|
||||
// lagging.
|
||||
if (buffer.Count == 1 &&
|
||||
timestamp <= buffer.Values[0].remoteTimestamp)
|
||||
return;
|
||||
|
||||
// for size >= 2, we are already interpolating between the first two
|
||||
// so only add snapshots that are newer than the second entry.
|
||||
// aka 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.
|
||||
if (buffer.Count >= 2 &&
|
||||
timestamp <= buffer.Values[1].remoteTimestamp)
|
||||
return;
|
||||
|
||||
// otherwise sort it into the list
|
||||
// an UDP messages might arrive twice sometimes.
|
||||
// SortedList throws if key already exists, so check.
|
||||
if (!buffer.ContainsKey(timestamp))
|
||||
buffer.Add(timestamp, snapshot);
|
||||
}
|
||||
|
||||
// helper function to check if we have >= n old enough snapshots.
|
||||
// NOTE: we check LOCAL timestamp here.
|
||||
// not REMOTE timestamp.
|
||||
// we buffer for 'bufferTime' locally.
|
||||
// it has nothing to do with remote timestamp.
|
||||
// and we wouldn't know the current remoteTime either.
|
||||
public static bool HasAmountOlderThan<T>(SortedList<double, T> buffer, double threshold, int amount)
|
||||
where T : Snapshot =>
|
||||
buffer.Count >= amount &&
|
||||
buffer.Values[amount - 1].localTimestamp <= threshold;
|
||||
|
||||
// calculate catchup.
|
||||
// the goal is to buffer 'bufferTime' snapshots.
|
||||
// for whatever reason, we might see growing buffers.
|
||||
// in which case we should speed up to avoid ever growing delay.
|
||||
// -> everything after 'threshold' is multiplied by 'multiplier'
|
||||
public static double CalculateCatchup<T>(SortedList<double, T> buffer, int catchupThreshold, double catchupMultiplier)
|
||||
where T : Snapshot
|
||||
{
|
||||
// NOTE: we count ALL buffer entires > threshold as excess.
|
||||
// not just the 'old enough' ones.
|
||||
// if buffer keeps growing, we have to catch up no matter what.
|
||||
int excess = buffer.Count - catchupThreshold;
|
||||
return excess > 0 ? excess * catchupMultiplier : 0;
|
||||
}
|
||||
|
||||
// get first & second buffer entries and delta between them.
|
||||
// helper function because we use this several times.
|
||||
// => assumes at least two entries in buffer.
|
||||
public static void GetFirstSecondAndDelta<T>(SortedList<double, T> buffer, out T first, out T second, out double delta)
|
||||
where T : Snapshot
|
||||
{
|
||||
// get first & second
|
||||
first = buffer.Values[0];
|
||||
second = buffer.Values[1];
|
||||
|
||||
// delta between first & second is needed a lot
|
||||
delta = second.remoteTimestamp - first.remoteTimestamp;
|
||||
}
|
||||
|
||||
// the core snapshot interpolation algorithm.
|
||||
// for a given remoteTime, interpolationTime and buffer,
|
||||
// we tick the snapshot simulation once.
|
||||
// => it's the same one on server and client
|
||||
// => should be called every Update() depending on authority
|
||||
//
|
||||
// time: LOCAL time since startup in seconds. like Unity's Time.time.
|
||||
// deltaTime: Time.deltaTime from Unity. parameter for easier tests.
|
||||
// interpolationTime: time in interpolation. moved along deltaTime.
|
||||
// between [0, delta] where delta is snapshot
|
||||
// B.timestamp - A.timestamp.
|
||||
// IMPORTANT:
|
||||
// => we use actual time instead of a relative
|
||||
// t [0,1] because overshoot is easier to handle.
|
||||
// if relative t overshoots but next snapshots are
|
||||
// further apart than the current ones, it's not
|
||||
// obvious how to calculate it.
|
||||
// => for example, if t = 3 every time we skip we would have to
|
||||
// make sure to adjust the subtracted value relative to the
|
||||
// skipped delta. way too complex.
|
||||
// => actual time can overshoot without problems.
|
||||
// we know it's always by actual time.
|
||||
// bufferTime: time in seconds that we buffer snapshots.
|
||||
// buffer: our buffer of snapshots.
|
||||
// Compute() assumes full integrity of the snapshots.
|
||||
// for example, when interpolating between A=0 and C=2,
|
||||
// make sure that you don't add B=1 between A and C if that
|
||||
// snapshot arrived after we already started interpolating.
|
||||
// => InsertIfNewEnough needs to protect against the 'ACB' problem
|
||||
// catchupThreshold: amount of buffer entries after which we start to
|
||||
// accelerate to catch up.
|
||||
// if 'bufferTime' is 'sendInterval * 3', then try
|
||||
// a value > 3 like 6.
|
||||
// catchupMultiplier: catchup by % per additional excess buffer entry
|
||||
// over the amount of 'catchupThreshold'.
|
||||
// Interpolate: interpolates one snapshot to another, returns the result
|
||||
// T Interpolate(T from, T to, double t);
|
||||
// => needs to be Func<T> instead of a function in the Snapshot
|
||||
// interface because that would require boxing.
|
||||
// => make sure to only allocate that function once.
|
||||
//
|
||||
// returns
|
||||
// 'true' if it spit out a snapshot to apply.
|
||||
// 'false' means computation moved along, but nothing to apply.
|
||||
public static bool Compute<T>(
|
||||
double time,
|
||||
double deltaTime,
|
||||
ref double interpolationTime,
|
||||
double bufferTime,
|
||||
SortedList<double, T> buffer,
|
||||
int catchupThreshold,
|
||||
float catchupMultiplier,
|
||||
Func<T, T, double, T> Interpolate,
|
||||
out T computed)
|
||||
where T : Snapshot
|
||||
{
|
||||
// we buffer snapshots for 'bufferTime'
|
||||
// for example:
|
||||
// * we buffer for 3 x sendInterval = 300ms
|
||||
// * the idea is to wait long enough so we at least have a few
|
||||
// snapshots to interpolate between
|
||||
// * we process anything older 100ms immediately
|
||||
//
|
||||
// IMPORTANT: snapshot timestamps are _remote_ time
|
||||
// we need to interpolate and calculate buffer lifetimes based on it.
|
||||
// -> we don't know remote's current time
|
||||
// -> NetworkTime.time fluctuates too much, that's no good
|
||||
// -> we _could_ calculate an offset when the first snapshot arrives,
|
||||
// but if there was high latency then we'll always calculate time
|
||||
// with high latency
|
||||
// -> at any given time, we are interpolating from snapshot A to B
|
||||
// => seems like A.timestamp += deltaTime is a good way to do it
|
||||
|
||||
computed = default;
|
||||
//Debug.Log($"{name} snapshotbuffer={buffer.Count}");
|
||||
|
||||
// we always need two OLD ENOUGH snapshots to interpolate.
|
||||
// otherwise there's nothing to do.
|
||||
double threshold = time - bufferTime;
|
||||
if (!HasAmountOlderThan(buffer, threshold, 2))
|
||||
return false;
|
||||
|
||||
// multiply deltaTime by catchup.
|
||||
// for example, assuming a catch up of 50%:
|
||||
// - deltaTime = 1s => 1.5s
|
||||
// - deltaTime = 0.1s => 0.15s
|
||||
// in other words, variations in deltaTime don't matter.
|
||||
// simply multiply. that's just how time works.
|
||||
// (50% catch up means 0.5, so we multiply by 1.5)
|
||||
//
|
||||
// if '0' catchup then we multiply by '1', which changes nothing.
|
||||
// (faster branch prediction)
|
||||
double catchup = CalculateCatchup(buffer, catchupThreshold, catchupMultiplier);
|
||||
deltaTime *= (1 + catchup);
|
||||
|
||||
// interpolationTime starts at 0 and we add deltaTime to move
|
||||
// along the interpolation.
|
||||
//
|
||||
// ONLY while we have snapshots to interpolate.
|
||||
// otherwise we might increase it to infinity which would lead
|
||||
// to skipping the next snapshots entirely.
|
||||
//
|
||||
// IMPORTANT: interpolationTime as actual time instead of
|
||||
// t [0,1] allows us to overshoot and subtract easily.
|
||||
// if t was [0,1], and we overshoot by 0.1, that's a
|
||||
// RELATIVE overshoot for the delta between B.time - A.time.
|
||||
// => if the next C.time - B.time is not the same delta,
|
||||
// then the relative overshoot would speed up or slow
|
||||
// down the interpolation! CAREFUL.
|
||||
//
|
||||
// IMPORTANT: we NEVER add deltaTime to 'time'.
|
||||
// 'time' is already NOW. that's how Unity works.
|
||||
interpolationTime += deltaTime;
|
||||
|
||||
// get first & second & delta
|
||||
GetFirstSecondAndDelta(buffer, out T first, out T second, out double delta);
|
||||
|
||||
// reached goal and have more old enough snapshots in buffer?
|
||||
// then skip and move to next.
|
||||
// for example, if we have snapshots at t=1,2,3
|
||||
// and we are at interpolationTime = 2.5, then
|
||||
// we should skip the first one, subtract delta and interpolate
|
||||
// between 2,3 instead.
|
||||
//
|
||||
// IMPORTANT: we only ever use old enough snapshots.
|
||||
// if we wouldn't check for old enough, then we would
|
||||
// move to the next one, interpolate a little bit,
|
||||
// and then in next compute() wait again because it
|
||||
// wasn't old enough yet.
|
||||
while (interpolationTime >= delta &&
|
||||
HasAmountOlderThan(buffer, threshold, 3))
|
||||
{
|
||||
// subtract exactly delta from interpolation time
|
||||
// instead of setting to '0', where we would lose the
|
||||
// overshoot part and see jitter again.
|
||||
//
|
||||
// IMPORTANT: subtracting delta TIME works perfectly.
|
||||
// subtracting '1' from a ratio of t [0,1] would
|
||||
// leave the overshoot as relative between the
|
||||
// next delta. if next delta is different, then
|
||||
// overshoot would be bigger than planned and
|
||||
// speed up the interpolation.
|
||||
interpolationTime -= delta;
|
||||
//Debug.LogWarning($"{name} overshot and is now at: {interpolationTime}");
|
||||
|
||||
// remove first, get first, second & delta again after change.
|
||||
buffer.RemoveAt(0);
|
||||
GetFirstSecondAndDelta(buffer, out first, out second, out delta);
|
||||
|
||||
// NOTE: it's worth consider spitting out all snapshots
|
||||
// that we skipped, in case someone still wants to move
|
||||
// along them to avoid physics collisions.
|
||||
// * for NetworkTransform it's unnecessary as we always
|
||||
// set transform.position, which can go anywhere.
|
||||
// * for CharacterController it's worth considering
|
||||
}
|
||||
|
||||
// interpolationTime is actual time, NOT a 't' ratio [0,1].
|
||||
// we need 't' between [0,1] relative.
|
||||
// InverseLerp calculates just that.
|
||||
// InverseLerp CLAMPS between [0,1] and DOES NOT extrapolate!
|
||||
// => we already skipped ahead as many as possible above.
|
||||
// => we do NOT extrapolate for the reasons below.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// SEE TEST: Compute_Step5_OvershootWithoutEnoughSnapshots_NeverExtrapolates()
|
||||
double t = Mathd.InverseLerp(first.remoteTimestamp, second.remoteTimestamp, first.remoteTimestamp + interpolationTime);
|
||||
//Debug.Log($"InverseLerp({first.remoteTimestamp:F2}, {second.remoteTimestamp:F2}, {first.remoteTimestamp} + {interpolationTime:F2}) = {t:F2} snapshotbuffer={buffer.Count}");
|
||||
|
||||
// interpolate snapshot, return true to indicate we computed one
|
||||
computed = Interpolate(first, second, t);
|
||||
|
||||
// interpolationTime:
|
||||
// overshooting is ONLY allowed for smooth transitions when
|
||||
// immediately moving to the NEXT snapshot afterwards.
|
||||
//
|
||||
// if there is ANY break, for example:
|
||||
// * reached second snapshot and waiting for more
|
||||
// * reached second snapshot and next one isn't old enough yet
|
||||
//
|
||||
// then we SHOULD NOT overshoot because:
|
||||
// * increasing interpolationTime by deltaTime while waiting
|
||||
// would make it grow HUGE to 100+.
|
||||
// * once we have more snapshots, we would skip most of them
|
||||
// instantly instead of actually interpolating through them.
|
||||
//
|
||||
// in other words, if we DON'T have >= 3 old enough.
|
||||
if (!HasAmountOlderThan(buffer, threshold, 3))
|
||||
{
|
||||
// interpolationTime is always from 0..delta.
|
||||
// so we cap it at delta.
|
||||
// DO NOT cap it at second.remoteTimestamp.
|
||||
// (that's why when interpolating the third parameter is
|
||||
// first.time + interpolationTime)
|
||||
// => covered with test:
|
||||
// Compute_Step5_OvershootWithEnoughSnapshots_NextIsntOldEnough()
|
||||
interpolationTime = Math.Min(interpolationTime, delta);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72c16070d85334011853813488ab1431
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user