Interpolation

From Valve Developer Community
Jump to navigation Jump to search

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 and sv_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:

  1. Initialise our CInterpolatedVar in the class constructor, giving it a debug name in the process.
    Todo: How to debug?
  2. Call AddVar() to latch the CInterpolatedVar onto the actual variable.
  3. Ensure that ShouldInterpolate() returns true when we want it to. (This entity isn't visible so wouldn't normally be interpolated.)
  4. On the server, call SetSimulationTime(). The simulation latch we chose in AddVar() 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.

See also