TEXT RPG의 후반 구현 직업 선택, 던전 진입, 몬스터 전투, 드랍 아이템, 인벤토리, 포션 제작, 포션 구매, 경험치와 레벨업 한 흐름 구현 리뷰.
Git : https://github.com/leeseungjae97/Simple-Text-RPG
전체 구조
PlayerMonsterPlayer→Warrior,Magician,Thief,ArcherMonster→Slime,Goblin,Orc,DragonInventory<t>main.cpp의 게임 루프
플레이어 전용 기능은 Player에 넣고, 각 직업은 Player를 상속받게 구현했다. 몬스터도 같은 방식으로 Monster를 기반 클래스로 두고, 각 몬스터가 개별 공격 함수를 오버라이드하게 구현했다.
스탯과 공용 데이터
스탯은 구조체 대신 인덱스 상수와 배열 조합으로 구현했다.
const int MAX_HP = 0;
const int MAX_MP = 1;
const int ATK = 2;
const int DEF = 3;
플레이어와 캐릭터는 이 인덱스를 기준으로 m_stat[SIZE] 배열을 사용하게 구현했다. 이렇게 구현하면 배열 기반이라 빠르고 단순하지만, 인덱스를 잘못 쓰면 바로 잘못된 값에 접근할 수 있기 때문에 상수 이름을 명확하게 두는 방식으로 관리했다.
공용 테이블도 pch.h에 모아서 구현했다.
- 직업 enum
- 몬스터 enum
- 포션 가격 테이블
- 드랍 아이템 2차원 배열
- 몬스터별 골드 보상
- 몬스터별 경험치 보상
- 몬스터 스폰 순서
- 포션 레시피
예를 들면 드랍 아이템은 몬스터 종류별로 4개씩 나오게 2차원 배열로 구현했다.
static Item dropTable[4][4] =
{
{{"Herb", 50}, {"Clear Water", 50}, {"Berry", 50}, {"Slime Jelly", 50}},
{{"Herb", 50}, {"Clear Water", 50}, {"Berry", 50}, {"Goblin ear", 50}},
{{"Herb", 50}, {"Clear Water", 50}, {"Berry", 50}, {"Orc finger", 100}},
{{"Herb", 50}, {"Clear Water", 50}, {"Berry", 50}, {"Dragon horn", 1000}}
};
즉, 하드코딩을 완전히 피하지는 않았지만, 최소한 게임 전체에서 반복해서 쓰는 데이터는 한곳에 묶어서 관리하는 형태로 구현했다.
플레이어 상태와 포션
플레이어는 최대 체력/마나와 현재 체력/마나를 분리해서 구현했다.
m_stat[MAX_HP],m_stat[MAX_MP]는 최대값m_HP,m_MP는 현재값
이렇게 분리해서 구현한 이유는 레벨업이나 강화로 최대 능력치가 바뀌더라도, 전투 중의 실시간 체력은 따로 관리해야 했기 때문이다.
피해 처리는 Damaged() 함수로 구현했다.
void Player::Damaged(int amount)
{
m_HP = max(0, m_HP - amount);
}
포션 사용도 UseHPP(), UseMPP()로 분리해서 구현했다. 여기서는 재고를 먼저 검사하고, 있으면 현재 HP/MP를 최대치 기준으로 회복하게 구현했다.
int restoreHP = min(m_stat[MAX_HP], m_HP + 50);
m_HP = restoreHP;
즉, 포션은 단순히 수치를 더하는 방식이 아니라 최대값을 넘지 않도록 min()을 써서 구현했다.
경험치와 레벨업
경험치는 Player::AddExp()에서 직접 처리하게 구현했다.
bool Player::AddExp(int value)
{
m_exp += value;
if (m_exp >= m_maxExp)
{
m_stat[MAX_HP] += 10;
m_stat[MAX_MP] += 5;
m_stat[ATK] += 5;
m_level++;
m_exp = 0;
m_maxExp += 50;
return true;
}
return false;
}
여기서 중요한 점은 레벨업 여부를 bool로 반환하게 구현했다는 점이다. 이렇게 구현하면 전투 함수 쪽에서 레벨업이 실제로 발생했을 때만 추가 성장 메뉴를 호출할 수 있다.
몬스터 구조
몬스터도 직업 구조와 비슷하게 구현했다. Monster에 공통 데이터와 가상 공격 함수를 두고, 각 몬스터가 자신의 능력치를 생성자에서 넘기게 구현했다.
Monster(string name, int hp, int atk, int def, eMonster type);
virtual int attack(Player* player) = 0;
예를 들면 슬라임은 이렇게 구현했다.
Slime::Slime()
: Monster("Slime", 100, 70, 10, eMonster::eSlime)
{
}
몬스터는 m_maxHp를 따로 저장하고, 전투 후 Restore()를 호출해서 다시 원래 체력으로 돌아가게 구현했다.
인벤토리
인벤토리는 STL vector가 아니라 템플릿 클래스 Inventory<t>로 직접 구현했다.
template <typename t="">
class Inventory
{
public:
void AddItem(T item);
void RemoveLastItem();
void PrintAllItems();
void Resize(int capacity);
void SortItems();
int FindItmeCount(string name);
bool UseItem(string name, int count);
};
포션 제작에 필요한 재료 소비를 위해 FindItmeCount()와 UseItem()도 같이 구현했다. 재료 개수를 먼저 확인하고, 충분하면 마지막 원소와 교환한 뒤 제거하는 방식으로 구현했다.
입력 처리와 메인 루프
입력은 단순 cin >> value만 쓰지 않고, 잘못된 숫자 입력을 막기 위해 ReadInt() 함수를 따로 구현했다.
int ReadInt()
{
int value;
while (!(cin >> value))
{
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << "Please enter a number: ";
}
return value;
}
즉, 잘못된 입력이 들어와도 프로그램이 바로 꼬이지 않도록 입력 검증 루프를 먼저 구현했다.
메인 루프는 MainMenu(Player* player)에서 구현했다. 여기서 다음 선택지를 반복해서 보여주게 구현했다.
- 던전 진입
- 인벤토리 확인
- 포션 상점
- 플레이어 상태 보기
- 종료
즉, 게임 전체 흐름은 Game()에서 캐릭터를 생성하고, MainMenu()에서 실제 플레이 루프를 돌게 구현했다.
전투
전투는 Battle(Player* player, Monster* monster) 함수 하나로 구현했다. 턴 순서는 고정 턴제 형태로 구현했다.
- 플레이어 턴
- 공격 또는 아이템 사용
- 몬스터 생존 확인
- 몬스터 턴
- 플레이어 생존 확인
플레이어는 공격 또는 포션 사용을 선택할 수 있게 구현했다.
몬스터가 죽으면 몬스터 타입을 기준으로 드랍 아이템을 랜덤하게 하나 주고, 골드와 경험치도 같이 지급하게 구현했다.
int ran = rand() % 4;
Item item = dropTable[monster->GetType()][ran];
inventory.AddItem(item);
gold += dropMony[monster->GetType()];
bool bLevelUp = player->AddExp(dropEXP[monster->GetType()]);
즉, 전투는 단순 체력 깎기만 한 것이 아니라, 드랍, 경험치, 레벨업, 상태 복구까지 연결한 전투 흐름으로 구현했다.
또한 보스 드래곤은 별도로 처리해서, 쓰러뜨리면 바로 게임 클리어가 출력되게 구현했다.
던전 구조
던전은 monsterPool 벡터와 stageCleared[] 배열을 조합해서 구현했다.
MakeDataPool()에서 몬스터 스폰 테이블을 읽고 실제 몬스터 객체를 미리 생성하게 구현했다.
for (int i = 0; i < maxDungeon; ++i)
{
eMonster type = monsterSpawn[i];
if(type == eSlime)
monsterPool.push_back(new Slime());
else if(type == eGoblin)
monsterPool.push_back(new Goblin());
else if (type == eOrc)
monsterPool.push_back(new Orc());
else
monsterPool.push_back(new Dragon());
}
던전 입장 화면에서는 각 방의 몬스터를 보여주고, 이전 방을 모두 클리어해야 보스 방을 열 수 있게 구현했다.
포션 상점과 제작
포션 상점은 unordered_map<string, int>를 사용해서 재고를 관리하게 구현했다.
unordered_map<string, int> potionStock_;
포션 가격도 shopItemTable로 따로 관리하게 구현했다. 레시피는 PotionRecipe 구조체 배열을 만들어서 vector<potionrecipe> recipe로 옮겨 담게 구현했다.
상점 메뉴에서는 다음 기능을 구현했다.
- 전체 레시피 보기
- 이름으로 레시피 검색
- 재료로 레시피 검색
- 포션 제작
- 포션 구매
재료 검색은 두 재료 필드를 모두 검사하게 구현했다.
if (recipe[i].igd1 == ingredient || recipe[i].igd2 == ingredient)
포션 제작은 인벤토리에서 재료 개수를 검사한 뒤, 충분하면 UseItem()으로 재료를 소비하고 포션 개수를 늘리게 구현했다.
프로젝트에서 사용한 문법
이번 프로젝트에서 직접 사용한 핵심 문법은 다음과 같다.
- 클래스와 상속
- 순수 가상 함수
- 오버라이드
- 템플릿 클래스
- 동적 배열과 메모리 관리
vectorunordered_map- enum
- 전역 테이블
- 입력 검증 루프
- 포인터 기반 객체 생성
'TIL' 카테고리의 다른 글
| FPS Project 1 (0) | 2026.05.06 |
|---|---|
| Rendering : Forward, Deferred (0) | 2026.05.04 |
| TEXT RPG 1 (0) | 2026.04.28 |
| EventGraph (0) | 2026.04.24 |
| 3인칭 캐릭터를 1인칭으로 사용할 때 얼굴 가림 문제 (0) | 2026.04.23 |