123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- // standalone lag compensation algorithm
- // based on the Valve Networking Model:
- // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
- using System.Collections.Generic;
- namespace Mirror
- {
- public static class LagCompensation
- {
- // history is of <timestamp, capture>.
- // Queue allows for fast 'remove first' and 'append last'.
- //
- // make sure to always insert in order.
- // inserting out of order like [1,2,4,3] would cause issues.
- // can't safeguard this because Queue doesn't have .Last access.
- public static void Insert<T>(
- Queue<KeyValuePair<double, T>> history,
- int historyLimit,
- double timestamp,
- T capture)
- where T : Capture
- {
- // make space according to history limit.
- // do this before inserting, to avoid resizing past capacity.
- if (history.Count >= historyLimit)
- history.Dequeue();
- // insert
- history.Enqueue(new KeyValuePair<double, T>(timestamp, capture));
- }
- // get the two snapshots closest to a given timestamp.
- // those can be used to interpolate the exact snapshot at that time.
- // if timestamp is newer than the newest history entry, then we extrapolate.
- // 't' will be between 1 and 2, before is second last, after is last.
- // callers should Lerp(before, after, t=1.5) to extrapolate the hit.
- // see comments below for extrapolation.
- public static bool Sample<T>(
- Queue<KeyValuePair<double, T>> history,
- double timestamp, // current server time
- double interval, // capture interval
- out T before,
- out T after,
- out double t) // interpolation factor
- where T : Capture
- {
- before = default;
- after = default;
- t = 0;
- // can't sample an empty history
- // interpolation needs at least one entry.
- // extrapolation needs at least two entries.
- // can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
- if(history.Count < 2) {
- return false;
- }
- // older than oldest
- if (timestamp < history.Peek().Key) {
- return false;
- }
- // iterate through the history
- // TODO faster version: guess start index by how many 'intervals' we are behind.
- // search around that area.
- // should be O(1) most of the time, unless sampling was off.
- KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
- KeyValuePair<double, T> prevPrev = new KeyValuePair<double, T>();
- foreach(KeyValuePair<double, T> entry in history) {
- // exact match?
- if (timestamp == entry.Key) {
- before = entry.Value;
- after = entry.Value;
- t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
- return true;
- }
- // did we check beyond timestamp? then return the previous two.
- if (entry.Key > timestamp) {
- before = prev.Value;
- after = entry.Value;
- t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
- return true;
- }
- // remember the last two for extrapolation.
- // Queue doesn't have access to .Last.
- prevPrev = prev;
- prev = entry;
- }
- // newer than newest: extrapolate up to one interval.
- // let's say we capture every 100 ms:
- // 100, 200, 300, 400
- // and the server is at 499
- // if a client sends CmdFire at time 480, then there's no history entry.
- // => adding the current entry every time would be too expensive.
- // worst case we would capture at 401, 402, 403, 404, ... 100 times
- // => not extrapolating isn't great. low latency clients would be
- // punished by missing their targets since no entry at 'time' was found.
- // => extrapolation is the best solution. make sure this works as
- // expected and within limits.
- if (prev.Key < timestamp && timestamp <= prev.Key + interval) {
- // return the last two valid snapshots.
- // can't just return (after, after) because we can't extrapolate
- // if their distance is 0.
- before = prevPrev.Value;
- after = prev.Value;
- // InverseLerp will give [after, after+interval].
- // but we return [before, after, t].
- // so add +1 for the distance from before->after
- t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp);
- return true;
- }
- return false;
- }
- // never trust the client.
- // we estimate when a message was sent.
- // don't trust the client to tell us the time.
- // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
- // Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
- // => lag compensation demo estimation is off by only ~6ms
- public static double EstimateTime(double serverTime, double rtt, double bufferTime)
- {
- // packet latency is one trip from client to server, so rtt / 2
- // client view interpolation is the snapshot interpolation buffer time
- double latency = rtt / 2;
- return serverTime - latency - bufferTime;
- }
- // convenience function to draw all history gizmos.
- // this should be called from OnDrawGizmos.
- public static void DrawGizmos<T>(Queue<KeyValuePair<double, T>> history)
- where T : Capture
- {
- foreach (KeyValuePair<double, T> entry in history)
- entry.Value.DrawGizmo();
- }
- }
- }
|