racer home

Multiplayer - interpolation/prediction

 

Home Nice graphics are good to draw attention, but underneath, the physics do the real fun job.


Dolphinity Organiser - free planning, project management and organizing software for all your action lists

Introduction

This page tells a bit about the interpolation done in Racer for multiplayer. The multiplayer code has 2 parts - linear interpolation and splined interpolation. Linear interpolation is simpler and is quite useful for LAN playing, but requires slightly higher update rates.

Linear interpolation

A good page to read would be the page on Valve's Half Life 2 multiplayer networking. Racer follows a similar method, only where Valve will use the last 2 packets and interpolate between those in Half Life 2, Racer uses the last packet to predict the car locations, rather than to lag too much behind. This is hopefully helpful when 2 cars are racing close to eachother and touching.

To explain Racer's linear interpolation, review the image below:

linear interpolation multiplayer racer

2 packets are remembered by Racer; the latest incoming packet with position & velocity information, and the one before that ('prevPos' above). If you would just take the latest packet and interpolate from there, you'd get a small jump every time a packet arrives.

To deal with that, the 2 latest packets are used. Here is what happens above:

This all works quite nicely in practice. There is some flux due to network timings changing, but it is very low.

Timing in linear interpolation

Note the above method is not that trivial. We use 2 separate timings:

Time syncing

The above method requires each multiplayer client to be clock-synced to the server. In LAN, we achieve around 5 milliseconds accuracy using the following method (it's a bit dependent on framerate really). It's a bit like a simple NTP protocol.

Note that you have to filter the resulting times a bit to avoid sudden jumps in the time offsets. For LAN, this is not too important though.

Source code

As always, some source code goes a long way, especially if the above text is a bit unclear. It's not entirely wonderfully styled; the 'last' packet data is in the variables 'lastPos' and 'vel'. The previous packet's data is in 'prevPos' and 'prevVel'. Upon reception of a new packet, a function is called which stores things like 'prevPacketTime' and such. 'prevPacketTime' is the server time which was in the packet, 'lastPacketTime' is the time in the last received packet (in server time, just like 'prevPacketTime'). lastSimTime is the simulation time at which the last packet was received, and RMGR->time->GetSimTime() gets the current simulation time (in milliseconds).

Here's my prediction function:

void RNetworkInfo::PredictLinear(RCar *car)
// Predict the state (position/orientation) of the car at the current time
// This is really prediction - whereas Half-Life 2 uses interpolation between the last 2 packets,
// we try to keep with the real current position, to improve collisions (and cars
// don't generally move around that fast, derivative-of-acceleration-wise (jerk) )
{
   // Try to predict current positon
   DVector3 *pos=car->GetPosition();
   int       tServer;                // This is in milliseconds (int)
   rfloat    t,
              tSinceLastPacket,       // Time since last packet's event time
              tSinceLastUpdate,       // Time (in real server time) since last update was received
              tSincePrevPacket,       // Time since previous packet's event time
              tLerp;                  // Interpolation from previous packet's position to currently predicted packet position
   RNetworkInfo *ni=car->GetNetworkInfo();

   // Time since last packet info
   tServer=RMGR->network->GetServerTime();
   tSincePrevPacket=(tServer-prevPacketTime)*0.001f;
   tSinceLastPacket=(tServer-lastPacketTime)*0.001f;
   tSinceLastUpdate=(RMGR->time->GetSimTime()-lastSimTime)*0.001f;

   // Calculate time to interpolate from the previous path
   // to ideal (last) packet's path
   tLerp=tSinceLastUpdate/(float(RMGR->timePerNetworkUpdate)*0.001f);
   rfloat tLerpRaw=tLerp;

   // Clamp to 0..1 - it's just about the previous -> last path so no prediction or history
   if(tLerp<0)tLerp=0;
   else if(tLerp>1.0f)tLerp=1.0f;

   // Cache 1-tLerp
   rfloat oneMinusTlerp=1.0f-tLerp;

   //
   // Linear interpolation - should be perfect for static linear velocity tests with 1 server and 1 client
   // More clients will mean the clock varies a bit from clients -> srv -> other clients
   //
   t=Limiter(tSinceLastPacket);

   // Calculate current position based on packet info
   DVector3 posLast,posPrev;
   posLast.x=lastPos.x+vel.x*t;
   posLast.y=lastPos.y+vel.y*t;
   posLast.z=lastPos.z+vel.z*t;

   // Calculate predicted previous position - use time since last interpolation time
   // Calculate interpolated velocity for a bit more smoothness
   DVector3 ipVel;
   ipVel.x=tLerp*vel.x+oneMinusTlerp*prevVel.x;
   ipVel.y=tLerp*vel.y+oneMinusTlerp*prevVel.y;
   ipVel.z=tLerp*vel.z+oneMinusTlerp*prevVel.z;

   t=Limiter(tSincePrevPacket);
   posPrev.x=prevPos.x+ipVel.x*t;
   posPrev.y=prevPos.y+ipVel.y*t;
   posPrev.z=prevPos.z+ipVel.z*t;

   // Calculate position, interpolating from posPrev -> posLast as time goes by
   // to correct towards the last received packet
   curPos.x=tLerp*posLast.x+oneMinusTlerp*posPrev.x;
   curPos.y=tLerp*posLast.y+oneMinusTlerp*posPrev.y;
   curPos.z=tLerp*posLast.z+oneMinusTlerp*posPrev.z;

   *pos=curPos;

   // Use a different method for orientation, which interpolates
   // the rotation from the previous to the new quat.
   // Use unclamped lerp from prev->last since we want prediction
   t=tLerpRaw;       // limit this a bit?
   // Calculate interpolant into 'quat' (current value)
   prevQuat.Slerp(&lastQuat,t,&quat);
}



Dolphinity Organiser - free planning, project management and organizing software for all your action lists

(last updated November 13, 2012 )