
기본 직업 시스템을 확장하여 Player와 Monster가 서로 상호작용하는 실제 전투 시뮬레이션을 구현했다. 헤더 파일 간에 서로를 참조할 때 발생하는 순환 참조 문제를 '전방 선언'이라는 기법으로 해결하는 중요한 경험을 했다. 💪 또한, while문을 이용한 전투 루프를 설계하여 턴 기반 전투의 기본 로직을 완성했다. 직업마다 각기 다른 공격 방식으로 몬스터에게 피해를 입히는 다형성의 실제적인 활용법을 체득했다. ⚔️
while문을 이용한 전투 루프 로직 설계하기// main.cpp: 게임의 전체적인 흐름과 전투 루프를 관리합니다.
#include <iostream>
#include <limits> // cin.ignore()를 위해 필요
#include "player.h"
#include "monster.h"
// ... (warrior.h, magician.h 등 모든 직업 헤더 포함) ...
using namespace std;
int main() {
Player* player = nullptr;
// ... (이전과 동일하게 닉네임 입력 및 직업 선택) ...
// 'new' 키워드로 Monster 객체를 동적으로 생성합니다.
cout << "\n야생의 슬라임이 나타났다!\n" << endl;
Monster* slime = new Monster("슬라임", 50, 10, 5);
cout << "\n엔터를 눌러 전투를 시작하세요...";
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cin.get();
// --- 전투 루프 ---
// 플레이어와 몬스터 둘 다 살아있는 동안 계속 반복됩니다.
while (player->getHP() > 0 && slime->getHP() > 0) {
// 플레이어의 턴
cout << "\n--- 플레이어의 턴 ---" << endl;
player->attack(slime); // 플레이어가 몬스터를 공격!
if (slime->getHP() <= 0) break; // 몬스터가 죽으면 루프 탈출
// 몬스터의 턴
cout << "\n--- 몬스터의 턴 ---" << endl;
slime->attack(player); // 몬스터가 플레이어를 공격!
if (player->getHP() <= 0) break; // 플레이어가 죽으면 루프 탈출
}
// ... (전투 결과 출력 및 delete로 메모리 해제) ...
return 0;
}
// player.h: Player 클래스에 Monster와의 상호작용 기능을 추가합니다.
#pragma once
#include <string>
using namespace std;
// 전방 선언 (Forward Declaration)
// #include "monster.h" 대신 사용합니다.
// "Monster라는 클래스가 어딘가에 있을 거야"라고 컴파일러에게 미리 알려주기만 합니다.
// 이렇게 하면 Monster의 구체적인 내용은 몰라도 Monster* 타입은 사용할 수 있게 되어,
// monster.h와의 순환 참조 문제를 해결할 수 있습니다.
class Monster;
class Player {
public:
Player(string nickname);
// 이제 attack 함수는 Monster 객체의 포인터를 인자로 받습니다.
virtual void attack(Monster* monster) = 0;
// 외부에서 플레이어의 정보를 안전하게 가져가거나(getter) 설정(setter)할 함수들
int getDefence();
int getHP();
bool setHP(int HP); // HP를 설정하고 생존 여부를 bool 값으로 반환합니다.
};
// monster.h: Monster 클래스의 설계도입니다.
#pragma once
#include <string>
using namespace std;
class Player; // Player 클래스도 똑같이 전방 선언해줍니다.
class Monster {
public:
Monster(string name, int hp, int power, int defence);
// Monster의 attack 함수는 Player 객체의 포인터를 인자로 받습니다.
void attack(Player* player);
int getDefence();
int getHP();
bool setHP(int HP);
};
// archer.cpp: Archer 클래스의 다중 공격 로직을 구현합니다.
#include "archer.h"
#include "monster.h" // Monster의 구체적인 멤버 함수(getName, getDefence 등)를 사용해야 하므로 .cpp 파일에서는 반드시 #include 합니다.
#include <iostream>
void Archer::attack(Monster* monster) {
cout << "\n" << job_name << " '" << nickname << "' (이)가 " << monster->getName() << "에게 화살을 쏩니다!" << endl;
// 몬스터의 방어력을 고려하여 총 데미지를 계산합니다.
int total_damage = this->power - monster->getDefence();
if (total_damage < 1) total_damage = 1;
// 궁수는 3회 공격하므로, 1회당 데미지를 계산합니다.
int damage_per_hit = total_damage / 3;
if (damage_per_hit == 0) damage_per_hit = 1;
for (int i = 0; i < 3; i++) {
if (monster->getHP() <= 0) break; // 몬스터가 중간에 죽으면 공격 중단
cout << damage_per_hit << "의 피해를 입혔습니다! (타격 " << i + 1 << ")" << endl;
bool is_alive = monster->setHP(monster->getHP() - damage_per_hit); // 몬스터의 HP를 깎습니다.
if (!is_alive) {
cout << monster->getName() << "을(를) 물리쳤습니다!" << endl;
break;
}
}
}
player가 monster를 공격하는데, monster->getDefence() 함수가 없다는 컴파일 오류를 만났다. 다른 클래스의 정보를 가져오려면 반드시 해당 함수(getter)를 만들어줘야 한다는 캡슐화의 기본을 다시 한번 깨달았다.player.h에 #include "monster.h"를, monster.h에 #include "player.h"를 넣었더니 서로를 무한정 불러오는 순환 참조 오류가 발생했다. 헤더 파일에서는 포인터만 쓸 거라면, #include 대신 '전방 선언'을 써야 한다는 것을 배웠다. 💡attack() 함수 선언을 지우지 않았더니, 자식 클래스들이 더는 그 함수를 구현하지 않는다는 이유로 LNK2001 링커 오류가 발생했다. 안 쓰는 코드는 과감히 정리해야 한다는 교훈을 얻었다. 🧹| 개념 | 설명 | 비고 |
|---|---|---|
| 클래스 간 상호작용 | 한 객체가 다른 객체의 멤버 함수를 호출하는 등 서로 협력하여 프로그램을 구성하는 것. | player->attack(monster) |
| 전방 선언 | #include 없이 클래스의 이름만 미리 알려주는 기술. 헤더 파일의 순환 참조를 막을 때 사용한다. | class Monster; |
| Getter/Setter | 클래스의 멤버 변수를 안전하게 읽고 쓰는 데 사용하는 함수. 캡슐화를 지키는 좋은 방법이다. | getHP(), setHP() |
| 게임 루프 | 게임의 핵심 로직이 계속 반복되는 while문. 게임의 상태를 계속 갱신한다. | while(player->getHP() > 0 && ...) |
| 동적 바인딩 | 실행 시점에 실제 객체의 타입에 따라 호출될 함수가 결정되는 것. 다형성의 핵심 원리이다. | player->attack()이 Warrior의 공격, Archer의 공격으로 다르게 동작했다. |