TEXT RPG의 후반 구현 직업 선택, 던전 진입, 몬스터 전투, 드랍 아이템, 인벤토리, 포션 제작, 포션 구매, 경험치와 레벨업 한 흐름 구현 리뷰.


Git : https://github.com/leeseungjae97/Simple-Text-RPG

전체 구조

  • Player
  • Monster
  • PlayerWarrior, Magician, Thief, Archer
  • MonsterSlime, Goblin, Orc, Dragon
  • Inventory<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 &gt;= 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) 함수 하나로 구현했다. 턴 순서는 고정 턴제 형태로 구현했다.

  1. 플레이어 턴
  2. 공격 또는 아이템 사용
  3. 몬스터 생존 확인
  4. 몬스터 턴
  5. 플레이어 생존 확인

플레이어는 공격 또는 포션 사용을 선택할 수 있게 구현했다.

몬스터가 죽으면 몬스터 타입을 기준으로 드랍 아이템을 랜덤하게 하나 주고, 골드와 경험치도 같이 지급하게 구현했다.

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 &lt; 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()으로 재료를 소비하고 포션 개수를 늘리게 구현했다.

프로젝트에서 사용한 문법

이번 프로젝트에서 직접 사용한 핵심 문법은 다음과 같다.

  • 클래스와 상속
  • 순수 가상 함수
  • 오버라이드
  • 템플릿 클래스
  • 동적 배열과 메모리 관리
  • vector
  • unordered_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