| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 | // 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;        }    }}
 |