이 Step에서 다루는 것

  • “작은 게임”을 함수 단위로 쪼개서 흐름을 만드는 연습
  • 데이터(스탯)와 로직(전투/선택)을 분리하는 감각
  • 디버깅 연습: 브레이크포인트/조건부/콜스택/트레이스를 실제로 어디에 쓰는지

학습 목표

  • main → 로비 → 필드 → 전투 흐름을 머릿속에 그리고, 코드를 따라갈 수 있다.
  • “값이 이상하다/흐름이 꼬였다”를 디버깅으로 증거 기반으로 해결할 수 있다.

전체 게임 흐름(텍스트RPG 버전)

이미지가 없어도 흐름이 잡히도록, 텍스트로 “길”부터 고정합니다.

main()
  └─ EnterLobby()
       ├─ SelectPlayer()
       ├─ (입력) 1: EnterField()
       │          ├─ CreateRandomMonster()
       │          ├─ (입력) 1: EnterBattle()
       │          │            └─ WaitForNextKey()
       │          └─ (입력) 그 외: 로비로 복귀
       └─ (입력) 그 외: 종료

핵심은 “함수 이름만 봐도 흐름이 읽히는 구조”를 만드는 것입니다.


게임 루프 기본 구조(정석)와 텍스트RPG의 타협

정석 게임 루프:

입력(Input) → 로직(Logic) → 출력(Render)

텍스트RPG 예제에서는 출력/입력이 코드에 섞여 있습니다(콘솔 기반이라 어쩔 수 없는 타협).
하지만 “머릿속에서는” 항상 Input/Logic/Render를 분리해서 생각하는 습관이 중요합니다.


데이터 설계(스탯/타입)와 전역 사용에 대한 주의

강의에서는 “구조를 단순하게 만들기 위해” 전역 변수를 사용했습니다.
학습 초반에는 괜찮지만, 규모가 커지면 전역은 디버깅 지옥이 되기 쉽습니다.

그래서 “일단 전역으로 만들고(학습용), 다음 단계에서 인자로 넘겨서 전역을 제거한다”를 목표로 잡으면 좋습니다.

enum PlayerType { PT_None, PT_Knight, PT_Archer, PT_Mage };
enum MonsterType { MT_None, MT_Slime, MT_Orc, MT_Skeleton };

struct StatInfo {
    int hp;
    int attack;
    int defence;
};

PlayerType playerType;
StatInfo playerStat;

MonsterType monsterType;
StatInfo monsterStat;
  • 핵심 1: enum으로 매직 넘버를 없애서 읽히는 코드를 만든다.
  • 핵심 2: StatInfo로 데이터를 묶어서 “HP/ATT/DEF” 관련 로직을 반복하지 않는다.

main()의 역할: “초기화 + 진입점”

int main() {
    srand((unsigned int)time(0));  // 난수 시드 초기화
    EnterLobby();  // 로비 함수로 진입 → 게임 시작
    return 0;
}
  • srand(time(0))rand()가 매번 같은 패턴을 뱉지 않게(의사난수 시드) 설정합니다.
  • main()은 최대한 짧게: “세팅하고 시작한다”만 남기면, 디버깅/구조 파악이 쉬워집니다.

EnterLobby(): 최상위 메뉴 루프(“나가지 않는 이유”가 명확해야 함)

void EnterLobby() {
    while (true) {
        SelectPlayer();  // 직업 선택
        int input;
        cin >> input;

        if (input == 1)
            EnterField();  // 필드로 입장
        else
            return;  // 게임 종료
    }
}

학습 포인트:

  • while (true) 루프를 쓸 때는 “어떤 조건에서 나가는지(return/break)”가 코드에 반드시 드러나야 합니다.
  • 로비는 게임의 “상위 상태”입니다. 아래(필드/전투)에서 return으로 빠져나오면 로비로 복귀합니다.

SelectPlayer(): 입력 검증 + 상태 초기화

void SelectPlayer() {
    while (true) {
        int choice;
        cin >> choice;
        playerType = (PlayerType)choice;

        if (choice == PT_Knight) {
            playerStat = {150, 10, 5};
            break;
        } else if (choice == PT_Archer) {
            playerStat = {100, 15, 3};
            break;
        } else if (choice == PT_Mage) {
            playerStat = {80, 25, 0};
            break;
        }
    }
}

학습 포인트:

  • “잘못 입력하면 다시”는 게임에서 정말 흔한 패턴입니다. 그래서 while(true) + break가 자주 등장합니다.
  • 여기서 버그가 나면 이후 모든 흐름이 깨집니다. 직업 선택 직후는 브레이크포인트를 걸기 좋은 지점입니다.

EnterField(): 상태 출력 → 몬스터 생성 → 선택(전투/도주)

