C++: 내일배움캠프 2번 과제 - 캐릭터 전직 구현
내일배움캠프 2번 과제는 캐릭터의 전직과 전투를 구현하는 것이다. 클래스와 상속, 객체지향에 익숙해 지기 위한 과제로 만들어진 듯 하다.
조건
- 기본 클래스 정의: 순수 가상함수 Attack()을 포함하는 캐릭터 기존 클래스인 Player를 정의한다. 직업, 이름, 레벨, 체력, 공격력같은 기본적인 정보를 가진다.
- 직업 클래스: 전사, 마법사, 도적, 궁수 4가지 직업의 클래스를 자식 클래스로 생성하고 공격을 구현한다.
- 메인 로직
- 사용자에게 번호를 입력받아
- 부모타입 포인터에 직업 객체를 할당해
- 공격을 실행한다. 전반적인 구조를 잡느라 시간을 꽤 날렸던 1번 과제보다 어떤 클래스를 만들지 정해져있어 어찌보면 더 쉬울 수 있겠다. 선택적인 도전 과제로 몬스터를 추가로 구현한다.
- Monster 클래스를 구현한다.
- 이름(name), HP(10), 공격력(30), 방어력(10), 스피드(10)를 기본으로 가진다. 이름은 생성자에서 이름을 입력받는다.
- 플레이어 Player*를 공격해 몬스터 공격력- 플레이어 방어력 의 데미지를 계산해 체력에 갱신한다. (데미지 0이하면 1로 계)
- Getter/와Setter를 구현한다.
- 플레이어의 Attack(Monster* monster)를 구현한다.
- 공격후 몬스터의 생존 여부를 확인
- 생존시 HP 표시
- 사망시 축하 메세지 출력
- 공격후 몬스터의 생존 여부를 확인
메타프로그래밍 수준으로 들어가면 직업 정보같은건 클래스에 포함하지 않아도 될 것이다. 클래스에서 이미 전사인지 마법사인지 정해져있을텐데, 별도로 이름을 설정하는 것은 불필요 할 수도 있겠다. 아니면 클래스마다 참조하는 별도의 직업 데이터 에셋을 따로 둘수도 있을 것이고 말이다. 다만 이번 과제는 그렇게 까지 깊게 가지도 않고, 필수요소에 정해져 놨으니 포함하도록 하겠다. Monster 클래스도 그냥 전투 가능한 객체로서 Player 클래스와 가지고 있는 스탯 종류등을 공유하도록 Character 클래스같은 것을 만들고, 전투에 관련된 정보의 처리를 담당하는 BattleManager같은 클래스를 따로 만들어, 인터페이스처럼 다중상속 해서 Attack(Character* character)로 구현하는게 관리도 편하고 확장성도 좋기야 하겠다. 나중에 전사가 마법사를 때릴 수도 있는 것 아닌가? 다만, 과제에서는 도전 기능에선 Attack(Monster* monster)라는 특정한 함수를 구현하는걸 원하는 것 같다. 그래서 일단 Player 클래스를 만들어 4개 직업에 상속해 Attack()까지 구현하도록 하겠다. 몬스터까지 상속하면 Attack(Monster* monster)와 Attack(Player* player) 두개가 캐스팅 가능함 탓에 오버로딩에 문제가 있을 것 같다. 때문에 Monster를 별도의 클래스로 만들고, Player와 Monster에 서로를 때리는 각각의 Attack 함수를 구현해보도록 하자.
Player 클래스 구현
먼저, Player 추상 클래스를 먼저 구현해보자. Attack()함수와 위에 언급한 스탯들, 그리고 각 스텟의 Getter와 Setter를 가져야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//Player.h
#pragma once
#include<string>
enum IntInfoType {
HP, //체력
ATK, //공격력
DFS, //방어력
ACC, //정확도
SPD, //속도
INT_COUNT
};
const std::string IntInfoName[IntInfoType::INT_COUNT] = {"체력", "공격력", "방어력", "정확도", "속도" };
enum StrInfoType {
TITLE, //칭호
JOB, //직업 이름
NAME, //캐릭터 이름
STR_COUNT
};
const std::string StrInfoName[StrInfoType::STR_COUNT] = {"칭호", "직업", "이름"};
class Player {
protected:
std::string Names[StrInfoType::STR_COUNT] = {" "};
int Stats[IntInfoType::INT_COUNT] = {0};
public:
virtual ~Player() = default;
virtual void Attack() = 0; //공격 함수
//Setters
void SetTitle(std::string val) { Names[StrInfoType::TITLE] = val; }
void SetJob(std::string val) { Names[StrInfoType::JOB] = val; }
void SetName(std::string val) { Names[StrInfoType::NAME] = val; }
virtual void SetHP(int val) { if (val >= 0) Stats[IntInfoType::HP] = val; }
void SetAtk(int val) { if(val >= 0) Stats[IntInfoType::ATK] = val; }
void SetDfs(int val) { if (val >= 0) Stats[IntInfoType::DFS] = val; }
void SetAcc(int val) { if (val >= 0) Stats[IntInfoType::ACC] = val; }
void SetSpd(int val) { if (val >= 0) Stats[IntInfoType::SPD] = val; }
//Getters
const std::string& GetTitle() const { return Names[StrInfoType::TITLE]; }
const std::string& GetJob() const { return Names[StrInfoType::JOB]; }
const std::string& GetName() const { return Names[StrInfoType::NAME]; }
int GetHP() const { return Stats[IntInfoType::HP]; }
int GetAtk() const { return Stats[IntInfoType::ATK]; }
int GetDfs() const { return Stats[IntInfoType::DFS]; }
int GetAcc() const { return Stats[IntInfoType::ACC]; }
int GetSpd() const { return Stats[IntInfoType::SPD]; }
virtual void ShowAllInfo();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Player.cpp
#include<iostream>
#include "Player.h"
#include "Monster.h"
using namespace std;
void Player::ShowAllInfo() {
//문자열 정보 먼저 출력
cout << "<<";
for (int i = 0; i < StrInfoType::STR_COUNT; i++) {
cout << " " << Names[i];
}
cout << "의 정보 >>" << endl;
//정수 스탯들 출력
for (int i = 0; i < IntInfoType::INT_COUNT; i++) {
cout << "\t|=> " << IntInfoName[i] << ": " << Stats[i] << endl;
}
}
나는 직업이름, 별명, 체력등 정보를 private으로 넣어놓았고, 각각의 Getter와 Setter는 헤더에 구현해놓았다. 정수 맴버들의 Setter에는 간단히 0이상의 값인지만 확인하는 입력 검사를 넣어놨다. 열거형을 이용해 정수형 변수와 문자열 변수들을 배열로 관리하도록 해놨으며, 모든 정보를 출력하는 함수인 ShowAllInfo()에서 열거형과 반복문으로 정보를 출력할 수 있다. 그리고 지금은 필요없겠으나 나중에 파티 이름, 길드 이름, 회복력등 각종 스텟을 추가하려해도 열거형,과 Getter, Setter의 수정 정도만 필요를 요하고 이외의 코드는 건드리지 않아도 되도록 할것이다.
상속 클래스 구현
이제 이 Player클래스를 상속받는 4개 직업 클래스, 전사, 마법사, 도적, 궁수를 구현한다. 직업별로 기능의 차이는 아직 딱히 없이 공격 텍스트의 차이만 만들 것이기 때문에, 전사를 기준으로 구현하고 잘 작동하는지 확인해보자.
1
2
3
4
5
6
7
8
9
//Warrior.h
#pragma once
#include "Player.h"
class Warrior : public Player {
public:
Warrior(std::string Val);
void Attack() override;
};
1
2
3
4
5
6
7
8
9
10
11
12
//Warrior.cpp
#include <iostream>
#include "Warrior.h"
using namespace std;
Warrior::Warrior(string name) {
SetJob("전사");
SetName(name);
}
void Warrior::Attack() {
cout << "전사가 장검을 휘두릅니다!" << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//main.cpp
#include "Player.h"
#include "Warrior.h"
using namespace std;
int main() {
Player* player = new Warrior("빅토르");
player->SetTitle("강인한 자");
player->SetHP(50);
player->SetAtk(10);
player->SetDfs(7);
player->SetSpd(3);
player->SetAcc(3);
player->ShowAllInfo();
player->Attack();
delete player;
}
실행해보면 강인한 전사 빅토르의 정보와 검을 휘두른단 텍스트를 콘솔에서 볼 수 있다. 이미 Player클래스에서 Getter/Setter/ShowAllInfo까지 있어서, 각각의 클래스를 상속 후에 개별적으로 구현할 기능이 많이 없다. 때문에 당장은 설정할 이름을 매개변수로 받는 생성자와 Attack()의 구현정도만 들어갔다.
다른 클래스에도 단순히 공격 텍스트만 바꾸는 정도의 차이라, 여기 작성하는 것은 넘어가도록 하겠다. 대신 구현이 끝난 후 어떻게 다형성된 각 직업 클래스를 Player 포인터로 다룰 수 있는지 메인 함수에서의 전직 캐릭터 생성 로직을 만들어보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//main.cpp
#include <iostream>
#include "Warrior.h"
#include "Magician.h"
#include "Thief.h"
#include "Archer.h"
using namespace std;
int main() {
//사용자 입력
int selection;
cout << "환영합니다! 선택할 직업을 골라주세요!\n";
while (true) {
cout << "1.전사\n"
"2.마법사\n"
"3.도적\n"
"4.궁수\n"
"=>";
cin >> selection;
if (selection < 1 || selection > 4) {
cout << "입력이 올바르지 않습니다!다시 입력해주세요!" << endl;
continue;
}
else {
break;
}
}
cin.ignore();
string title = "";
cout << "선택되었습니다! 당신의 칭호를 알려주세요: ";
while (true) {
getline(cin, title);
if (title == "") {
cout << "칭호를 못들었어요! 다시 알려주세요: ";
continue;
}
else {
break;
}
}
string name = "";
cout << title << ", 당신의 이름은 무엇인가요?: ";
while (true) {
getline(cin, name);
if (name == "") {
cout << "이름을 못알아 듣겠군요! 다시 알려주세요: ";
continue;
}
else {
break;
}
}
//플레이어 캐릭터 생성 및 스탯 부여
Player* player;
switch (selection) {
case 1:
player = new Warrior(name);
player->SetHP(80);
player->SetAtk(17);
player->SetDfs(20);
player->SetSpd(10);
player->SetAcc(13);
break;
case 2:
player = new Magician(name);
player->SetHP(50);
player->SetAtk(18);
player->SetDfs(10);
player->SetSpd(13);
player->SetAcc(13);
break;
case 3:
player = new Thief(name);
player->SetHP(60);
player->SetAtk(16);
player->SetDfs(15);
player->SetSpd(20);
player->SetAcc(15);
break;
case 4:
player = new Archer(name);
player->SetHP(60);
player->SetAtk(16);
player->SetDfs(10);
player->SetSpd(15);
player->SetAcc(20);
break;
default:
cout << "오류를 감지했습니다! 시뮬레이션을 종료합니다!" << endl;
return 0;
}
player->SetTitle(title);
//공격 후 정보 출력
player->Attack();
player->ShowAllInfo();
delete player;
}
내 경우엔 유물수집가 도적 록산느의 비수를 꽂는다는 공격 텍스트와 캐릭터 정보를 볼 수 있었다. 함수 마지막에는 포인터를 삭제함으로서 객체를 해제하면서 끝난다. 여기까지 필수요소의 구현은 끝났다. 필요하다면, 각 캐릭터의 스텟을 메인함수 로직에서 개별적으로 입력하도록 해놓았지만, 캐릭터 내부 생성자를 통해 직업의 기본값으로 초기화하고 플레이어가 사용하는 기술, 장비, 상호작용에 따라 변하게도 구성할 수 있을것이다.
몬스터 클래스 구현
이제 몬스터 클래스를 구현해보자. Player와 기능은 겹치되, 상속받으면 Attack(Monster* monster)를 직업 클래스에서 구현할 수 없을 테니, Player의 코드만 복사해와서 상속받지 않는 개별 클래스로 구현하도록 하겠다. 이미 Player 클래스에는 몬스터가 가져야하는 이름, 공격력, 방어력등 스텟들이 들어가있다. 다만 다른점이 있다면 몬스터 클래스에선 Attack(Player* Player)를 이용해 플레이어를 공격할 수 있다는 점이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//Monster.h
#pragma once
#include <string>
#include "Player.h"
class Monster {
protected:
std::string Names[StrInfoType::STR_COUNT];
int Stats[IntInfoType::INT_COUNT];
public:
Monster(std::string Val);
virtual ~Monster() = default;
virtual void Attack(Player* player);//공격 함수
//Setters
void SetTitle(std::string val) { Names[StrInfoType::TITLE] = val; }
void SetJob(std::string val) { Names[StrInfoType::JOB] = val; }
void SetName(std::string val) { Names[StrInfoType::NAME] = val; }
virtual void SetHP(int val) { if (val >= 0) Stats[IntInfoType::HP] = val; }
void SetAtk(int val) { if (val >= 0) Stats[IntInfoType::ATK] = val; }
void SetDfs(int val) { if (val >= 0) Stats[IntInfoType::DFS] = val; }
void SetAcc(int val) { if (val >= 0) Stats[IntInfoType::ACC] = val; }
void SetSpd(int val) { if (val >= 0) Stats[IntInfoType::SPD] = val; }
//Getters
const std::string& GetTitle() const { return Names[StrInfoType::TITLE]; }
const std::string& GetJob() const { return Names[StrInfoType::JOB]; }
const std::string& GetName() const { return Names[StrInfoType::NAME]; }
int GetHP() const { return Stats[IntInfoType::HP]; }
int GetAtk() const { return Stats[IntInfoType::ATK]; }
int GetDfs() const { return Stats[IntInfoType::DFS]; }
int GetAcc() const { return Stats[IntInfoType::ACC]; }
int GetSpd() const { return Stats[IntInfoType::SPD]; }
virtual void ShowAllInfo();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//Monster.cpp
#include <iostream>
#include "Monster.h"
using namespace std;
Monster::Monster(std::string Val)
{
Names[StrInfoType::TITLE] = "느릿느릿한";
Names[StrInfoType::NAME] = Val;
Stats[IntInfoType::HP] = 10;
Stats[IntInfoType::ATK] = 30;
Stats[IntInfoType::DFS] = 10;
Stats[IntInfoType::SPD] = 10;
Stats[IntInfoType::ACC] = 5;
}
void Monster::Attack(Player* player)
{
//데미지 계산
int Damage = max(GetAtk() - player->GetDfs(), 1);
cout << GetName() <<"이(가) " << Damage << "의 피해를 가합니다!" << endl;
//남은체력 계산후 메세지 출력
int RemainingHP = max(0, player->GetHP() - Damage);
if(RemainingHP > 0) cout << "플레이어 " << player->GetName() << " 의 체력이 " << RemainingHP << " 만큼 남았습니다!" << endl;
else cout << "플레이어 " << player->GetName() << " 의 체력이 소진되었습니다!" << endl;
player->SetHP(RemainingHP);
}
void Monster::ShowAllInfo() {
//문자열 정보 먼저 출력
cout << "<<";
for (int i = 0; i < StrInfoType::STR_COUNT; i++) {
cout << " " << Names[i];
}
cout << "의 정보 >>" << endl;
//정수 스탯들 출력
for (int i = 0; i < IntInfoType::INT_COUNT; i++) {
cout << "\t|=> " << IntInfoName[i] << ": " << Stats[i] << endl;
}
}
앞서 말했듯 Monster의 대부분의 기능이 Player와 겹친다. 차이점이 있다면 생성자와 공격 함수정도이다. 생성자에서는 이름을 매개변수로 받고 “느릿느릿한” 기초적인 몬스터를 만들도록 구현했다. 그리고 Attack(Player* player)를 이용해 플레이어 캐릭터를 공격하도록 몬스터를 호출하면 느릿느릿한 몬스터는 자신과 플레이어의 스텟을 바탕으로 피해와 남은 체력을 계산해 업데이트하고 그에 맞는 텍스트를 출력할 것이다.
이제 플레이어 클래스에 몬스터와의 상호작용을 구현해보자. Attack(Monster* monster)를 Player 클래스에 순수 가상 함수로 추가해놓은 후, 자식 클래스에서 오버라이딩해 구현해햐 한다. 이번엔 궁수 클래스를 기준으로 작성해보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//Player.h
#pragma once
#include<string>
class Monster; //몬스터 클래스 전방선언(순환 관계 방지)
enum IntInfoType {
HP, //체력
ATK, //공격력
DFS, //방어력
ACC, //정확도
SPD, //속도
INT_COUNT
};
const std::string IntInfoName[IntInfoType::INT_COUNT] = {"체력", "공격력", "방어력", "정확도", "속도" };
enum StrInfoType {
TITLE, //칭호
JOB, //직업 이름
NAME, //캐릭터 이름
STR_COUNT
};
const std::string StrInfoName[StrInfoType::STR_COUNT] = {"칭호", "직업", "이름"};
class Player {
protected:
std::string Names[StrInfoType::STR_COUNT] = {" "};
int Stats[IntInfoType::INT_COUNT] = {0};
public:
virtual ~Player() = default;
virtual void Attack() = 0; //공격 함수
virtual void Attack(Monster* monster) = 0; //몬스터 공격 함수
//Setters
void SetTitle(std::string val) { Names[StrInfoType::TITLE] = val; }
void SetJob(std::string val) { Names[StrInfoType::JOB] = val; }
void SetName(std::string val) { Names[StrInfoType::NAME] = val; }
virtual void SetHP(int val) { if (val >= 1) Stats[IntInfoType::HP] = val; }
void SetAtk(int val) { if(val >= 0) Stats[IntInfoType::ATK] = val; }
void SetDfs(int val) { if (val >= 0) Stats[IntInfoType::DFS] = val; }
void SetAcc(int val) { if (val >= 0) Stats[IntInfoType::ACC] = val; }
void SetSpd(int val) { if (val >= 0) Stats[IntInfoType::SPD] = val; }
//Getters
const std::string& GetTitle() const { return Names[StrInfoType::TITLE]; }
const std::string& GetJob() const { return Names[StrInfoType::JOB]; }
const std::string& GetName() const { return Names[StrInfoType::NAME]; }
int GetHP() const { return Stats[IntInfoType::HP]; }
int GetAtk() const { return Stats[IntInfoType::ATK]; }
int GetDfs() const { return Stats[IntInfoType::DFS]; }
int GetAcc() const { return Stats[IntInfoType::ACC]; }
int GetSpd() const { return Stats[IntInfoType::SPD]; }
virtual void ShowAllInfo();
};
1
2
3
4
5
6
7
8
9
10
//Archer.h
#pragma once
#include "Player.h"
class Archer : public Player {
public:
Archer(std::string Val){SetName(Val);}
void Attack() override;
void Attack(Monster* monster) override; //몬스터 공격 함수
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Archer.cpp
#include <iostream>
#include "Archer.h"
using namespace std;
void Archer::Attack() {
cout << "궁수가 화살을 발사합니다!" << endl;
}
void Archer::Attack(Monster* monster)
{
cout << "화살을 3번 쏩니다!" << endl;
int Damage = max(GetAtk() - monster->GetDfs(), 1);
int DivDmg = max(Damage / 3, 1);
for (int i = 0; i < 3; i++) {//궁수는 1/3씩 3번
cout << GetName() << "이(가) " << DivDmg << "의 피해를 가합니다!" << endl;
int RemainingHP = max(monster->GetHP() - DivDmg, 0);
monster->SetHP(RemainingHP);
if (RemainingHP > 0) cout << monster->GetTitle() << " " << monster->GetName() << " 의 체력이 " << RemainingHP << " 만큼 남았습니다!" << endl;
else{
cout << "축하합니다! 몬스터를 쓰러뜨렸습니다!" << endl;
break;
}
}
}
1) 몬스터를 때리고, 2) 체력을 업데이트하고, 3) 처치했는지 메세지를 출력 기능까지 들어갔다. 각 직업에서는 전사와 마법사가 1번, 도적이 1/5의 피해로 5번, 궁수가 1/3만큼 3번 때린다는 것만 빼면 전체적으로 동일하게 작동한다.
이 부분은 내가 말한 전투 로직을 개별 클래스로 분리하는게 좋다고 말한 이유이기도 하다. 데미지의 산정, 데미지의 출력, 남은 체력 계산, 추가 메세지 출력이라는 기본적인 공격 과정은 플레이어와 몬스터간에 동일하다. 다를 수 있는 개별 직업의 기술 내지는 데미지 산출 공식만 함수로 만들어 두고, 외부의 전투 담당 클래스가 이를 불러 처리하는 쪽이 코드의 재사용성을 개선할 수 있다는 점이다. 지금 처치 메세지만 출력하고 처치에 따른 게임 종료 직업 클래스에는 구현하지 않았는데, 이는 게임의 룰을 담당하는 클래스에서 다룸이 적합하겠다. 이는 더욱이 전투/게임 규칙의 진행과 적용을 담당하는 클래스가 따로 있는게 그런 처치, 게임오버, 승패여부등을 판단하는데 더 도움이 될 것이란 의미도 포함한다.
구조에 대한 의견은 마무리하고, 메인 함수에서 어떻게 몬스터와 플레이어가 상호작용할지 보고 마무리하자. 반복문 을 통해 이전 과제와 유사하게 계속 선택지를 만들고 플레이어나 몬스터의 체력이 다 떨어지면 프로그램이 종료되도록 구현하도록 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//main.cpp
#include <iostream>
#include "Warrior.h"
#include "Magician.h"
#include "Thief.h"
#include "Archer.h"
using namespace std;
int main() {
//사용자 입력
int selection;
cout << "환영합니다! 선택할 직업을 골라주세요!\n";
while (true) {
cout << "1.전사\n"
"2.마법사\n"
"3.도적\n"
"4.궁수\n"
"=>";
cin >> selection;
if (selection < 1 || selection > 4) {
cout << "입력이 올바르지 않습니다!다시 입력해주세요!" << endl;
continue;
}
else {
break;
}
}
cin.ignore();
string title = "";
cout << "선택되었습니다! 당신의 칭호를 알려주세요: ";
while (true) {
getline(cin, title);
if (title == "") {
cout << "칭호를 못들었어요! 다시 알려주세요: ";
continue;
}
else {
break;
}
}
string name = "";
cout << title << ", 당신의 이름은 무엇인가요?: ";
while (true) {
getline(cin, name);
if (name == "") {
cout << "이름을 못알아 듣겠군요! 다시 알려주세요: ";
continue;
}
else {
break;
}
}
//플레이어 캐릭터 생성 및 스탯 부여
Player* player;
switch (selection) {
case 1:
player = new Warrior(name);
player->SetHP(80);
player->SetAtk(17);
player->SetDfs(20);
player->SetSpd(10);
player->SetAcc(13);
break;
case 2:
player = new Magician(name);
player->SetHP(50);
player->SetAtk(18);
player->SetDfs(10);
player->SetSpd(13);
player->SetAcc(13);
break;
case 3:
player = new Thief(name);
player->SetHP(60);
player->SetAtk(16);
player->SetDfs(15);
player->SetSpd(20);
player->SetAcc(15);
break;
case 4:
player = new Archer(name);
player->SetHP(60);
player->SetAtk(16);
player->SetDfs(10);
player->SetSpd(15);
player->SetAcc(20);
break;
default:
cout << "오류를 감지했습니다! 시뮬레이션을 종료합니다!" << endl;
return 0;
}
player->SetTitle(title);
//공격 후 정보 출력
player->Attack();
player->ShowAllInfo();
//몬스터 생성
Monster* monster = new Monster("슬라임");
cout << "몬스터가 생성되었습니다!" << endl;
monster->ShowAllInfo();
while (true) {
//플레이어의 행동
int BattleChoice = -1;
cout << "====================\n"
"선택지를 고르세요!\n"
"1. 공격한다!\n"
"2. 가만히 있는다!\n"
"=>";
cin >> BattleChoice;
if (BattleChoice < 0 || BattleChoice > 2) {
cout << "선택지가 올바르지 않습니다!\n";
continue;
}
switch (BattleChoice) {
case 1:
cout << "공격을 실행합니다!" << endl;
player->Attack(monster);
break;
case 2:
cout << "가만히 있습니다..." << endl;
break;
default:
cout << "선택지에 오류가 있습니다! 프로그램을 종료합니다!" << endl;
return 0;
}
if (monster->GetHP() <= 0) break;
//몬스터의 공격
cout << "몬스터가 공격해옵니다!" << endl;
monster->Attack(player);
if (player->GetHP() <= 0) break;
}
//종료 메세지 출력
cout << "====================\n"
"전투가 끝났습니다!\n"
"====================" << endl;
delete monster;
delete player;
}
마지막 부분에 무한 반복문을 이용해 선택지를 계속 진행하고, 끝나면 각 포인터를 삭제하면서 종료한다. 체력이 크지 않기에 몬스터는 2번정도 공격하면 끝나고, 플레이어의 체력이 다한 경우에도 반복문이 종료되며 프로그램이 끝난다. 가만히 있는 2번 선택지를 골라 플레이어의 패배를 경험할 수도 있게 해놓았다.
이정도면 요구사항은 충분히 만족했다고 본다. 제시한 구조에 대한 조건이 있기 때문에 굳이 더 복잡하게 구현한 것 같다는 생각이 들기도 하는데, 일단은 이정도로 마무리 하자.
개선점: 함수 반환형에서의 const 사용, 헤더파일 순환참조, HP 버그
더 전투와 스텟도 클래스를 나누면 더 객체지향적인 구조가 될 것이란 말은 위에서 많이 했으니 생략하고 평소 코딩하던 습관에서에 개선점, 새로이 배운점에 대해 적어보도록 하겠다. 먼저 함수에서의 const 사용이다.
Player함수를 구현할 때 처음 getter들을 만드는데 있어서 시도할 때는 처음에는 단순히 값을 복사하는 것으로 구현했다. 원시 자료형, 그러니까 int, float, char처럼, 클래스에서 별도로 정의된 게 아닌 c언어부터 제공되는 기초적인 자료형들은 복사로 구현하는게 직관적이고 올바르다. 이 값들은 구조가 복잡하거나 사이즈가 크지 않아서 복사로 전달하더라도 비용이 크지 않다. 굳이 함수에서의 변경이 변수에 반영되어야 하는게 아니라면 그대로 가도 좋다.
다만 사용자가 정의한 구조체나 클래스, 원시자료형이 아닌 타입들은 복사보단 const &를 이용해 전달하는 편이 더 좋다. 복사하기 위한 메모리 공간을 아낄 수 있고, const를 이용해 수정까지 방지되니 복사를 통한 전달과 유사한 기능을 내면서도 연산 비용을 아낄 수 있다. 완성한 Player 클래스 예시에서 보면 그래서 string은 const & 를 이용해 전달하고 있고, int는 별도의 상수 키워드 없이 전달하고 있다. 다만 const에 대해 햇갈리면 안되는게, 함수 앞에 붙는 const와 뒤에 붙는 const의 의미가 다르다.
1
2
3
4
class temp{
public:
const string& Func() const {}
};
위 함수에서 함수 앞에 오는 const는 반환 자료형에 관한 수식이다. 즉 const string&의 자료형으로 반환하겠다는 표현이다. 그에 비해 함수 뒤에 온 const는 뒤에 올 스코프에 대한 수식이라 할 수 있겠다. 스코프 안에서 1) 맴버 변수의 수정이 일어날 수 없으며, 2)때문에 const 객체로 외부에서 참조되었을 때 변화가 없음이 보장되기에 이 함수 또한 사용 가능하다. 같은 const이지만 위치에 따라 기능이 다른 것이다.
const에 이어 하나 더 중간에 실수했던게 있다면 헤더파일을 순환참조하는 부분이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//A.h
#pragma once
#include "B.h"
class A{
private:
B* b;
}
//B.h
#pragma once
#include "A.h"
class B{
private:
A* a;
}
위 예시에서 보노라면 A.h파일에선 A클래스를 정의하기 위해 B.h를 포함하고 있고, B에서도 반대로 A를 포함하고 있다. 이는 순환참조관계를 만들어 컴파일 에러를 일으킨다. 헤더파일에 대해 배울때 가르치는 중복 포함 문제와 같은 영역의 문제이다. 해법은 간단히 Player.h 에서 Monster 클래스를 전방선언한 후 헤더파일을 제거한 것처럼 한쪽에서 참조를 끊으면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//A.h
#pragma once
#include "B.h"
class A{
private:
B* b;
}
//B.h
#pragma once
class A; //전방선언!
class B{
private:
A* a; //만약 포인터가 아닌 객체 선언이였으면 전방선언으로도 불가!
}
전방선언함으로서 헤더파일을 포함 안해도 되고, 그렇기에 순환 문제가 사라져 에러는 나지 않는다. 하지만 권장되진 않지만 헤더파일 안에서 B에서 a의 맴버를 바로 호출함을 구현하려 한다면 에러를 발생시킬 것이다. 전방선언만 했지, B입장에선 A 안에 어떤 맴버가 있는지 모르기 때문이다. 그렇기에 구현은 소스파일로 분리하고, 헤더파일간의 참조를 피해 순환을 막고 계층적 구조를 만들고자 하는 것이 헤더파일 사용의 의도라 할 수 있겠다.
다만, 클래스가 서로를 알고있어야 하는 구조를 되도록이면 안 만들면 좋지 않겠는가? 경우에 따라서는 서로를 참조해야 하는데 오히려 공통적인 기능이 많다면 아예 한 부모 클래스로 통일시키는 것이 나을것이다. Monster와 Player를 하나의 클래스에서 파생시키는 것에 대해 말했던 것처럼 말이다. A와 B를 하나의 클래스에서 파생시켜 부모 클래스를 넣어놓을 수 있다면 더 깔끔한 구조가 될 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Parent.h
#pragma once
class Parent{
}
//A.h
#pragma once
#include "Parent.h"
class A: Parent{
private:
Parent b;
}
//B.h
#pragma once
#include "Parent.h"
class B: Parent{
private:
Parent a;
}
그리고 중간에 경험한 다른 실수로으로는 전투 구현중 HP 업데이트 문제가 있다.
1
virtual void SetHP(int val) { if (val >= 1) Stats[IntInfoType::HP] = val; }
원래 처음에는 생성되면서 HP가 0으로 설정되면 의도와 다르다고 판단했기에 1이상인걸 검사하는 조건을 넣어놨었다. 그 자체는 문제가 되지 않을 수 있겠으나, HP가 0으로 내려가야 하는 걸 구현하게 되면 말이 다르다. 바로 전투를 구현할 때이다. 마지막 반복문을 이용한 전투에서 캐릭터나 몬스터의 체력이 0이하인지 검사하는 코드가 작동을 안했었다. 반복문 자체에는 문제가 없다고 생각해 계속 확인한 결과 초기에 설정한 HP를 1이상으로 설정하는 조건이 문제였고, 수정 후 의도대로 전투와 종료까지 작동했다. 무심코 넣은 작은 부분이 큰 흐름에 영향을 줄 수 있음을 보이는 예시라 할 수 있겠다.