Interpolation
A multiplayer client will typically need to render three or more frames with each server update it receives (assuming 60fps+ and cl_updaterate 20).
Source's interpolation system prevents the jittery motion this would ordinarily lead to by buffering server updates then playing them back with the gaps smoothly interpolated between. It can also protect against glitches caused by packet loss.
The server knows how much interpolation each client has, and adjusts lag compensation accordingly.
Impact and management
Interpolation adds artificial latency to a player's view of the game world and as such should be kept to the barest minimum. Unfortunately, Valve's games still default to a minimum interpolation delay ("lerp") of 100ms, a value tuned for the era of dial-up modems!
- Players should set
cl_interp 0
, as this will ensure that the lerp is the precise length needed to accommodate the current server update rate. Raising the update rate will reduce lerp further. - Modders should consider removing or renaming
cl_interp
to prevent confusion. - Server operators can regulate lerp with
sv_client_min_interp_ratio
andsv_client_max_interp_ratio
.
Players who suffer from packet loss may want to raise cl_interp_ratio
to 3 (protects against one dropped packet) or even 4 (protects against two consecutive dropped packets).
Implementation
This simple entity prints an interpolated float every frame.
Server:
#include "cbase.h"
class CInterpDemo : public CBaseEntity
{
public:
DECLARE_CLASS(CInterpDemo, CBaseEntity);
DECLARE_SERVERCLASS();
CInterpDemo() { m_MyFloat = 0; }
void Spawn() { SetNextThink(gpGlobals->curtime + 0.0001); BaseClass::Spawn(); }
int UpdateTransmitState() { return SetTransmitState( FL_EDICT_ALWAYS ); }
void Think();
CNetworkVar(float,m_MyFloat);
};
IMPLEMENT_SERVERCLASS_ST(CInterpDemo, DTInterpDemo)
SendPropFloat( SENDINFO(m_MyFloat) ),
END_SEND_TABLE()
LINK_ENTITY_TO_CLASS( interp_demo, CInterpDemo );
void CInterpDemo::Think()
{
m_MyFloat += 0.1;
// without this, LATCH_SIMULATION_VAR will never be triggered
SetSimulationTime( gpGlobals->curtime );
SetNextThink(gpGlobals->curtime + 0.0001);
BaseClass::Think();
}
Client:
#include "cbase.h"
class C_InterpDemo : public C_BaseEntity
{
public:
DECLARE_CLASS(C_InterpDemo, C_BaseEntity);
DECLARE_CLIENTCLASS();
C_InterpDemo();
bool ShouldInterpolate() { return true; } // ordinarily only entities in PVS are interpolated
void PostDataUpdate(DataUpdateType_t updateType);
void ClientThink();
float m_MyFloat;
char* UpdateMsg;
CInterpolatedVar<float> m_iv_MyFloat;
};
IMPLEMENT_CLIENTCLASS_DT(C_InterpDemo,DTInterpDemo,CInterpDemo)
RecvPropFloat( RECVINFO(m_MyFloat) ),
END_RECV_TABLE()
LINK_ENTITY_TO_CLASS( interp_demo, C_InterpDemo );
C_InterpDemo::C_InterpDemo() :
m_iv_MyFloat("C_InterpDemo::m_iv_MyFloat") // just a debug name, can be anything unique
{
// This is a simulation latch, so the variable will only be interpolated
// if the entity is moving or has a new SimulationTime.
AddVar( &m_MyFloat, &m_iv_MyFloat, LATCH_SIMULATION_VAR );
UpdateMsg = "";
}
void C_InterpDemo::PostDataUpdate(DataUpdateType_t updateType)
{
UpdateMsg = " (from server)";
SetNextClientThink(CLIENT_THINK_ALWAYS);
BaseClass::PostDataUpdate(updateType);
}
void C_InterpDemo::ClientThink()
{
Msg("Interpolated float: %f%s\n",m_MyFloat,UpdateMsg);
UpdateMsg = "";
SetNextClientThink(CLIENT_THINK_ALWAYS);
}
The important steps are:
- Initialise our
CInterpolatedVar
in the class constructor, giving it a debug name in the process.Todo: How to debug? - Call
AddVar()
to latch theCInterpolatedVar
onto the actual variable. - Ensure that
ShouldInterpolate()
returns true when we want it to. (This entity isn't visible so wouldn't normally be interpolated.) - On the server, call
SetSimulationTime()
. The simulation latch we chose inAddVar()
is triggered by changes to this value or to the origin/angles. You could also choose an animation latch which is triggered by changes to the entity's current animation frame ("cycle").
Troubleshooting
If your variable is not being interpolated:
- Check
C_BaseEntity::PostDataUpdate()
, which is where the interp system kicks in. - If the interpolated value needs to be passed to other code (e.g. VPhysics positioning), make sure you are doing so in
ClientThink()
every frame. - Make sure your entity is in PVS on the client.