코딩

[C++] 전직 & 전투 시스템 구현— Project HW02

story98138 2026. 3. 11. 19:31

1. 오늘의 개발 개요

캐릭터의 성장과 재미를 결정짓는 핵심 요소인 전직(Job Change) 시스템 전투(Battle) 로직을 설계하고 구현했습니다. C++의 클래스 상속과 다형성을 적극 활용해 확장 가능한 구조를 목표로 잡았고, Visual Studio 2022 환경에서 HW02 솔루션을 구성하며 빌드 파이프라인도 함께 정비했습니다.


2. 프로젝트 환경 설정

개발 환경 Visual Studio 2022 (Version 17.14)
솔루션 HW02.sln
소스 파일 Player, Warrior, Magician, Monster, Main (총 9개 파일)

 


3. 클래스 구조 설계

공통 속성과 인터페이스를 Player 베이스 클래스에 집중시키고, 직업별 특성은 파생 클래스에서 구현했습니다. attack() 순수 가상 함수로 선언해 Player 객체를 직접 생성하지 못하도록 강제하고, 직업마다 고유 공격 동작을 반드시 구현하게 했습니다.

 

 

// Player.h

class Player
{
public:
    Player(std::string Nickname);

    virtual void attack() = 0;   // 순수 가상 함수 → 추상 클래스
    void printPlayerStatus();
    void TakeDamage(int damage);
    virtual ~Player() {}           // 가상 소멸자

    // Getter 함수들...

protected:
    std::string Job_Name, Nickname;
    int Level, HP, MP, Power, Defence, Accuracy, Speed;
};
💡 순수 가상 함수를 쓴 이유
  • virtual void attack() = 0; 선언으로 Player는 추상 클래스가 됨 → new Player() 직접 생성 불가
  • 파생 클래스가 attack()을 반드시 override하도록 컴파일 단계에서 강제
  • Main에서 Player* 포인터 하나로 Warrior·Magician을 동일하게 다룰 수 있는 다형성 확보

4. 전직 시스템 구현

닉네임 입력 후 직업을 선택하면 전직이 완료됩니다. 파생 클래스 생성자에서 베이스 이니셜라이저(: Player(Nickname))로 공통 초기화를 먼저 수행하고, 이후 직업 보너스 스탯을 추가하는 방식입니다.

// Warrior.cpp
Warrior::Warrior(std::string Nickname) : Player(Nickname)
{
    this->Job_Name = "전사";
    this->HP    += 50;
    this->Power += 20;
}

void Warrior::attack()
{
    std::cout << "전사 [ " << this->Nickname << " ] : 검을 휘두릅니다!" << std::endl;
}

 

// Magician.cpp
Magician::Magician(std::string Nickname) : Player(Nickname)
{
    this->Job_Name = "마도사";
    this->HP    += 10;
    this->MP    += 50;
    this->Power += 30;
}

void Magician::attack()
{
    std::cout << "마도사 [ " << this->Nickname << " ] : 파이어볼을 시전합니다!" << std::endl;
}

 

// Main.cpp - 전직 선택 흐름
Player* player = nullptr;

std::cout << "* 닉네임을 입력해주세요: ";
std::cin >> nickname;

switch (job_choice) {
case 1: player = new Warrior(nickname);   break;
case 2: player = new Magician(nickname);  break;
default:
    std::cout << "잘못된 입력입니다." << std::endl;
    return 1;
}

 

💡 직업별 최종 스탯 비교 (초보자 기준 스탯에서 증가)

직업 HP MP 공격력 방어력
초보자 100 50 10 5
전사 150(+50) 50 30(+20) 5
마도사 110(+10) 100(+50) 40(+30) 5

 


5. 전투 시스템 구현

몬스터 "타락한 나무정령"(HP 200, 공격력 15)과의 턴제 전투입니다. 플레이어가 먼저 공격 → 몬스터 생존 시 반격하는 구조이며, 플레이어 피해에는 방어력 감소 공식이 적용됩니다.

 

