Dealing with Latency...?

Started by
13 comments, last by John Schultz 18 years, 10 months ago
Ok, I followed the "Targetting" tutorial on Gamedev for my little spaceship game (think subspace clone), and i'm still getting a little bit jaggedy results (it improved quite a bit, but not all the way). What i'm doing: Before I send out the player update which holds their position and velocity info, I look at a latency variable which gets filled every few seconds (this variable holds my guess at latency by sending out a packet to the server and seeing how long it takes to get a response and dividing that time in half) then I move the ship to where it will be in that in the future after that amount of latency time elapses to make up for the lag from player to server, and send off that packet to the server. The server gets the packet and sends it to all the other players. The other player recieves the original player's update, and looks at it's own latency to the server (obtained in the same way I mentioned first), and advances the ships update position even further according to that latency. Looking at the latency results, it appears one client has around a 15ms latency, the other client has around a 29ms latency. How can I improve upon this implementation to get even smoother results? Right now this is all on a LAN, and i'm kinda surprised the latency is enough to even make the movement jaggedy without interpolation of some sort in the first place. Hopefully my description of how i'm doing it right now made some sense.
Advertisement
You should constantly be computing round trip time using a filtered mean-deviation estimator:

RTT calculation.
More info here.

Part of the extra delay could be due to your simulation rate and network update rate. If you are running at 60Hz, there will be an average extra delay of 1/60*.5, or 16.667*.5 = 8.33ms.