void EnterField() {
    while (true) {
        cout << "[PLAYER] HP : " << playerStat.hp
             << " / ATT : " << playerStat.attack
             << " / DEF : " << playerStat.defence
             << '\n';

        CreateRandomMonster();  // 랜덤 몬스터 생성

        int input;
        cin >> input;
        if (input == 1) {
            EnterBattle();  // 전투 시작
            if (playerStat.hp == 0)
                return;  // 사망 시 리턴
        } else {
            return;  // 도주 시 리턴
        }
    }
}

학습 포인트:

  • EnterField()는 “전투를 여러 번 할 수 있는 상태”이므로 루프가 자연스럽습니다.
  • playerStat.hp == 0 체크는 “전투 종료 후 상위 상태가 후처리”를 하는 전형적인 흐름입니다.

실전 디버깅 포인트:

  • “전투가 끝났는데 로비로 안 돌아간다” → EnterField()의 리턴 조건에 브레이크포인트
  • “체력이 음수가 된다/이상하다” → EnterBattle()에서 HP 감소 직후 조건부 브레이크(hp < 0)

CreateRandomMonster(): 랜덤 스폰 + enum으로 읽히게 만들기

void CreateRandomMonster() {
    int randomChoice = 1 + rand() % 3;

    switch (randomChoice) {
        case MT_Slime:
            monsterStat = {30, 2, 0};
            break;
        case MT_Orc:
            monsterStat = {80, 15, 5};
            break;
        case MT_Skeleton:
            monsterStat = {100, 20, 10};
            break;
    }
}

학습 포인트:

  • rand() % 3의 범위는 0~2이므로 +11~3을 만든다는 감각을 확실히 잡아야 합니다.
  • 이 함수는 “데이터 세팅 함수”이므로, 전투 버그가 나면 여기서 스탯이 제대로 들어갔는지부터 확인합니다.

EnterBattle(): 전투 루프의 최소 형태

void EnterBattle() {
    while (true) {
        int damage = playerStat.attack - monsterStat.defence;
        if (damage < 0) damage = 0;
        monsterStat.hp -= damage;

        if (monsterStat.hp <= 0) {
            cout << "몬스터 처치!" << '\n';
            WaitForNextKey();
            return;
        }

        int damage2 = monsterStat.attack - playerStat.defence;
        if (damage2 < 0) damage2 = 0;
        playerStat.hp -= damage2;

        if (playerStat.hp <= 0) {
            cout << "플레이어 사망..." << '\n';
            WaitForNextKey();
            playerStat.hp = 0; // 상위 상태에서 조건 체크가 명확해짐
            return;
        }
    }
}

학습 포인트(전투 로직 핵심):

  • 데미지 계산은 반드시 “음수면 0” 보정이 필요합니다. (방어력이 더 크다고 힐이 되면 이상함)
  • HP는 0 아래로 내려갈 수 있으니, 종료 조건을 <= 0 로 잡는 습관이 중요합니다.

실전 디버깅 포인트(추천):

  • monsterStat.hp -= damage; 줄에 브레이크포인트 → 몬스터 HP가 줄어드는지 확인
  • playerStat.hp -= damage2; 줄에 조건부 브레이크포인트 → playerStat.hp <= 0일 때만 멈추기
  • Call Stack으로 “필드에서 전투로 들어온 경로”가 맞는지 확인

WaitForNextKey(): 흐름을 끊어 확인하기(디버깅 보조)

void WaitForNextKey() {
    cout << "계속하려면 1을 누르세요 >> ";
    int input;
    cin >> input;
    system("cls");
}

학습 포인트:

  • 콘솔 게임에서는 메시지가 순식간에 지나가므로 “확인용 대기”가 유용합니다.

주의:

  • system("cls")는 Windows 콘솔에 의존합니다. (다른 환경에서는 동작이 다를 수 있음)
  • system() 호출은 보안/성능 관점에서 지양되는 경우가 많습니다. (학습용으로만 사용)

TextRPG 학습 포인트(실전 버그/디버깅까지)

  • 구조 연습: “큰 코드 1개”가 아니라 함수들로 흐름을 만든다
  • 데이터 묶기: StatInfo로 데이터 뭉치기 → 이후 “인자로 넘기는 설계”로 확장 가능
  • 실수 포인트 TOP 3:
    • HP가 0 아래로 내려가는데 출력이 이상함<= 0 체크/0으로 고정(clamp) 확인
    • 몬스터 체력이 안 줄어듦monsterStat.hp 대신 playerStat.hp를 깎는 오타(브레이크포인트로 즉시 발견)
    • 무한 루프while(true)에서 나가는 조건이 실제로 실행되는지(Call Stack + 조건부 브레이크)

체크 질문 (스스로 답해보기)

  • “전투가 끝나면 왜 EnterBattle()에서 return을 하게 만들었을까?”
  • “필드에서 도망치면 왜 return이 맞고, break면 왜 부족할까?”
  • “전역 변수가 편한데도, 프로젝트가 커지면 왜 위험해질까?”

profile
李家네_공부방

0개의 댓글