객체 리플리케이션

게임 서버 프로그래밍/네트워크 개념정리

2020. 3. 2. 15:54

정의

객체의 상태를 한 호스트에서 다른 호스트로 복제 전달하는 행위

 

패킷 구성

Packet Type (4byte)

패킷 타입을 enum으로 표시한다.

enum pakcetType
{
	PT_HELLO,
	PT_ReplicationData,
	PT_Disconnect,
	PT_MAX
};

PT_HELLO는 관례로 호스트가 서로 접속하면 맨 처음에 일종의 '인사' 패킷을 주고받는 것

PT_ReplicationData는 데이터를 리플리케이션 하겠다는 것

PT_Disconnect는 연결 해제 절차를 요청하는 것

 

Network ID (4byte - uint32_t)

직렬화를 하기 위해서 링킹 과정이 필요한 프로퍼티들이 있었다. 다시 한번 더 말하자면, 여러 번 참조될 법한 객체에 고유 식별자 혹은 ID를 부여해 두었다가 이들 객체를 직렬 화할 때 오로지 식별자 값만 직렬 화하는 것이다

Network ID 자체는 4byte의 단조 증가 정수이며, 네트워크상으로 전송될 패킷에 넘버를 매기는 역할을 한다.

std::unordered_map<uint32_t, GameObject*> mNetworkIdToGameObjectMap;
std::unordered_map<const GameObject*, uint32_t> mGameObjectToNetworkIdMap;

Network ID와 패킷에 실어나를 GameObject* (네트워크 상으로 전송될 객체들은 GameObject를 상속 받음)

를 매핑해두었다가, ID를 통해서 호스트끼리 매핑된 테이블의 GameObject를 찾아 데이터를 풀어 넣어주는 방식을

이용한다.

 

Class ID (4byte)

Network ID를 통해 객체를 전달받은 후 해당 객체가 어떤 클래스인지 확인할 필요가 있다.

따라서, Class ID를 이용해 해당 클래스를 찾을 것인데 '객체 생성 레지스트리'라는 것을 이용한다.

더보기

객체 생성 레지스트리는 일반적으로 정의된 용어가 아닌 '멀티플레이어 게임 프로그래밍' 책에서 임의로 정의한 것

클래스 식별자를 해당 클래스의 객체 생성용 함수에 매핑해둔 것을 의미한다.

이 레지스트리를 사용해 생성 함수를 id로 찾고, 찾은 함수를 호출해 원하는 객체를 생성할 수 있다.

 

이렇게 번거로운 작업을 하는 이유는 하드 코딩하여 Class ID를 Switch문을 통해 지정하면 코드가 더러워질뿐더러

게임 플레이 코드와 네트워크 코드가 서로 종속되기 때문에 미연에 방지를 하는 것이다.

using GameObjectCreationFunc = GameObject* (*)();
//....

std::unordered_map<uint32_t, GameObjectCreationFunc> mNameToGameObjectCreationFunctionMap;

GameObject를 상속받은 객체들은 기본적으로 자기 자신의 객체를 반환하는 GetClassId()를 가지며,

이것을 uint32_t 타입의 Class ID와 함께 함수 포인터 형식으로 매핑해 둔다. 이제 송신 호스트가 GameObject의 클래스 식별자를 기록할 때 객체의 GetClassId()만 호출하면 식별자를 얻을 수 있다.

 

월드 상태의 변경

만약 게임의 규모가 굉장히 작고 송수신할 데이터 자체가 적다면 전체 월드 상태를 단일 패킷으로 보내도 될 것이다.

하지만, 규모가 큰 게임의 경우 한 번에 송수신 할 데이터가 많기 때문에 월드 상태를 단일 패킷으로 보내는 것은 불가능하다. 하지만, 굳이 월드 상태 전체를 패킷으로 보내지 않고서도 변경점을 기록한 패킷을 보내고, 수신 측이 변경 내역을 토대로 월드 상태를 갱신해 주면 된다.

 

객체 상태를 3가지로 나타내면 생성 , 갱신 , 소멸이 있다.

enum ReplicationAction
{
	RA_Create,
	RA_Update,
	RA_Destroy,
	RA_MAX
};

앞의 패킷 헤더에서 ReplicationAction이 추가된 것뿐이다. 송신 측 수신 측 코드를 ReplicationAction에 따라서

어떻게 처리할지 지정한다. 주의할 점은 네트워크상에는 여러 가지 비신뢰성을 일으키는 요소들이 있으므로,

수신자가 목표 게임 객체를 찾지 못하는 경우가 있는데, 이 경우에도 일단 해당 부분을 읽어줘야 다음 내용을

처리할 수 있으므로, 일단 메모리 스트림을 읽어 들여 적절한 분량만큼 스트림을 전진시켜야 한다.

 

객체의 상태 부분을 리플리케이션 할 때는 변경된 부분만 리플리케이션 한다.

enum Status_Property 
{
	Name = 1<<0,
	LegCount = 1<<1,
	HeadCount = 1<<2,
	Health = 1<<3,
	Max
};

void Write(OutputMemoryBitStream& inStream, uint32_t inProperties)
{
	inStream.Write(inProperties, GetRequiredBits<MAX>::Value);

	if ((inProperties & Name) != 0)
		inStream.Write(mName);
	if ((inProperties & LegCount) != 0)
		inStream.Write(mLegCount);
	if ((inProperties & HeadCount) != 0)
		inStream.Write(mHeadCount);
	if ((inProperties & Health) != 0)
		inStream.Write(mHealth);
}

enum을 bit 형식의 flag로 만들어준 후 조합을 통해 어떤 특성이 변경되었는지 쉽게 표현할 수 있다.

각 요소와 & 연산을 통해서 변경점을 쉽게 구해낼 수 있다.

 

RPC (Remote Procedure Call) - 원격 프로시저 호출

멀티플레이어 게임에선, 상태뿐만 아니라 어떤 기능을 수행하는 함수 자체를 수행시켜야 할 때가 있다.

RPC는 한 호스트가 하나 이상의 다른 호스트에서 특정 프로시저가 원격 수행되도록 지시하는 행위이다.

 

RPC도 앞의 Enum의 ReplicationAction에 식별자를 만들어 두고 객체 레지스트리와 마찬가지로 해시 맵을 두어

시스템 사이의 종속성을 제거하여 RPC에 사용될 함수 생성 부분에서 해시 맵에 자신을 등록하는 방법을 이용한다.

 

void UnWrapPlaySound(InputMemoryBitStream& inStream)
{
	string soundName;
	Vector3 location;
	float volume;

	inStream.Read(soundName);
	inStream.Read(location);
	inStream.Read(volume);
	PlaySound(soundName, location, volume);
}

void RegisterRPCs(RPCManager* inRPCManager)
{
	inRPCManager->RegisterUnwrapFunction('PSND', UnWrapPlaySound);
}

RPC 호출부와 등록부를 분리함으로써 코드도 깔끔해졌다. 등록은 함수를 가지고 있는 클래스 자체에서 하고,

해당 함수를 사용하는 클래스는 RPCManager의 해시 맵에 key를 넣어줌으로써 쉽게 사용할 수 있게 됐다.

 

 

'게임 서버 프로그래밍 > 네트워크 개념정리' 카테고리의 다른 글

IOCP 공부노트: (2) IOCompletionPort  (0) 2020.07.13
IOCP 공부 노트: (1) _beginthreadex , CreateThread  (0) 2020.07.10
압축  (0) 2020.02.24
직렬화  (0) 2020.02.24
NAT과 P2P  (0) 2020.02.19