See Dead reckoning and Ghosts for networked object motion (smoothly interpolate between object's and their ghosts).
See here for more information.
Smoothing motion.
Can you explain the formulas from your first link a bit?

mean’ = (7/8)*mean + (1/8)*(rtt)
sdev’ = (3/4)*sdev + (1/4)*abs(rtt-mean)


mainly, what are the variables 'mean', 'rtt', 'sdev', and how do I get them?
I would assume, from context, that those variables are:

rtt: raw round-trip-time sample, as measured on a single packet round-trip
mean: average round-trip-time, as used by the system
sdev: standard deviation of the round trip time

This just runs simple statistics on the round trip time in an attempt to smoothly adapt the program to changing networking conditions.
enum Bool { True, False, FileNotFound };
Ok thanks for clarifying it. I know how to get the average, but I forget how to get the standard deviation of a set of numbers. And then what do I do with the results of those formulas? Do I use the mean' or sdev' as the guessed timeframe for moving the object?

Still trying to wrap my head around the cubic spline tutorial, the description of how to implement the formulas didn't make a lot of sense.
Quote:Original post by nPawn
Ok thanks for clarifying it. I know how to get the average, but I forget how to get the standard deviation of a set of numbers. And then what do I do with the results of those formulas? Do I use the mean' or sdev' as the guessed timeframe for moving the object?


See Jacobson's paper for a complete description of the algorithm. The concepts are important, and are a good starting point. However, feel free to modify and change the design once you get an idea for how it works with your particular application. A real-time graph showing network history is extremely valuable in tuning a custom protocol.

An example Graphical Trend Analyzer (if there is any interest, I can provide the rest of the code after E3):

struct TrendAnalyzer {  int historyLength;  FIRfilterDynamic filter;  FIRfilterDynamic filterMin;  FIRfilterDynamic filterMax;  DynArrayNCD<float> sampledHistory;  DynArrayNCD<float> filteredHistory;  float absoluteMin,absoluteMax;  float filteredMin,filteredMax;  float filteredMinMaxRate;  float RANGE_INIT;  int head;  TrendAnalyzer() : sampledHistory(0), filteredHistory(0), historyLength(0) {    RANGE_INIT = 1e6;    filteredMinMaxRate = 1.f/(60.f*2.f);    reset();  }  ~TrendAnalyzer() {    free();  }  void free(void) {    sampledHistory.free();    filteredHistory.free();  } // free  void init(int _historyLength) {    free();    historyLength = _historyLength;    sampledHistory.setSizeMaxCount(historyLength);    filteredHistory.setSizeMaxCount(historyLength);    filter.init(_historyLength);    filterMin.init(_historyLength);    filterMax.init(_historyLength);    reset();  } // init  void reset(void) {    for (int i=0; i < historyLength; i++) {      sampledHistory  = 0.f;      filteredHistory = 0.f;    } // for    absoluteMin = RANGE_INIT;    absoluteMax = -absoluteMin;    filteredMin = 0.f;    filteredMax = 0.f;    head = 0;    filter.reset();    filterMin.reset();    filterMax.reset();  } // reset  void update(float sample) {    if (sample < absoluteMin) absoluteMin = sample;    if (sample > absoluteMax) absoluteMax = sample;    sampledHistory[head]  = sample;    filteredHistory[head] = filter.updateAndGetFilteredSample(sample);    if (++head == historyLength) head = 0;        float currentMin = RANGE_INIT;    float currentMax = -currentMin;        int i;    for (i=0;i < historyLength; i++) {      float sample = sampledHistory;      if (sample < currentMin) currentMin = sample;      if (sample > currentMax) currentMax = sample;      sample = filteredHistory;    } // for        float filteredMinT = filterMin.updateAndGetFilteredSample(currentMin);    float filteredMaxT = filterMax.updateAndGetFilteredSample(currentMax);    filteredMin = rLerp(filteredMinMaxRate,filteredMin,filteredMinT);    filteredMax = rLerp(filteredMinMaxRate,filteredMax,filteredMaxT);      } // update    virtual void line2D(float x1,float y1,float x2,float y2,ColorARGB color)=0;  virtual void rect2D(float x,float y,float width,float height,ColorARGB color)=0;  virtual void text2D(const char * s,float x,float y,ColorARGB color)=0;  virtual float textPixelWidth(const char * s)=0;  virtual float textPixelHeight(const char * s)=0;    virtual void render(float cx,float cy,    float width,float height,    float borderWidth,    ColorARGB backgroundColor,    ColorARGB historyColor,    ColorARGB filteredHistoryColor,    ColorARGB minColor,   // Min value line    ColorARGB maxColor,   // Max value line    float minRange,float maxRange, // When both are -1, use dynamic range.    float scale,          // Scale the rendered values (scale = frameRate*8/1000, scale*bytesPerFrame = KBits/sec).    const char * units,   // f/s, KBit/s, etc.    const char * title) {        rect2D(cx,cy,width,height,backgroundColor);        width  -= borderWidth*2.f;    height -= borderWidth*2.f;    float maxOffsetY = height-1.f;        float startX = cx + borderWidth;    float startY = cy + borderWidth + maxOffsetY; // Drawing from bottom up        float scaleX = (width-1.f)/float(historyLength-1);    float scaleY;        float sampleOffsetY,rangeY;        if (minRange == -1.f && maxRange == -1.f) {      rangeY = filteredMax - filteredMin;      sampleOffsetY = filteredMin;    } else {      rangeY = maxRange - minRange;      sampleOffsetY = minRange;    } // if        if (rangeY > 0.f) scaleY = maxOffsetY/rangeY;    else              scaleY = 0.f;        float lastX;    float lastY;        // Draw sampledHistory    int pos = head; // Start at oldest sample, move toward newest sample    int i;    for (i=0; i < historyLength; i++) {      float x = startX + float(i)*scaleX;      float biasedY = sampledHistory[pos]-sampleOffsetY; // Bias so min is near zero      float y = startY - clamp(biasedY*scaleY,0.f,maxOffsetY); // Drawing up      if (i > 0) {        line2D(lastX,lastY,x,y,historyColor);      } // if      lastX = x;      lastY = y;      if (++pos == historyLength) pos = 0;    } // for        // Draw filteredHistory over sampledHistory    pos = head; // Start at oldest sample, move toward newest sample    for (i=0; i < historyLength; i++) {      float x = startX + float(i)*scaleX;      float biasedY = filteredHistory[pos]-sampleOffsetY; // Bias so min is near zero      float y = startY - clamp(biasedY*scaleY,0.f,maxOffsetY); // Drawing up      if (i > 0) {        line2D(lastX,lastY,x,y,filteredHistoryColor);      } // if      lastX = x;      lastY = y;      if (++pos == historyLength) pos = 0;    } // for        rangeY = absoluteMax - absoluteMin;    if (rangeY > 0.f) scaleY = maxOffsetY/rangeY;    else              scaleY = 0.f;        // Render title    float stW = startX + width - 1.f;    float stYMin = startY-height;    float stX = startX;    text2D(title,stX,stYMin,ColorARGB(127,127,240,240));        // Render average (last filtered history value)    int newestIndex = head > 0 ? head-1 : historyLength-1;    char buff[64];    sprintf(buff,"AVE %3.3f %s",filteredHistory[newestIndex]*scale,units);    text2D(buff,stX,startY-borderWidth,ColorARGB(127,200,200,100));        // Render Absolute Max    sprintf(buff,"AMAX %.3f",absoluteMax*scale);    stX = stW - textPixelWidth(buff);    text2D(buff,stX,stYMin,ColorARGB(127,192,192,192));    stYMin += textPixelHeight(buff)+1.f;        // Render Absolute Min    sprintf(buff,"AMIN %.3f",absoluteMin*scale);    stX = startX + width - textPixelWidth(buff) - 1.f;    float stYMax = startY-borderWidth;    text2D(buff,stX,stYMax,ColorARGB(127,192,192,192));    stYMax -= textPixelHeight(buff)+1.f;        // Render filteredMax line    float lineY = startY-filteredMax*scaleY; // Drawing up    lineY = clamp(lineY,stYMin,stYMax);    line2D(startX,lineY,lastX,lineY,maxColor);    sprintf(buff,"FMAX %3.3f",filteredMax*scale);    float fmaxWidth = textPixelWidth(buff);    stX = stW - fmaxWidth;    text2D(buff,stX,lineY,ColorARGB(127,200,100,100));    float minLineY = lineY + textPixelHeight(buff) - 1.f;        // Render filteredMin line    lineY = startY-filteredMin*scaleY; // Drawing up    lineY = clamp(lineY,stYMin,stYMax);    line2D(startX,lineY,lastX,lineY,minColor);    sprintf(buff,"FMIN %3.3f",filteredMin*scale);    stX = stW - textPixelWidth(buff);    if (lineY < minLineY) stX -= fmaxWidth+textPixelWidth(" ");        text2D(buff,stX,lineY,ColorARGB(127,100,200,100));  } // render  };


Quote:Original post by nPawn
Still trying to wrap my head around the cubic spline tutorial, the description of how to implement the formulas didn't make a lot of sense.


Skip the spline method for now. To start, just interpolate between the two positions/orientations.

This method adds some positional delay (lag), but it's easy to implement:

alpha = 1.f/15.f // arbitrary. Set to suit desired behavior.

localPosition = lerp(alpha,localPosition,ghostPosition) // vectors
localRotation = slerp(alpha,localRotation,ghostRotation) // quats
(You can do the same for the velocities).

The above is an IIR filter (Infinite Impulse Response filter), and is also a low-pass filter (high frequency motion will be smoothed: this also results in some delay).
I assume Lerp is just finding the midpoint between the two points? then we multiply that by the alpha?
Quote:Original post by nPawn
I assume Lerp is just finding the midpoint between the two points? then we multiply that by the alpha?


lerp = Linear Interpolation. Example for a vector:

inline Vec3 lerp(const flt a,const Vec3 & lo,const Vec3 & hi) {  return lo + (hi - lo)*a;} // Vec3 lerp


http://catb.org/~esr/jargon/html/L/LERP.html
http://ggt.sourceforge.net/html/group__Interp.html

If your game is 2D, you won't need slerp (for 3D rotations). For 2D, just lerp the rotation angles.

See www.google.com and search for lerp, slerp, etc., for more info.
using the Lerp is the smoothest i've seen yet. It's really smooth on one computer, but another computer still gets spurts of extra speed (like it's zooming to catch up), but the jerkiness/jumping for it is all but gone. Gonna play with things until I can hopefully get the "catch up" fixed.

Would you mind explaining what the Lerp is doing? Linear Interpolation doesn't mean anything to me, i've heard the term but dunno what it means. Sorry for all the questions but i'm really new with this topic so i'm not sure what is being accomplished other than a smoothing of sorts. Thanks for all your help.
The problem with using that alpha is that, if you want to move from point 0 to point 1 with an alpha of 10 (say), you will move:

In the first timestep, from 0.0 to 0.1
In the second timestep, from 0.1 to 0.19
In the third timestep, from 0.19 to 0.271
...

I e, you'll start out going fast, and "ease off" towards the goal.

There are better interpolators/extrapolators pointed at from the Forum FAQ.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement