Interpolation

From Valve Developer Community
< Zh
Jump to navigation Jump to search
Info content.png
This page is Machine translated

It is not recommended to use machine translation without any corrections.

If the article is not corrected in the long term, it will be removed.
English (en)中文 (zh)Translate (Translate)

在多人游戏中,客户端通常需要在每次收到服务器更新时渲染三帧或更多帧(假设帧率为60fps+且cl_updaterate(en)设为20)。

Source引擎的插值系统通过缓冲服务器更新并在间隙间进行平滑插值回放,避免了可能出现的画面抖动问题。该系统还能防止因丢包导致的显示异常。

服务器知晓每个客户端的插值量,并据此调整延迟补偿(en)

影响与管理

插值会为玩家视角增加额外延迟,因此应将其控制在最低限度。遗憾的是,V社游戏仍默认使用100ms的最小插值延迟("lerp"),这个数值是为拨号上网时代设计的!

  • 玩家应设置cl_interp 0,这将确保lerp长度精确匹配当前服务器更新速率(en)。提升更新速率可进一步减少lerp。
  • Mod开发者应考虑移除或重命名cl_interp以避免混淆。
  • 服务器运营者可通过sv_client_min_interp_ratiosv_client_max_interp_ratio调控lerp。

遭遇丢包的玩家可将cl_interp_ratio设为3(防御单次丢包)甚至4(防御连续两次丢包)。

实现

这个简单实体每帧输出一个插值后的浮点数。

服务端:

#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;

	// 没有这行,LATCH_SIMULATION_VAR永远不会触发
	SetSimulationTime( gpGlobals->curtime ); 

	SetNextThink(gpGlobals->curtime + 0.0001);
	BaseClass::Think();
}

客户端:

#include "cbase.h"

class C_InterpDemo : public C_BaseEntity
{
public:
	DECLARE_CLASS(C_InterpDemo, C_BaseEntity);
	DECLARE_CLIENTCLASS();
 
	C_InterpDemo();

	bool ShouldInterpolate() { return true; } // 通常只有PVS中的实体会被插值
 
	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") // 仅调试用名称,可自定义唯一标识
{
	// 这是模拟锁存器,仅当实体移动或更新SimulationTime时进行插值
	AddVar( &m_MyFloat, &m_iv_MyFloat, LATCH_SIMULATION_VAR );
	UpdateMsg = "";
}
 
void C_InterpDemo::PostDataUpdate(DataUpdateType_t updateType)
{
	UpdateMsg = "(来自服务端)";
	SetNextClientThink(CLIENT_THINK_ALWAYS);
	BaseClass::PostDataUpdate(updateType);
}
 
void C_InterpDemo::ClientThink()
{
	Msg("插值浮点数:%f%s\n",m_MyFloat,UpdateMsg);
	UpdateMsg = "";
	SetNextClientThink(CLIENT_THINK_ALWAYS);
}

关键步骤如下:

  1. 在类构造函数中初始化CInterpolatedVar并赋予调试名称。
    待完善: 如何调试?
  2. 调用AddVar()CInterpolatedVar与目标变量绑定
  3. 确保需要插值时ShouldInterpolate()返回true(该实体不可见,通常不会被插值)
  4. 在服务端调用SetSimulationTime()。在AddVar()中选择的模拟锁存器会因该值或位置/角度的变化而触发。也可选择动画锁存器,其由实体当前动画帧("cycle(en)")变化触发

故障排查

若变量未插值:

  • 检查C_BaseEntity::PostDataUpdate(),这是插值系统的触发点
  • 若插值需传递至其他代码(如VPhysics定位),确保在ClientThink()(en)中每帧执行
  • 确认实体在客户端的PVS(en)范围内

参见