소스엔진 멀티플레이어 네트워크 : 보간


https://developer.valvesoftware.com/wiki/Interpolation


멀티플레이어 게임의 클라이언트는 서버의 각 업데이트를 받을 때 3 프레임 이상을 렌더해야 하는 것이 보통이다. (60fps에 cl_updaterate 20으로 가정)

소스엔진의 보간 시스템은 서버 업데이트 사이의 데이터 간격이나 패킷 손실로 인해 생길 수 있는 홱 움직이는 움직임을 부드럽게 보간하여 방지한다.

서버는 각 플레이어가 얼마나 보간되는지 파악하여 렉 보정을 적절히 보정한다.

영향과 관리

보간은 플레이어 관점에서 항상 유지되어야 하는 임의의 지연시간을 추가한다. 안타깝게도 Valve의 게임들은 여전히 이 최소 보간 지연 시간(“lerp”)을 전화모뎀 사용하던 때에 정해뒀던 100ms로 잡아두고 있다!

  • 서버의 업데이트 비율에 맞춰서 보간 지연이 할당되게 하려면 플레이어가 cl_interp 0를 설정해야 한다. 서버의 업데이트 비율이 높아지면 이 지연시간이 줄어든다.
  • 모더들은 혼란을 방지하려면 cl_interp를 삭제하거나 이름을 바꾸는 것을 고려할 것.
  • 서버에서 보간 지연시간을 조절할 수 있다. (sv_client_min_interp_ratio, sv_client_max_interp_ratio)

패킷 손실을 겪는 유저는 cl_interp_ratio를 3이나 (패킷 하나 손실 보호) 4 정도(연속된 패킷 2개 손실 보호)로 높일 수 있다.

구현

아래의 단순한 엔티티에서 매 프레임마다 보간된 소수를 출력한다.

서버:

#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; } // 보통 시야에 보이는 엔티티만 해당됨
 
	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") // 그저 디버깅용 이름이다.
{
	// 이건 시뮬레이션에서 걸쇠 역할로, 여기서 엔티티가 움직이거나 새로운 시뮬레이션 시간을 가졌을 때, 이 변수만 보간되도록 한다.
	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);
}

중요한 단계는:

  1. 클래스 생성자에 CInterpolatedVar를 초기화하고 디버깅용 이름을 부여한다. To do: 디버그를 어떻게?
  2. AddVar()를 호출하고 실제 변수를 CInterpolatedVar에 건다.
  3. ShouldInterpolate()에서 우리가 원하는 때에 true를 호출해 보간될 수 있도록 한다. (이 엔티티의 경우는 보이지 않아서 일반적인 경우에는 보간되지 않는다.)
  4. 서버에서 SetSimulationTime()를 호출하여 시뮬레이션의 걸쇠로 선택한 AddVar()가 이 값이 변하거나 위치/각도가 변할 때 발동되도록 한다. 또는 엔티티에서 현재 에니메이션의 프레임(“cycle“) 이 변할 때 발동되는 애니메이션 걸쇠를 선택할 수 있다.

문제 해결

변수가 보간되지 않았을 경우:

  • 보간 시스템이 시작되는 C_BaseEntity::PostDataUpdate()를 확인
  • 보간되는 값이 다른 코드에도 통과되어야 한다면 (즉, VPhysic 포지셔닝) ClientThink()에서 매 프레임마다 그렇게 작동되고 있는지 확인할 것
  • 해당 엔티티가 클라이언트의 시야 내에 있는지 확인할 것
, ,

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다