// Player.cpp - 방어력 적용 피해 계산
void Player::TakeDamage(int damage) {
    int actualDamage = damage - this->Defence;
    if (actualDamage <= 0) {
        actualDamage = 1;   // 최소 1 데미지 보장
    }
    this->HP -= actualDamage;
    if (this->HP < 0) { this->HP = 0; }

    std::cout << "[ 시스템 ] " << this->Nickname
              << "이(가) " << actualDamage << "의 피해를 입었습니다! (방어력 적용됨)"
              << std::endl;
}

 

// Main.cpp - 턴제 전투 루프
Monster* treeSpirit = new Monster("타락한 나무정령", 200, 15);

while (true)
{
    std::cout << "1. 공격하기  2. 상태창 보기  3. 게임 종료" << std::endl;
    std::cin >> action_choice;

    if (action_choice == 1) {
        player->attack();                                    // 직업별 공격 메시지
        treeSpirit->TakeDamage(player->getPower());         // 공격력만큼 피해

        if (treeSpirit->getHP() <= 0) {
            std::cout << "*** 타락한 나무정령을 물리쳤습니다! 전투 승리! ***" << std::endl;
            break;
        }

        std::cout << "[ 타락한 나무정령의 반격! 꽃가루 뿌리기!! ]" << std::endl;
        player->TakeDamage(treeSpirit->getAttackPower());    // 방어력 차감 후 피해

        if (player->getHP() <= 0) {
            std::cout << "*** 플레이어가 쓰러졌습니다... 게임 오버. ***" << std::endl;
            break;
        }
    }
    else if (action_choice == 2) {
        player->printPlayerStatus();
        treeSpirit->printMonsterStatus();
    }
}

delete player;   // 가상 소멸자 덕분에 파생 클래스 소멸자까지 안전하게 호출

 

💡 전투 데미지 흐름 정리
  • 플레이어 → 몬스터: getPower() 그대로 적용 (Monster에는 방어력 없음)
  • 몬스터 → 플레이어: getAttackPower() - Defence(5) = 실질 피해 10, 최소 1 보장
  • 마도사(공격력 40)는 전사(30)보다 10 강하지만 HP는 40 적음 → 직업 선택에 실질적인 트레이드오프 발생

6. 개발 중 직면한 문제 & 해결

1. 추상 클래스인 Player를 직접 생성하려는 시도

 

문제: Player 가 순수 가상 함수를 포함한 추상 클래스이므로 new Player(nickname) 처럼 직접 생성 불가. 전직 전 상태 처리 방법 고민.

 

해결: Player* Player = nullptr 으로 초기화 후, switch 문에서 직접 선택에 따라 new Warrior() 또는 new Magician() 할당, 포인터를 베이스 타입으로 유지해 attack() 호출 시 다형성이 자동으로 적용됨.

 

 

2. 가상 소멸자 누락으로 인한 메모리 누수 가능성

 

문제: Player* player 에서  delete player 시 소멸자가 가상이 아니라면 파생 클래스 소멸자가 호출되지 않아 메모리 누수 발생 가능.

 

해결: Player.h 에 virtual ~Player() {} 를 명시해 베이스 포인터로 delete 시에도 파생 클래스 소멸자까지 안전하게 호출되도록 처리.

 


7. 오늘 배운 점 & 회고

기술적 인사이트
  • 순수 가상 함수(= 0)는 인터페이스 강제 수단 — 직업 추가 시 attack() 빠뜨리면 컴파일 에러로 즉시 감지
  • 베이스 클래스 포인터(Player*)로 파생 객체를 다루면 전투 코드가 직업에 무관하게 동일하게 동작
  • 가상 소멸자는 상속 구조가 있으면 항상 선언해야 안전함

 

핵심 교훈

"상속 구조를 설계할 때 순수 가상 함수와 가상 소멸자는 한 세트다. 추상 클래스로 만들 거라면 파생 클래스에서 반드시 구현할 메서드를 = 0으로 강제하고, 소멸자는 꼭 virtual로 선언해 메모리 누수를 예방하자."