// 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 . // 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( Queue> 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(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( Queue> 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 prev = new KeyValuePair(); KeyValuePair prevPrev = new KeyValuePair(); foreach(KeyValuePair 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(Queue> history) where T : Capture { foreach (KeyValuePair entry in history) entry.Value.DrawGizmo(); } } }