KoreanFoodie's Study

[C++ 게임 서버] 5-6. 패킷 직렬화 #3 본문

Game Dev/Game Server

[C++ 게임 서버] 5-6. 패킷 직렬화 #3

GoldGiver 2023. 12. 16. 12:21

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 5-6. 패킷 직렬화 #3

핵심 :

1. 가변 데이터 안에 가변 데이터를 넣는 경우도, 결국 Offset 과 Count 를 이용해 버퍼에 데이터를 기록하는 방식으로 처리한다.

2. 추후에 소개할 ProtoBuf 를 이용하면, 패킷을 만들 때 가변 데이터를 일일히 코드로 넣는 귀찮음을 줄일 수 있다(다만 불필요한 복사가 있을 수는 있음). 

저번에는 가변 데이터를 패킷에 넣어 보았는데... 만약 가변 데이터 안에 또 가변 데이터가 있다면 어떻게 해야 할까?

예를 들어, BuffListItem 이라는 구조체에 해당 버프의 대상들 정보(Victims 라고 하자)를 담는다면 어떻게 해야 할까?

struct BuffsListItem
{
    uint64 buffId;
    float remainTime;

    // Victim List
    uint16 victimsOffset;
    uint16 victimsCount;
};

일단 위의 데이터가 들어가야 하는 패킷은 아래처럼 정의할 수 있을 것이다.

// [ PKT_S_TEST ][BuffsListItem BuffsListItem BuffsListItem][victim victim][victim victim]
struct PKT_S_TEST
{
	uint16 packetSize; // 공용 헤더
	uint16 packetId; // 공용 헤더
	uint64 id; // 8
	uint32 hp; // 4
	uint16 attack; // 2
	uint16 buffsOffset;
	uint16 buffsCount;
};

패킷의 정의는 거의 똑같은데, 잘 보면 기존에 BuffsListItem 으로 할당된 공간 외에, victim 에 대한 정보가 버퍼에 기록되어야 함을 알 수 있다.

사실 이전에 우리가 만들어 놓은 PacketList 를 활용하면, 쉽게 확장을 할 수 있다. 즉, ServerPacketHandler 에서는 아래와 같이 버퍼를 사용하게 될 것이다 :

// [ PKT_S_TEST ][BuffsListItem BuffsListItem BuffsListItem][victim victim]
class PKT_S_TEST_WRITE
{
public:
	using BuffsListItem = PKT_S_TEST::BuffsListItem;
	using BuffsList = PacketList<PKT_S_TEST::BuffsListItem>;
	using BuffsVictimsList = PacketList<uint64>;

	PKT_S_TEST_WRITE(uint64 id, uint32 hp, uint16 attack)
	{
		_sendBuffer = GSendBufferManager->Open(4096);
		_bw = BufferWriter(_sendBuffer->Buffer(), _sendBuffer->AllocSize());

		_pkt = _bw.Reserve<PKT_S_TEST>();
		_pkt->packetSize = 0; // To Fill
		_pkt->packetId = S_TEST;
		_pkt->id = id;
		_pkt->hp = hp;
		_pkt->attack = attack;
		_pkt->buffsOffset = 0; // To Fill
		_pkt->buffsCount = 0; // To Fill
	}

	BuffsList ReserveBuffsList(uint16 buffCount)
	{
		BuffsListItem* firstBuffsListItem = _bw.Reserve<BuffsListItem>(buffCount);
		_pkt->buffsOffset = (uint64)firstBuffsListItem - (uint64)_pkt;
		_pkt->buffsCount = buffCount;
		return BuffsList(firstBuffsListItem, buffCount);
	}

	BuffsVictimsList ReserveBuffsVictimsList(BuffsListItem* buffsItem, uint16 victimsCount)
	{
		uint64* firstVictimsListItem = _bw.Reserve<uint64>(victimsCount);
		buffsItem->victimsOffset = (uint64)firstVictimsListItem - (uint64)_pkt;
		buffsItem->victimsCount = victimsCount;
		return BuffsVictimsList(firstVictimsListItem, victimsCount);
	}

	SendBufferRef CloseAndReturn()
	{
		// 패킷 사이즈 계산
		_pkt->packetSize = _bw.WriteSize();

		_sendBuffer->Close(_bw.WriteSize());
		return _sendBuffer;
	}

private:
	PKT_S_TEST* _pkt = nullptr;
	SendBufferRef _sendBuffer;
	BufferWriter _bw;
};

코드가 조금 길어졌는데, 사실 이전과 달라진 부분은 크게 없다. 😅

ReserveBuffsVictimsList 를 보면, victimsCount 만큼의 공간을 확보한 다음, Offset 과 Count 를 넣어주고 있다. 이는 기존에 BuffList 에 대한 정보를 버퍼에 기록하는 것과 비슷한 작업이다!

그리고 이제는 리스트 안에 리스트가 있는 형태라, packetSize 가 한번에 계산이 안되었었다. 😂 이를 위해, 마지막에 CloseAndReturn 함수를 통해 패킷의 총 사이즈를 기록할 수 있도록 만들어줬다.

 

그렇다면 실제로 GameServer 에서는 데이터를 어떻게 넣어주어야 할까? 사실 이 부분이 조금 귀찮을 수 있는데, 왜냐하면 아래와 같이 일일히 데이터를 넣어 주어야 하기 때문이다.

// [ PKT_S_TEST ]
PKT_S_TEST_WRITE pktWriter(1001, 100, 10);

// [ PKT_S_TEST ][BuffsListItem BuffsListItem BuffsListItem]
PKT_S_TEST_WRITE::BuffsList buffList = pktWriter.ReserveBuffsList(3);
buffList[0] = { 100, 1.5f };
buffList[1] = { 200, 2.3f };
buffList[2] = { 300, 0.7f };

PKT_S_TEST_WRITE::BuffsVictimsList vic0 = pktWriter.ReserveBuffsVictimsList(&buffList[0], 3);
{
    vic0[0] = 1000;
    vic0[1] = 2000;
    vic0[2] = 3000;
}

PKT_S_TEST_WRITE::BuffsVictimsList vic1 = pktWriter.ReserveBuffsVictimsList(&buffList[1], 1);
{
    vic1[0] = 1000;
}

PKT_S_TEST_WRITE::BuffsVictimsList vic2 = pktWriter.ReserveBuffsVictimsList(&buffList[2], 2);
{
    vic2[0] = 3000;
    vic2[1] = 5000;
}

SendBufferRef sendBuffer = pktWriter.CloseAndReturn();

하지만 데이터 구조가 복잡해지고, 양이 많아지면 위와 같은 방식으로 작업하는 것은 애로 사항이 있을 것이다. 물론 이를 해결하기 위해, ProtoBuf 를 사용하기도 하는데... 그건 곧 다루게 될 것이다. 😉

 

그럼 ClientPacketHandler 에서는, 다음처럼 기존과 크게 다르지 않게 데이터를 읽을 수 있게 된다!

PKT_S_TEST::BuffsList buffs = pkt->GetBuffsList();

for (auto& buff : buffs)
{
    PKT_S_TEST::BuffsVictimsList victims =  pkt->GetBuffsVictimList(&buff);

    for (auto& victim : victims)
        cout << "Victim : " << victim << endl;
}
Comments