Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Will "Rubber banding" resolve multiplayer interpolation stutter?

I wrote multiplayer Pong using UDP. I am using interpolation and extrapolation in order to create a smooth looking effect on the client.

It works. However, there is a bit of constant stuttering in the ball. It jumps a tiny bit forward every time a new packet is received. It looks a little laggy, but it is playable.

There must be a way to make the game look smoother. I've read about Rubber Banding. What would be the best way to move from here?

I hope someone who is able to answer my question well will find it.

Update

As requested by Ivan, here is a graph of the ping times. However, I do believe that the problem exists inside the client smoothing code.

enter image description here

like image 238
Z0q Avatar asked Oct 11 '16 12:10

Z0q


2 Answers

Filling in with context from your previous question, I understand that you are sending paddle & ball positions from each client to the other. However, as long as the clients agree on where the paddles are at each moment in time, the ball's movement is completely determined (barring rounding errors), and you should experiment zero ball-stutter. Each client should keep its own internal state with the positions and speeds of paddles and the ball. Pseudocode would be similar to the following:

// input thread
if input changed,
   alter paddle speed and/or direction
   send timestamped message to inform my opponent of paddle change

// incoming network thread
if paddle packet received
   alter opponent's paddle speed and/or direction at time it was sent
   fix any errors in previously extrapolated paddle position <--- Easy
if ball-packet received
   fix any errors in ball position and speed <--- Tricky

// update entities thread
for each entity (my paddle, opponent paddle, the ball)
   compute updated entity position, adjusted by time-since-last-update
   if ball reached my end, send ball-packet to other side
   draw updated entity

This assumes that two package types are being exchanged:

  • paddle packets are timestamped positions + speeds of paddles, and are sent whenever a client alters the speed of its own paddle.
  • ball packets are timestamped positions + speeds of the ball, and are sent whenever a ball reaches a client's (local) side, whether it bounces off the paddle or not.

The pseudocode is performing extrapolation ("assume things keep on moving as usual") for all unknowns in the update-entities thread. The only point where problems arise is marked with <--- arrows.

You can easily correct for paddle positions by warping them to their new position, possibly interpolating the movement over a short period to make it less jarring.

Correcting for ball positions is easy if both clients more-or-less agree (and then you can do the interpolation trick again, to smoothen it up further). However, one client may see a near miss and another a near hit. In this case, since you are using a peer-to-peer model, we are letting the local client make the call, and explain what happened to the opponent (in another design, you would have a central server making these kinds of decisions; this is good to avoid cheating). You cannot avoid an ugly jump there if both clients disagree - but hopefully, this should be relatively rare and short, unless it coincides with a ping spike.

like image 130
tucuxi Avatar answered Sep 22 '22 13:09

tucuxi


One of the ideas which allows to get rid of this effect is Using smoothing when applying error prediction corrections on client.

How that works

At some point in your code you identify that ball position and client are different.

Discrepancy

Instead of applying that as a correction to client code immediately (which is one reason you can see those jumps), you perform that over some time, cl_smoothtime e.g. 500ms.

At first your program should store time when the error detection event occured m_flPredictionErrorTime.

public void onErrorDetected() {
    this.m_flPredictionErrorTime = System.currentTimeMillis();
}

Somewhere close to the display code you calculate how much of error you're going to display. Here's some pseudocode for that.

public void draw() {
    Point preditctionError = this.clientPredictedBallCoordinates - this.serverCoordinates;
    Point deltaToDisplay = calculateErrorVector(preditctionError);
    Point positionToDisplay = clientPredictedBallCoordinates + deltaToDisplay;
    // actually draw the ball here
}

public Point calculateErrorVector(Point coordinatesDelta) {
     double errorAmount = ( System.currentTimeMillis() - this.m_flPredictionErrorTime ) / this.cl_smoothtime.
     if (errorAmount > 1.0) {
         // whole difference applied in full, so returning zero delta
         return new Point(0,0);
     }
     if (errorAmount < 0) {
         // no errors detected yet so return zero delta
         return new Point(0,0);
     }
     Point delta = new Point(coordinates.x*errorAmount, coordinates.y*errorAmount);
     return delta;
}

I've picked this idea from Source Multiplayer Networking wiki. Actual code example in Cpp is available in their SDK around GetPredictionErrorSmoothingVector function.

like image 34
Ivan Avatar answered Sep 24 '22 13:09

Ivan