발전하는 춘배
[원고, C++/OOP] 로그라이크 게임 만들어보기 9 - 아이템과 인벤토리 2 : 엔티티 스탯 시스템, 아이템 데이터화, 데이터 로드, 테스트 본문
[원고, C++/OOP] 로그라이크 게임 만들어보기 9 - 아이템과 인벤토리 2 : 엔티티 스탯 시스템, 아이템 데이터화, 데이터 로드, 테스트
춘배0 2026. 2. 21. 12:29이어서 실제 아이템 데이터들을 만들어보자.
귀찮아서 gpt한테 만들어달라 했다.
[
{
"name": "Small Potion",
"type": "consumable",
"description": "Heals +20 health to the player",
"effects": [
{ "type": "heal", "value": 20 }
]
},
{
"name": "Large Potion",
"type": "consumable",
"description": "Heals +50 health to the player",
"effects": [
{ "type": "heal", "value": 50 }
]
},
{
"name": "Iron Sword",
"type": "weapon",
"description": "A basic iron sword that increases attack power",
"stats": {
"attack": 10
}
},
{
"name": "Steel Sword",
"type": "weapon",
"description": "A stronger sword forged from steel",
"stats": {
"attack": 18
}
},
{
"name": "Leather Armor",
"type": "armor",
"description": "Light armor that provides minimal protection",
"stats": {
"defense": 5
}
},
{
"name": "Iron Armor",
"type": "armor",
"description": "Heavy armor that greatly increases defense",
"stats": {
"defense": 12
}
}
]
오 잘 보니 소비템은 내가 애초에 effects로 설계해서 나중에 추가를 고려했었는데,
장비템은 무기는 attack, 갑옷은 defense만 고려했지 다른 스탯을 추가할 수 있단 생각을 못 했었다.
못 한건지 안 한건지.
아무튼 이왕 하는 거 확장성을 또 고려해보자 해서 EquipmentItem 멤버도 Stat 구조체로 수정해준다.
struct Stat {
std::string name;
int value;
};
class EquipmentItem : public Item {
private:
std::vector<Stat> stats;
...
}
지금은 무기는 공격력만, 갑옷은 방어력만 올려주도록 짜 놨지만
스탯을 전반적으로 적용해주도록 수정해주어야 한다.
1. 엔티티 스탯 시스템
지금 엔티티는
int damage;
int defense = 0;
이런 식으로 구현되어 있는데 스탯 시스템을 도입해보자.
enum class StatType {
MAXHP,
DAMAGE,
DEFENSE
};
이렇게 스탯타입을 만들어주고 엔티티에
std::unordered_map<StatType, int> stats;
로 저장한다.
근데 생각해보면 플레이어는 '기본 스탯'을 가지고 태어나고, 장비 착용에 따른 '추가 스탯'을 얻어 합산 적용받게 된다.
이걸 코드로 표현할 순 없을까?
gpt형님의 아이디어를 참고해봤다.
class StatBlock {
private:
std::unordered_map<StatType, int> baseStats;
std::unordered_map<StatType, int> bonusStats;
public:
StatBlock();
StatBlock(int maxHp, int damage, int defense);
int get(StatType type) const;
void addBonus(StatType type, int value);
void removeBonus(StatType type, int value);
};
이렇게 만들어주고
Entity에서는
StatBlock stats; 로 사용해준다.
그리고 WeaponItem과 ArmorItem의 equip()과 unequip()을 수정해주어야하는데,
원래 각각 공격력과 방어력을 따로따로 올려주어야 해서 분리해놓았지만
이제 스탯 시스템으로 합쳐졌으므로 얘네 로직도 합칠 수가 있다.
이전:
bool WeaponItem::equip(Entity& target) {
target.setDamage(target.getDamage() + damage);
return true;
}
bool WeaponItem::unequip(Entity& target) {
target.setDamage(target.getDamage() - damage);
return true;
}
bool ArmorItem::equip(Entity& target) {
target.setDefense(target.getDefense() + defense);
return true;
}
bool ArmorItem::unequip(Entity& target) {
target.setDefense(target.getDefense() - defense);
return true;
}
이후:
bool EquipmentItem::equip(Entity &target) {
for (const auto &stat : stats) {
target.addBonusStat(stat.type, stat.value);
}
return true;
}
bool EquipmentItem::unequip(Entity &target) {
for (const auto &stat : stats) {
target.removeBonusStat(stat.type, stat.value);
}
return true;
}
사실 bool의 의미가 있나 싶긴 한데 뜯어고치기 귀찮스러워서 그냥 놔둠.
시간이 꽤 걸렸는데 이걸 왜 했더라.. 하면
gpt한테 json데이터 좀 만들어줘라
-> 어라 장비 공격력, 방어력을 stats로 묶어놨네
-> 스탯 시스템 만들어야지
-> ???
이제 다시 이어서 해보자.
2. ItemDatabase 구현
모든 아이템 정보를 저장해둘 ItemDatabase 클래스를 만든다.
내부 구현은 일단 vector<Item>으로 하고,
JSON 파싱을 해서 파일로부터 데이터를 로드하는 static void loadFromFile(const std::string &path);
item의 find를 도와주는 findItembyItemName(); 를 일단 구현해본다. 왜냐하면 아이템 이름은 고유하게 만들려고 하기 때문.
윽 문제가 있다.
vector<Item>은 불가능하다!
당장 바로 저번에 Item은 추상 클래스이므로 값(객체)을 저장하는 pair 생성이 불가능하다는 걸 배웠다.
벡터도 마찬가지로 값을 저장하므로, Item은 저장할 수 없고 포인터를 저장해주어야 한다.
그렇게 되면 이 items는 아이템의 프로토타입들을 저장하는 역할을 하게 된다.
나중에 아이템이 필요하면, ItemDB에서 그 아이템을 복제해와서 사용하는 식으로 쓰면 됨.
class ItemDatabase {
private:
static std::vector<std::unique_ptr<Item>> items;
public:
static void loadFromFile(const std::string& path);
static const Item* findItemByName(const std::string& name);
};
또 지난번에 알아봤던 전방 선언으로 #include <Item>을 대신해준다.
실제 구현은 타일 데이터 저장했던 것처럼 nlohmann/json 사용했는데
라이브러리 사용법은 따로 알아보기도 귀찮고 굳이 알아야 하나 싶어서 gpt한테 백프로 맡겼다.
일단 지금은 벡터라서 탐색이 덜 효율적이긴 한데
아이템 개수도 많지 않고 테스트용이므로 일단 놔둔다.
나중에 탐색 시간이 너무 길다 싶으면 map으로 딸깍 바꿔주면 됨
gpt가 잘 써줬겠지 가정하고 테스트 코드를 작성해본다.
3. 에러 해결하기
하나. 컴파일
일단 컴파일 에러가 뜬다

