하아찡
[C++] 인벤토리 본문
언리얼 버전 5.5.3
서버언어 C++로 구성했습니다.
RPG게임하면 인벤토리는 필수인 상황이다보니 인벤토리도 구성을 해봤습니다
일단 DB에다가 저장하기위해서 테이블을 하나 생성해줍니다.
인벤토리도 일반적으로 한칸씩 있는 인벤토리도있고, 디아블로처럼 아이템이 여러칸을 차지하게 하는 인벤토리도 존재하는데 일반적인 인벤토리로 구현했습니다.
실행화면
문제발생
로그인 할때 여러번 누르면,요청이 여러번들어가고 캐릭터정보도 여러번오네요... (이건 해당 본문내용과 별개입니다.)
인벤토리정보가 살짝 늦게오니 바로 인벤토리열었을때닌 인벤토리에 아무것도 없을때가 있네요. 이건 클라이언트를 잘 못다뤄서 타이밍적으로 어디서 처리를해야할지몰라서 예상했던 결과입니당...
CREATE TABLE `inventory` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`character_id` INT(10) NOT NULL,
`slot_index` INT(10) NOT NULL,
`item_id` INT(10) NOT NULL,
`quantity` INT(10) NULL DEFAULT '1',
`enhancement_level` INT(10) NULL DEFAULT NULL,
`durability` INT(10) NULL DEFAULT NULL,
`is_equipped` TINYINT(1) NULL DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE
)
;
해당 테이블에서 id값은 동일한 아이템이여도 다르게 구분해주는 고유값입니다.
아이템의 값은 item_id에 저장하게됩니다.
slot_index는 현재 아이템이 어느 위치에있는 아이템인지 구분해주기 위해 사용했고,
quantity는 포션같이 수량이 필요한 소모품들의 개수를 저장하기위해 넣어놨습니다.
enhancement_level은 장비일경우 강화수치를 저장하기위해서 넣어놨습니다.
durability는 장비의 내구도입니다.
is_equipped는 장비의 장착 여부인데 장착중인 장비를 확인하기위해서 사용했습니다.
그리고 또 장비가 있다면 캐릭터가 장비를 끼고있는지를 저장하는 테이블이 필요합니다.
CREATE TABLE `equipment` (
`character_id` INT(10) NOT NULL,
`slot_name` VARCHAR(20) NOT NULL COLLATE 'utf8mb4_0900_ai_ci',
`instance_id` INT(10) NOT NULL,
PRIMARY KEY (`character_id`, `slot_name`) USING BTREE
)
;
그래서 equiment 테이블을 만들었고
character_id는 캐릭터 id값을 넣어줍니다.
slot_name은 어떤 장비인지 장비슬롯이름을 저장합니다.
instance_id는 위에 inventory테이블에 id값을 저장하게 됩니다.
일단 현재는 클라이언트와 서버에 아이템 습득 기능이 없기때문에 DB에 임의로 데이터를 넣었습니다.
이제 해당 인벤토리 정보를 캐릭터가 접속했을때 서버측에 요청하게 됩니다.
실제로는 캐릭터가 게임서버에 접속하면 게임서버가 DB서버에 해당 캐릭터 인벤토리 및 장착중인 장비를 요청하게 됩니다.
현재는 인벤토리값만 요청하게 작업을 했습니다.
그래서 DB서버에서 데이터를 불러와서 다시 게임서버로 전달해주고 게임서버가 플레이어에게 전달해주는 방식으로 했습니다.
게임서버와 클라이언트간에 추가된 패킷프로토콜입니다.
message C_LOADINVENTORY
{
}
message S_LOADINVENTORY
{
repeated ItemData items = 1;
}
message C_LOADEQUIPMENT
{
}
message S_LOADEQUIPMENT
{
repeated ItemData items = 1;
}
게임서버와 DB서버간에 추가된 패킷 프로토콜입니다.
message C_DB_LOADEQUIPMENT
{
uint64 characterid = 1;
uint64 requestid = 2;
}
message S_DB_LOADEQUIPMENT
{
uint64 requestid = 1;
repeated ItemData items = 2;
}
message C_DB_LOADINVENTORY
{
uint64 characterid = 1;
uint64 requestid = 2;
}
message S_DB_LOADINVENTORY
{
uint64 requestid = 1;
repeated ItemData items = 2;
}
게임서버와 DB서버간에 requestID값이 필요한 이유는 게임서버가 요청을 보냈는데 어떤 플레이어의 요청인지는 DB서버는 모르기때문에 요청ID값을 같이 보내고 다시 DB서버에서도 요청ID값을 다시보내주는 방식으로 사용했습니다.
게임서버로 요청ID값이 있으면 해당 플레이어에게 값을 보내줄 수 있으닌깐요..!
게임서버가 요청한 정보를 DB서버가 처리 후 전달해주는 코드입니다.
bool Handle_C_DB_LOADEQUIPMENT(PacketSessionRef& session, Protocol::C_DB_LOADEQUIPMENT& pkt)
{
//플레이어 접속할때 인벤토리 요청 처리
Protocol::S_DB_LOADEQUIPMENT sendPkt;
DBConnection* dbConn = GDBConnectionPool->Pop();
dbConn->Unbind();
DBBind<1, 8> dbBind(*dbConn, "SELECT i.id, i.slot_index, i.item_id, i.quantity, i.enhancement_level, i.durability, i.is_equipped, eq.slot_name FROM equipment AS eq LEFT JOIN inventory AS i ON eq.instance_id = i.id WHERE eq.character_id = ?");
int64 DBID = pkt.characterid();
dbBind.BindParam(0, DBID);
int32 out_id;
int32 out_slotindex;
int32 out_itemcode;
int32 out_quanty;
int32 out_enhancement;
int32 out_durability;
bool out_equipped;
char out_slotname[256] = { 0 };
dbBind.BindCol(0, OUT out_id);
dbBind.BindCol(1, OUT out_slotindex);
dbBind.BindCol(2, OUT out_itemcode);
dbBind.BindCol(3, OUT out_quanty);
dbBind.BindCol(4, OUT out_enhancement);
dbBind.BindCol(5, OUT out_durability);
dbBind.BindCol(6, OUT out_equipped);
dbBind.BindCol(7, OUT out_slotname, 255);
ASSERT_CRASH(dbBind.Execute());
while (dbConn->Fetch())
{
sendPkt.set_requestid(pkt.requestid());
Protocol::ItemData* item = sendPkt.add_items();
item->set_id(out_id);
item->set_slot_index(out_slotindex);
item->set_code(out_itemcode);
item->set_quantity(out_quanty);
item->set_enhancement_level(out_enhancement);
item->set_durability(out_durability);
item->set_is_equipped(out_equipped);
string slotName(out_slotname);
item->set_slot_name(slotName);
}
GDBConnectionPool->Push(dbConn);
SendBufferRef sendBuffer = DBClientPacketHandler::MakeSendBuffer(sendPkt);
session->Send(sendBuffer);
return false;
}
bool Handle_C_DB_LOADINVENTORY(PacketSessionRef& session, Protocol::C_DB_LOADINVENTORY& pkt)
{
//플레이어 접속할때 인벤토리 요청 처리
Protocol::S_DB_LOADINVENTORY sendPkt;
DBConnection* dbConn = GDBConnectionPool->Pop();
dbConn->Unbind();
DBBind<1, 7> dbBind(*dbConn, "SELECT id, slot_index, item_id, quantity, enhancement_level, durability, is_equipped FROM inventory WHERE character_id = ?");
int64 DBID = pkt.characterid();
dbBind.BindParam(0, DBID);
int32 out_id;
int32 out_slotindex;
int32 out_itemcode;
int32 out_quanty;
int32 out_enhancement;
int32 out_durability;
bool out_equipped;
dbBind.BindCol(0, OUT out_id);
dbBind.BindCol(1, OUT out_slotindex);
dbBind.BindCol(2, OUT out_itemcode);
dbBind.BindCol(3, OUT out_quanty);
dbBind.BindCol(4, OUT out_enhancement);
dbBind.BindCol(5, OUT out_durability);
dbBind.BindCol(6, OUT out_equipped);
ASSERT_CRASH(dbBind.Execute());
while (dbConn->Fetch())
{
sendPkt.set_requestid(pkt.requestid());
Protocol::ItemData* item = sendPkt.add_items();
item->set_id(out_id);
item->set_slot_index(out_slotindex);
item->set_code(out_itemcode);
item->set_quantity(out_quanty);
item->set_enhancement_level(out_enhancement);
item->set_durability(out_durability);
item->set_is_equipped(out_equipped);
}
GDBConnectionPool->Push(dbConn);
SendBufferRef sendBuffer = DBClientPacketHandler::MakeSendBuffer(sendPkt);
session->Send(sendBuffer);
LOG("SendInventory");
return false;
}
bool Handle_C_DB_PING(PacketSessionRef& session, Protocol::C_DB_PING& pkt)
{
return false;
}
DB서버는 뭐 DB데이터 불러다가 다시 전달해주면 되닌깐 간단하고...
대신 장작정보는 left join을 사용해서 값을 가져왔습니다.
두둥 이제는 게임서버쪽 처리입니다.
위에서 말했듯이 입장했을때 게임서버가 각종 정보를 요청하게됩니다.
여기서 DB서버로 요청하는 함수를 호출하고
// 입장하는 클라이언트에게 입장됐다고 알림.
if(auto player = dynamic_pointer_cast<Player>(object))
{
Protocol::S_ENTER_GAME enterPkt;
enterPkt.set_success(true);
Protocol::ObjectInfo* playerInfo = enterPkt.mutable_objectinfo();
playerInfo->CopyFrom(*player->objectInfo);
//LOG("EnterRoom POS : X : " << player->posInfo->x());
SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(enterPkt);
if (auto session = player->session.lock()) {
//LOG("EnterRoom : Object ID : " << player->objectInfo->object_id());
session->Send(sendBuffer);
uint64 CharacterID = session->myCharacterDBID[player->objectInfo->object_id()];
// TODO 인벤토리 요청 / 장착중인 장비 요청.
Protocol::C_DB_LOADINVENTORY loadInvenPkt;
CharacterDB::GetPlayerEquipment(session, CharacterID); // 장비 요청
CharacterDB::GetPlayerInventory(session, CharacterID); // 인벤토리 요청
}
}
호출된 함수를 살펴보고...
void CharacterDB::GetPlayerEquipment(GameSessionRef gamesession, uint64 characterDBID)
{
int64 DBGenerator = ObjectUtils::s_DBGenerator.fetch_add(1);
g_requestMap[DBGenerator] = gamesession;
Protocol::C_DB_LOADEQUIPMENT pkt;
pkt.set_characterid(characterDBID);
pkt.set_requestid(DBGenerator);
SendBufferRef sendBuffer = DBServerPacketHandler::MakeSendBuffer(pkt);
GDBManager.Broadcast(sendBuffer);
// To -> S_LOADEQUIPMENT 쪽으로
}
void CharacterDB::GetPlayerInventory(GameSessionRef gamesession, uint64 characterDBID)
{
// DB서버에게 인벤토리 데이터 요청
int64 DBGenerator = ObjectUtils::s_DBGenerator.fetch_add(1);
g_requestMap[DBGenerator] = gamesession;
Protocol::C_DB_LOADINVENTORY pkt;
pkt.set_characterid(characterDBID);
pkt.set_requestid(DBGenerator);
SendBufferRef sendBuffer = DBServerPacketHandler::MakeSendBuffer(pkt);
GDBManager.Broadcast(sendBuffer);
// To -> S_LOADINVENTORY 쪽으로
}
이제 DB서버에서 받아온 패킷을 처리합니다!
bool Handle_S_DB_LOADINVENTORY(PacketSessionRef& session, Protocol::S_DB_LOADINVENTORY& pkt)
{
LOG("LOAD INVEN");
auto it = CharacterDB::g_requestMap.find(pkt.requestid());
if (it != CharacterDB::g_requestMap.end())
{
GameSessionRef session = it->second.lock();
if (session)
{
Protocol::S_LOADINVENTORY sendPkt;
for (auto item : pkt.items()) {
Protocol::ItemData* myItem = sendPkt.add_items();
myItem->CopyFrom(item);
}
// 클라이언트에게 플레이어 인벤토리 전달
SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(sendPkt);
session->Send(sendBuffer);
}
// 반드시 지우기
CharacterDB::g_requestMap.erase(it);
}
else
{
// 잘못된 requestId 응답 처리
return false;
}
return true;
}
그래서 패킷을 처리하면서 동시에 다시 요청ID값을 가지고 해당 클라이언트에게 값을 보내게 됩니다.
'C++ > 이것저것서버테스트' 카테고리의 다른 글
[C++] 인벤토리 - 3 (장비 장착) (0) | 2025.03.25 |
---|---|
[C++] 인벤토리 - 2 (인벤토리 서버저장) (0) | 2025.03.25 |
[C++] 서버에서 아이템 드랍 처리 (0) | 2025.03.22 |
[C++] 몬스터 정보, 아이템 정보 XML에서 불러오기 (0) | 2025.03.22 |
[C++] 서버 DB업로드 (1) | 2025.03.20 |