하아찡
[C++] 인벤토리 - 2 (인벤토리 서버저장) 본문
언리얼 버전 5.5.3
서버언어 C++로 구성했습니다.
이번글은 조금은 길 예정입니다.
이전글에서 인벤토리 DB설계만 진행하고 클라이언트와 서버측에서 데이터를 로드하는것만 처리했습니다.
이번에는 생각보다 많은양을 처리했습니다
- 아이템 습득
- 아이템 초과 습득 불가
- 소모품 중복습득시 증가
- 소모품 다 사용하면 제거
- 소모품 효과 적용
- 서버측 인벤토리 데이터 메모리 저장 (중요)
- 장비 장착 및 해제
- 장비데이터 메모리 저장 (중요)
- 장비 옵션 캐릭터 적용
이렇게 적어보니 생각보다 많은 양이 아닌거같네요...?
DB서버랑 게임서버랑 클라이언트를 왔다갔다해서그런가... @.@
아마 한페이지에 전부다 쓰긴 양이 많아서 여러 페이지로 나눠서 올릴듯합니다.
실행화면
작업을 하다가 고민한점.
경험치랑 골드를 얻을때마다 DB서버에 올리도록 작업을 해놨는데, 해당 작업이 효율적이지 못하다라는 생각이 들어서 고민을 했는데.. 어처피 서버가 플레이어들의 정보를 다 가지고 있는거라면? 특정 이벤트때 데이터를 저장을 하면 되지않을까?
뭐 결제 시스템이 들어가거나했을때는 데이터를 바로바로 저장을 해야겠지만 그렇지 않은경우는? 데이터가 날라가지만 않게 하고 중간중간 저장하게 하면 되지 않을까? 데이터 처리는 어차피 DB서버에서 불러와서 게임서버에서 다처리를하는데 말이죠.
그래서 이번에 인벤토리를 서버에 데이터를 저장하는 코드를 작성하면서 맵이 이동할때와 게임이 종료될때 그때마다 인벤토리 정보를 서버에 올리는 방식을 사용했습니다.
결과는 정상적으로 나오고 있습니다. 다만 DB서버에 데이터를 보내지 못하고 게임서버가 다운되는 경우가 발생할경우 데이터가 온전히 전달되지 않는 문제가 발생할 수 있습니다.( 일단 생각만해두고 나중에 처리를 해야할듯합니다.)
그래서 위 실행화면을 봤을때 게임을 종료하지 않은 상태라 DB데이터가 변경되지않았고, 게임을 종료후 다시 데이터를 살펴봤더니 데이터가 업데이트가 됐습니다.
PlayerInventory.h
#pragma once
#include <unordered_map>
#include "ItemUtils.h"
struct PlayerItemInfo
{
uint64 id = 0;
uint64 code = 0;
int32 quantity = 0;
int32 enhancement_level = 0;
int32 durability = 0;
int32 slot_index = 0;
bool is_equipped = 0;
bool is_new = false; // True일경우 DB서버에 Insert함
bool is_change = false; // True일경우 DB서버에 아이템을 업로드함.
};
class PlayerInventory
{
public:
PlayerInventory();
~PlayerInventory();
public:
void UpdateInventory(uint64 characterID); // DB서버에 업데이트 요청
void DropItem(ServerItemData item); // 아이템 습득 처리
bool GetLoadInventory(){return loadInventory;} // 인벤토리가 로드가 된적이 있는지 확인
void LoadInventory(Protocol::S_DB_LOADINVENTORY& pkt); // DB서버에서 받아온 데이터 게임서버로 저장
Protocol::S_LOADINVENTORY SendInventoryPkt(); // 맵 이동시 인벤토리 데이터 다시 전송할때 사용
void DeleteItem(int foundIndex); // 소모품 다 사용했을때
bool UseInventoryItem(int64 instanceID, int itemcode); // 장착 및 소모품 사용
private:
void ConvertItemDataToPlayerItemInfo(int32 slotIndex, PlayerItemInfo& playerInfo, ServerItemData item); // 변환작업
void ConvertPlayerItemInfoToItemData(Protocol::ItemData* item, PlayerItemInfo playerInfo); // 변환작업
int FoundItemIndex(int64 instanceID); // 인벤토리에 아이템이있는지
private:
int inventoryCnt = 35; // 5(Col) * 7(Row)
unordered_map<int, PlayerItemInfo> inventory; // 아이템값
bool loadInventory = false; // 한번 DB서버에서 값을 불러오면 게임서버 내부에서 처리
};
헤더에서 살펴봐야할점은 PlayerItemInfo라는 구조체입니다. 해당 구조체는 아이템 정보를 가지고있으며, 이미 가지고있지만 변화가 없는 아이템들은 DB에 재업로드를 하지않게 처리하기위한 is_new와 is_change 가 있습니다.
is_new 변수는 인벤토리에 새롭게 추가가 된 아이템일경우 true 가지고 있습니다.
is_change 변수는 해당 아이템값이 변경이 있을경우 사용합니다. 예를들어 소모품 개수가 늘거나 줄었을경우. 장비같은건 강화가 됐거나, 장착을했던가 장착을 해제했던가? 각종 상태변화가있을경우에 사용합니다.
각종함수는 본 헤더코드에 주석을 달아두었습니다.
PlayerInventory.cpp
#include "pch.h"
#include "PlayerInventory.h"
#include "CharacterDB.h"
PlayerInventory::PlayerInventory()
{
for (int i = 0; i < inventoryCnt; i++) {
PlayerItemInfo item;
inventory[i] = item;
}
}
PlayerInventory::~PlayerInventory()
{
}
void PlayerInventory::UpdateInventory(uint64 characterID)
{
// DB에 업데이트 요청
Protocol::C_DB_INSERTINVENTORY insertPkt;
Protocol::C_DB_UPDATEINVENTORY updatePkt;
for (const auto& [key, item] : inventory)
{
if (item.is_new) {
// 신규 아이템
Protocol::ItemData* itemData = insertPkt.add_items();
ConvertPlayerItemInfoToItemData(itemData, item);
}
else if (item.is_change) {
// 기존에 존재하는 템
Protocol::ItemData* itemData = updatePkt.add_items();
ConvertPlayerItemInfoToItemData(itemData, item);
}
}
CharacterDB::InsterPlayerInventoryItem(characterID, insertPkt);
CharacterDB::UpdatePlayerInventoryItem(characterID, updatePkt);
}
void PlayerInventory::DropItem(ServerItemData item)
{
// 아이템 얻음
// TODO 얻은 아이템이 소모품인지
bool bConsumable = item.Type == ItemType::Consumable;
// TODO 해당 소모품이 인벤토리에 존재하는지 그리고 제일 가까운 빈슬롯 찾기
uint64 targetCode = item.code;
bool found = false;
bool spaceSlot = false;
bool bOver = false;
int index = -1;
for (const auto& [key, item] : inventory)
{
if (bConsumable) {
if (item.code == 0 && spaceSlot == false) {
index = key; // 인덱스값
spaceSlot = true;
}
if (item.code == targetCode)
{
index = key; // 인덱스값
found = true; // 찾았는지?
break; // 소모품은 그위치에다가 값을 쓰면되닌깐 나감.
}
}
else {
if (item.code == 0) {
index = key; // 인덱스값
break;
}
}
}
if (bConsumable) {
// 소모품
if (found) {
// 더함
inventory[index].quantity++;
inventory[index].is_change = true; // true로 해야 나중에 DB에 업데이트 됨
return ;
}
}
//새로 추가
if (index == -1) {
// 인벤 꽉참
bOver = true;
return;
}
ConvertItemDataToPlayerItemInfo(index, inventory[index], item);
}
void PlayerInventory::LoadInventory(Protocol::S_DB_LOADINVENTORY& pkt)
{
for (auto& pktItem : pkt.items()) {
PlayerItemInfo item;
item.id = pktItem.id();
item.code = pktItem.code();
item.quantity = pktItem.quantity();
item.enhancement_level = pktItem.enhancement_level();
item.durability = pktItem.durability();
item.slot_index = pktItem.slot_index();
item.is_equipped = pktItem.is_equipped();
inventory[pktItem.slot_index()] = item;
}
// Load했다고 알림
loadInventory = true;
}
Protocol::S_LOADINVENTORY PlayerInventory::SendInventoryPkt()
{
Protocol::S_LOADINVENTORY pkt;
for (auto& item : inventory) {
Protocol::ItemData* myItem = pkt.add_items();
ConvertPlayerItemInfoToItemData(myItem, item.second); // 인벤토리 데이터를 넣음
}
return pkt;
}
void PlayerInventory::DeleteItem(int foundIndex)
{
PlayerItemInfo Keep = inventory[foundIndex];
PlayerItemInfo item;
inventory[foundIndex] = item;
// TODO DB에 Delete 패킷전송
CharacterDB::DeletePlayerInventoryItem(Keep.id);
}
bool PlayerInventory::UseInventoryItem(int64 instanceID, int itemcode)
{
int foundIndex = FoundItemIndex(instanceID);
// 해당아이템을 못찾음. 잘못된 요청처리
if (foundIndex == -1) {
LOG("NotFoundItem");
return false;
}
ServerItemData* item = ItemUtils::GetItem(itemcode);
if(item->Type == ItemType::Consumable){
// 소모품 사용
inventory[foundIndex].quantity--;
inventory[foundIndex].is_change = true;
if (inventory[foundIndex].quantity <= 0) {
// 소모품 다사용 인벤토리 제거
DeleteItem(foundIndex);
}
}
else if (item->Type == ItemType::Equipment)
{
// 장착장비
inventory[foundIndex].is_equipped = !inventory[foundIndex].is_equipped;
inventory[foundIndex].is_change = true;
}
LOG("UsedItem InstanceID : " << inventory[foundIndex].id << " ItemName : " << item->Name << " Item IsEquipment : " << inventory[foundIndex].is_equipped);
return true;
}
void PlayerInventory::ConvertItemDataToPlayerItemInfo(int32 slotIndex, PlayerItemInfo& playerInfo, ServerItemData item)
{
playerInfo.code = item.code;
playerInfo.quantity = 1;
playerInfo.slot_index = slotIndex;
playerInfo.is_new = true;
}
void PlayerInventory::ConvertPlayerItemInfoToItemData(Protocol::ItemData* item, PlayerItemInfo playerInfo)
{
item->set_id(playerInfo.id);
item->set_code(playerInfo.code);
item->set_quantity(playerInfo.quantity);
item->set_enhancement_level(playerInfo.enhancement_level);
item->set_slot_index(playerInfo.slot_index);
item->set_is_equipped(playerInfo.is_equipped);
}
int PlayerInventory::FoundItemIndex(int64 instanceID)
{
int foundIndex = -1;
for (const auto& [key, item] : inventory)
{
if (instanceID == item.id) {
foundIndex = key;
break;
}
}
return foundIndex;
}
Room.cpp (코드 일부)
if (player->Inventory->GetLoadInventory() == false) {
// TODO 인벤토리 DB서버에서 요청
CharacterDB::GetPlayerInventory(session, CharacterID); // 인벤토리 요청
}
else {
// TODO 메모리에 등록된 인벤토리 데이터 전송
Protocol::S_LOADINVENTORY inventoryPkt = player->Inventory->SendInventoryPkt();
SendBufferRef sendInventoryBuffer = ClientPacketHandler::MakeSendBuffer(inventoryPkt);
session->Send(sendInventoryBuffer);
}
인벤토리 데이터를 불러오는 작업입니다. 한번도 데이터를 불러온적이 없다면 DB서버에 데이터를 요청하고 클라이언트에게 데이터를 전달해줍니다.
하지만 한번 데이터를 받은뒤에는 GetLoadInventory함수가 True값을 리턴해서 게임서버가 가지고있는 데이터를 클라이언트에게 전달하게 됩니다.
'C++ > 이것저것서버테스트' 카테고리의 다른 글
[C++] 개인맵 및 제한시간 (0) | 2025.03.27 |
---|---|
[C++] 인벤토리 - 3 (장비 장착) (0) | 2025.03.25 |
[C++] 인벤토리 (1) | 2025.03.22 |
[C++] 서버에서 아이템 드랍 처리 (0) | 2025.03.22 |
[C++] 몬스터 정보, 아이템 정보 XML에서 불러오기 (0) | 2025.03.22 |