컨수머블아이템의 생성자는
ConsumableItem(const std::string& name, const std::string& description, const std::vector<Effect>& effects);
이건데
gpt 코드는

이렇게 생겼다.
힐벨류를 effects에 넣어서 전달해줘야 할듯.
if (type == "consumable") {
int healValue = entry["effects"][0]["value"];
Effect effect;
effect.type = EffectType::HEAL;
effect.value = healValue;
std::vector<Effect> effects;
effects.push_back(effect);
items.push_back(
std::make_unique<ConsumableItem>(name, description, effects)
);
}
이렇게 수정해준다.
딱 봤을 때 문제가 없는 코드는 아니다. healValue를 effects의 고정된 순서에서 뽑아오고 ([0]), 다른 효과들은 고려하지 않았기 때문에 확장성이 없다.
근데 일단 지금 소비템은 체력회복포션밖에 없으므로 놔둔다.
언젠간 반드시 문제가 되겠지.
마찬가지로 밑에 분기들도 수정해준다.

else if (type == "weapon") {
int attack = entry["stats"]["attack"];
std::vector<Stat> stats;
stats.push_back(Stat{StatType::DAMAGE, attack});
items.push_back(
std::make_unique<WeaponItem>(name, description, stats)
);
}
else if (type == "armor") {
int defense = entry["stats"]["defense"];
std::vector<Stat> stats;
stats.push_back(Stat{StatType::DEFENSE, defense});
items.push_back(
std::make_unique<ArmorItem>(name, description, stats)
);
}
소비템이랑 같은 로직인데 struct 초기화 문법만 도입해봤다.
마찬가지로 확장성이 없다는 문제는 있으나 굳이 해결해야하나 싶어서(확장을 할까?) 넘어간다.
둘. 아이템 데이터 로드
테스트:
int main() {
TileDatabase::loadFromFile("data/tile_def.json");
std::cout << "Tile definitions loaded successfully.\n";
ItemDatabase::loadFromFile("data/item_data.json");
std::cout << "Item definitions loaded successfully.\n";
ItemDatabase::printAllItems();
결과:
Tile definitions loaded successfully.
Item definitions loaded successfully.
Small Potion
Large Potion
Iron Sword
Steel Sword
Leather Armor
Iron Armor
로딩이 잘 됐다.
셋: 아이템 획득, 장착, 사용
플레이어에게 아이템을 줘 보자. 테스트 코드를 짜다 보니 뭔가 찝찝스러운 부분이 있었다.
if (command == "/getTestItem") {
for (int i = 0; i < 3; i++) {
player->getInventory().addItem(*ItemDatabase::findItemByName("Small Potion"), 1);
}
player->getInventory().addItem(*ItemDatabase::findItemByName("Iron Sword"), 1);
player->getInventory().addItem(*ItemDatabase::findItemByName("Iron Armor"), 1);
return;
}
addItem()안에 들어가는 게 너무 복잡스럽다.
이게 왜 찝찝한지 대신 좀 깨달아줘요 GPT형님

지금 내 데이터베이스는 그냥 프로토타입 저장소로 사용되고 있다.
즉 플레이어한테 아이템을 주려면 위에서처럼 이 프로토타입을 복사해서 인벤토리에 넣어주는 식으로 되는데
이것 보다는
데이터베이스의 내부에는 프로토타입을 저장하는 게 맞지만,
실제 아이템도 데이터베이스가 생성해서 건내주는 게 더 좋다.
std::unique_ptr<Item> ItemDatabase::createItem(const std::string& name) {
const Item* item = findItemByName(name);
if (item) {
return item->clone();
}
return nullptr;
}
이렇게 생성시켜주면 된다.
그러면 실제로 아이템을 인벤토리에 넣어주는 코드는
if (command == "/getTestItem") {
for (int i = 0; i < 3; i++) {
player->getInventory().addItem(ItemDatabase::createItem("Small Potion"), 1);
}
player->getInventory().addItem(ItemDatabase::createItem("Iron Sword"), 1);
player->getInventory().addItem(ItemDatabase::createItem("Iron Armor"), 1);
return;
}
이렇게 좀 더 예뻐짐
결과:
Enter command: /getTestItem
Enter command: /inv
-----------Inventory-----------
Inventory (3/32):
1. Small Potion x3
Heals +20 health to the player
2. Iron Sword x1
A basic iron sword that increases attack power
3. Iron Armor x1
Heavy armor that greatly increases defense
-------------------------------
-----------Equipment-----------
-------------------------------
아이템 지급이 잘 됐다.
장착해보자. 플레이어 기본 스탯은 이렇다.
Enter command: /stat
-----------Player Info-----------
Max HP: 30
Damage: 10
Defense: 0
HP: 30
---------------------------------
장비 장착
Enter command: /equip
Enter item slot: 2
Enter command: /inv
-----------Inventory-----------
Inventory (3/32):
1. Small Potion x3
Heals +20 health to the player
2. Iron Sword x1
A basic iron sword that increases attack power
3. Iron Armor x1
Heavy armor that greatly increases defense
-------------------------------
-----------Equipment-----------
Weapon: Iron Sword
-------------------------------
Enter command: /stat
-----------Player Info-----------
Max HP: 30
Damage: 20
Defense: 0
HP: 30
---------------------------------
대미지가 잘 올랐다.
갑옷도 입어보자.
Enter command: /equip
Enter item slot: 3
Enter command: /inv
-----------Inventory-----------
Inventory (3/32):
1. Small Potion x3
Heals +20 health to the player
2. Iron Sword x1
A basic iron sword that increases attack power
3. Iron Armor x1
Heavy armor that greatly increases defense
-------------------------------
-----------Equipment-----------
Weapon: Iron Sword
Armor: Iron Armor
-------------------------------
Enter command: /stat
-----------Player Info-----------
Max HP: 30
Damage: 20
Defense: 12
HP: 30
---------------------------------
방어력도 잘 올랐다.
적한테 피해를 입고 포션을 마셔보자.
아 공격력이 올랐더니 체력이 15인 적들이 원콤이 나버린다. (피해를 못 입는다)
장비 해제 커맨드는 만들질 않아서 게임 다시 시작해서 포션 테스트만 다시 해본다.
Enter command: /stat
-----------Player Info-----------
Max HP: 30
Damage: 10
Defense: 0
HP: 10
---------------------------------
Enter command: /use
Enter item slot: 1
Consumed Small Potion | HP +20.
Enter command: /inv
-----------Inventory-----------
Inventory (3/32):
1. Small Potion x2
Heals +20 health to the player
2. Iron Sword x1
A basic iron sword that increases attack power
3. Iron Armor x1
Heavy armor that greatly increases defense
-------------------------------
-----------Equipment-----------
-------------------------------
Enter command: /stat
-----------Player Info-----------
Max HP: 30
Damage: 10
Defense: 0
HP: 30
---------------------------------
체력이 10이었는데 포션을 사용했더니 인벤토리에서 하나 개수가 줄고 체력이 찼다.
성공!
'.원고' 카테고리의 다른 글
| [원고 / GCP, Docker, Code Server] GCP VM에 코드서버 띄우기 (0) | 2026.03.06 |
|---|---|
| [원고, C++/OOP] 로그라이크 게임 만들어보기 10 - 상태 머신 (0) | 2026.02.21 |
| [원고, C++/OOP] 로그라이크 게임 만들어보기 8 - 아이템과 인벤토리 1 : 설계, 추상 클래스, 상속, 순환 참조, 전방 선언 (0) | 2026.02.19 |
| [원고, C++/OOP] 로그라이크 게임 만들어보기 7 - 카메라 : 카메라와 렌더링(출력) 아키텍처, AI 에이전트를 이용한 디버깅 (0) | 2026.02.14 |
| [원고, C++/OOP] 로그라이크 게임 만들어보기 6 - 랜덤 맵 생성하기 : BSP 알고리즘, 플레이어 스폰, 몬스터 스폰 (0) | 2026.02.14 |
