C++: 내일배움캠프 1번 과제 - 캐릭터 상태창 구현
내일배움캠프 언리얼 과정 C++ 학습에서 1번 과제로 캐릭터의 상태창을 간단하게 구현하는 문제가 나왔다. 크게 어려운 조건은 아니지만 다음과 같은 것들을 구현해야 한다. 실시간에 기반한 게임에선 사용하기 어려울 수도 있지만, 여기서 만든 기초적인 구조 자체는 턴제 게임, 카드게임, TRPG스러운 장르에서 충분히 응용하고 활용할 수 있을 것이다.
조건
- 캐릭터 스텟 상자: 배열을 통해 HP, MP, 공격력, 방어력을 저장한다.
- 입력 검사: HP/MP 입력 후에 공격력/방어력을 입력받고, HP/MP 는 50이하로 입력할 수 없으며, 공격력과 방어력은 0 이하면 안된다.
- 포션 구현: HP/MP를 회복시키는 포션을 각각 5개씩 제공한다. 포션이 떨어지면 회복을 더 사용할 수 없다.
- 선택지 구현: 사용자가 입력한 번호에 따라 기능들을 실행한다.
- 프로그램을 종료한다.
- MP포션을 사용하고 MP 20을 회복한다.
- HP포션을 사용하고 HP 20을 회복한다.
- 공격력을 2배로 만든다.
- 방어력을 2배로 만든다.
- 현재의 스텟 정보를 출력한다.
- 레벨 업하고 포션을 각각 1개씩 추가해준다.
- 무한 루프: 종료를 선택하기 전까지 계속 사용자와 상호작용 해야한다.
구현에 오래 걸릴 것 같진 않아, 시간이 된다면 도전 기능도 해보아도 될 것 같다.
- 레벨: level변수로 레벨업에 따른 변화를 추적한다.
- 함수: 포션 충전 기능을 별도의 함수로 분리한다.
- 레벨 및 포션 추가 출력: 5번 입력시 레벨과 포션 갯수까지 출력한다.
- 확장 힘/지능등 다른 스탯도 추가한다.
스텟 구현
먼저 스텟부터 구현해보도록 하자. 관리해야 할 값이 많지 않고 간단한 프로그램이라 메인 함수 안에 변수로 넣어도 되겠지만, 클래스 구조를 사용하면 나중에 기능을 추가하고 관리하기에 용이할 것이다.
- 벡터를 사용하면 나중에 스텟 종류를 늘리기에 용이하겠으나, 기본기 연습이니 일단은 기본 배열을 사용해보도록하자.
- 추가 스텟 구현 도전과제도 지금 스텟 구조를 만들때 한번에 넣으면 좋겠다. 나는 힘, 지능, 화술을 추가할 것이다. 레벨도 스탯 클래스에 포함시키는 것이 좋겠다.
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
#include <iostream>
using namespace std;
class Status {
//0: HP, 1:MP, 2:공격력, 3:방어력, 4:힘, 5:지능, 6:화술
private:
int Stats[7];
//0: HP포션, 1: MP포션
int Potion[2];
int Level;
public:
//스탯 초기화
Status(int HP, int MP, int Attack, int Defense, int Strength, int Intelligence, int Speech){
Stats[0] = HP;
Stats[1] = MP;
Stats[2] = Attack;
Stats[3] = Defense;
Stats[4] = Strength;
Stats[5] = Intelligence;
Stats[6] = Speech;
}
//스탯 반환
int* GetStats() {
return Stats;
}
};
int main() {
Status* Player = new Status(50, 50, 30, 30, 3, 2, 4);
for (int i = 0; i < 7; i++) {
cout << Player->GetStats()[i] << endl;
}
}
배열을 이용해 클래스 안에 구현되고 잘 출력됨까지 확인 된다. 초기화 리스트로 간단하게 초기화시키고 싶었지만, 배열을 사용하고 있어서 그렇게 구현하는데는 무리가 있다. C++ 과제 std:array나 std:vector를 사용하면 되겠으나, 과제 의도와는 맞지 않는다고 판단해 배제했다. 어차피 스텟 입력을 검사해서 적용해야 하기에 뒤에서 수정할 내용이다.
스텟 입력 구현
위 생성자는 일단 잘 작동함을 확인하기 위함이였고, 이제는 사용자의 입력을 받아 스텟에 적용할 수 있도록 기능을 구현해야 한다. 사용자에게 입력을 요청하고, cin을 사용하여 받은 값을 검사 후에 적용하는 기능을 만들어보자.
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
#include <iostream>
using namespace std;
class Status {
//0: HP, 1:MP, 2:공격력, 3:방어력, 4:힘, 5:지능, 6:화술
private:
int Stats[7];
//0: HP포션, 1: MP포션
int Potion[2];
int Level;
public:
Status() = default;
~Status() = default;
void SetStatus() {
//HP, MP 입력
while (true) {
cout << "HP와 MP를 입력하세요 : ";
int HP, MP;
cin >> HP >> MP;
//입력 검사후 올바르지 않으면 반복문 다시 실행
if (HP <= 50 || MP <= 50) {
cout << "HP 혹은 MP가 50 이하입니다. 다시 입력해주세요." << endl;
continue;
}
//올바르면 할당 후 반복문 종료
else {
Stats[0] = HP;
Stats[1] = MP;
break;
}
}
//공격력/방어력 입력
while (true) {
cout << "공격력과 방어력을 입력하세요 : ";
int Atk, Dfs;
cin >> Atk >> Dfs;
//입력 검사후 올바르지 않으면 반복문 다시 실행
if (Atk <= 0 || Dfs <= 0) {
cout << "공격력 혹은 방어력이 0 이하입니다. 다시 입력해주세요." << endl;
continue;
}
//올바르면 할당 후 반복문 종료
else {
Stats[2] = Atk;
Stats[3] = Dfs;
break;
}
}
}
//스탯 반환
int* GetStats() {
return Stats;
}
};
int main() {
Status* Player = new Status();
Player->SetStatus();
int* temp = Player->GetStats();
for (int i = 0; i < 7; i++) {
cout << temp[i] << endl;
}
}
생성자를 없애고 스탯을 입력으로 대신하도록 바꿨다. 실행하면 사용자의 입력을 요청한 후 HP,MP,공격력,방어력까지 유효하지 않는 값은 잘 걸러낸 후 적용되는 것을 볼 수 있다.
리팩토링
다만 이제 기능이 많아질 예정임으로, 먼저 입출력과 실행을 담당하는 클래스인 PlayManager를 만들고, Status는 단순히 값의 저장과 수정만 담당하도록 구분할 것이다. 굳이 구분하지 않아도 구현하는데 문제는 없겠으나, 추가적인 확장성, 유지보수성, 객체지향적 관점 에서 볼때 과하게 한 클래스에 모든 것들이 몰려 있는건 좋지 않다고 볼 수 있다. 먼저 기본적인 스탯 설정 기능만 남긴 Status 클래스를 보자.
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
//Status.h
#include <iostream>
#include <string>
enum StatType { //스텟 이름 열거형
LV,
HP,
MP,
ATK,
DFS,
STR,
INT,
SPCH,
HPPT,
MPPT,
COUNT
};
//스텟 이름 전역변수
const std::string StatName[StatType::COUNT] =
{"레벨","HP", "MP", "공격력", "방어력", "힘", "지능", "화술","HP포션", "MP포션"};
class Status {
private:
int Stats[StatType::COUNT];
public:
Status() = default;
~Status() = default;
Status(int HP, int MP, int Atk, int Dfs);
int GetStat(StatType name);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Status.cpp
#include "Status.h"
using namespace std;
Status::Status(int HP, int MP, int Atk, int Dfs) {
//기본 스텟 정의
Stats[StatType::LV] = 0; //레벨
Stats[StatType::HP] = HP; //체력
Stats[StatType::MP] = MP; //마력
Stats[StatType::ATK] = Atk; //공격
Stats[StatType::DFS] = Dfs; //방어
Stats[StatType::STR] = 10; //힘
Stats[StatType::INT] = 10; //지능
Stats[StatType::SPCH] = 10; //화술
Stats[StatType::HPPT] = 5; //HP포션
Stats[StatType::MPPT] = 5; //MP포션
}
int Status::GetStat(StatType name) {//스탯 반환 함수
return(Stats[name]);
}
Status는 열거형을 기준으로 기존의 분리되있던 레벨, 포션갯수, 스탯을 하나의 배열로 합칠 것이다. 포션 갯수가 아이템이기에 엄밀히 말해 포함되기는 어색할 수 있으나, 현재는 두종류밖에 없는 포션의 관리 용이성을 위해 스탯 배열에 포함시켰고, 이는 코드를 효과적으로 관리하고 가독성을 높이는데 기여할 것이다. 코드상에서 어떤 인덱스가 어떤 스탯을 의미하는지 헤매이지 않아도 되며, 각 스텟의 이름을 하드코딩 할 필요 없이 문자열 배열에 열거형을 대입해 이름을 얻을 수 있다. 변경된 GetStat()을 포함해 열거형에 기반하는 기능들은 이제 가독성 높고 사전에 에러를 방지할 수 있는 구조를 제공한다. 이는 뒤에 나올 모든 스탯을 출력하는 함수에서도 용이하게 쓰일 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//PlayManager.h
#include <string>
#include "Status.h"
class PlayManager {
private: //Play()반복문 함수를 통해서만 Status 접근 가능
Status Stats;
//스텟 설정
void SetStatus();
//스탯 출력
void ShowStats();
public:
void Play();
};
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
//PlayManager.cpp
#include <iostream>
#include <string>
#include "Playmanager.h"
using namespace std;
void PlayManager::SetStatus() {
int HP, MP, Atk, Dfs;
//HP, MP 입력
while (true) {
cout << "HP와 MP를 입력하세요 : ";
cin >> HP >> MP;
//입력 검사후 올바르지 않으면 반복문 다시 실행
if (HP <= 50 || MP <= 50) {
cout << "HP 혹은 MP가 50 이하입니다. 다시 입력해주세요." << endl;
continue;
}
//올바르면 반복문 종료
else {
break;
}
}
//공격력/방어력 입력
while (true) {
cout << "공격력과 방어력을 입력하세요 : ";
cin >> Atk >> Dfs;
//입력 검사후 올바르지 않으면 반복문 다시 실행
if (Atk <= 0 || Dfs <= 0) {
cout << "공격력 혹은 방어력이 0 이하입니다. 다시 입력해주세요." << endl;
continue;
}
//올바르면 반복문 종료
else {
break;
}
}
//스탯 초기화
Stats = Status(HP, MP, Atk, Dfs);
cout << "환영합니다! HP/MP포션이 다섯 개씩 지급됩니다!\n"
"= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = " << endl;
}
void PlayManager::ShowStats()
{
int i = 0;
for (i; i < static_cast<int>(StatType::COUNT) - 1; i++) { //enum에 기반한 각 스탯 출력
cout << StatName[i] << ": " << Stats.GetStat(static_cast<StatType>(i)) << ", ";
}
cout << StatName[i] << ": " << Stats.GetStat(static_cast<StatType>(i)) << endl;
}
void PlayManager::Play() {
//초기 스탯 설정
SetStatus();
//스탯 출력
ShowStats();
}
}
PlayManager클래스는 기존 Status에 포함되어 있던 정보 저장과 수정 외의 기능들인 입출력과 뒤에 구현할 선택지 제어등 동작 기능들을 모을 수 있도록 분리했다. ShowStats()함수도 열거형에 기반하여 for문으로 반복출력하도록 구현하였고, 이는 StatType 열거형과 StatName 문자열 배열에서 새로운 추가나 삭제가 일어나도, 여기선 수정없이 수정된 열거형에 기반하여 작동할 것이다.
1
2
3
4
5
6
7
//main.cpp
#include "PlayManager.h"
int main() { //매니저 클래스를 통한 간접적 상호작용
PlayManager* Player = new PlayManager();
Player->Play();
}
메인 함수에서는 간단히 PlayerManager클래스를 이용해 동작을 호출하기만 하면 된다.
선택지 무한 루프
선택지를 만들 시간이다. 일단 선택지에 들어갈 기능들을 구현하도록 하자. 기능들은 스탯 값들을 직접적으로 수정해야 하기에 기능 자체는 Status클래스에 구현한다.
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
//Status.h
#include <iostream>
#include <string>
enum StatType { //스텟 이름 열거형
LV,
HP,
MP,
ATK,
DFS,
STR,
INT,
SPCH,
HPPT,
MPPT,
COUNT
};
//스텟 이름 전역변수
const std::string StatName[StatType::COUNT] =
{"레벨","HP", "MP", "공격력", "방어력", "힘", "지능", "화술","HP포션", "MP포션"};
class Status {
private:
int Stats[StatType::COUNT];
public:
Status() = default;
~Status() = default;
Status(int HP, int MP, int Atk, int Dfs);
void UseHPPotion(); //HP포션 사용
void UseMPPotion(); //MP포션 사용
void IncreaseAtk(); //공격 증가
void IncreaseDfs(); //방어 증가
void LevelUp(); //레벨업
void IncreasePotion(); //포션 지급
int GetStat(StatType name);
};
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
//Status.cpp
#include "Status.h"
Status::Status(int HP, int MP, int Atk, int Dfs) {
//기본 스텟 정의
Stats[StatType::LV] = 0; //레벨
Stats[StatType::HP] = HP; //체력
Stats[StatType::MP] = MP; //마력
Stats[StatType::ATK] = Atk; //공격
Stats[StatType::DFS] = Dfs; //방어
Stats[StatType::STR] = 10; //힘
Stats[StatType::INT] = 10; //지능
Stats[StatType::SPCH] = 10; //화술
Stats[StatType::HPPT] = 5; //HP포션
Stats[StatType::MPPT] = 5; //MP포션
}
void Status::UseHPPotion() {//HP포션 사용 함수
if (Stats[StatType::HPPT] < 1) { //포션 부족시 사용 불가
cout << "포션이 부족합니다!" << endl;
return;
}
cout << "포션을 사용해 HP를 회복합니다!" << endl;
Stats[StatType::HP] += 50;
Stats[StatType::HPPT] -= 1;
}
void Status::UseMPPotion() {//MP포션 사용 함수
if (Stats[StatType::MPPT] < 1) { //포션 부족시 사용 불가
cout << "포션이 부족합니다!" << endl;
return;
}
cout << "포션을 사용해 MP를 회복합니다!" << endl;
Stats[StatType::MP] += 50;
Stats[StatType::MPPT] -= 1;
}
void Status::IncreaseAtk() {//공격력 증가 함수
cout << "공격력 2배!" << endl;
Stats[StatType::ATK] *= 2;
}
void Status::IncreaseDfs() {//방어력 증가 함수
cout << "방어력 2배!" << endl;
Stats[StatType::DFS] = Stats[StatType::DFS] << 1;
}
void Status::LevelUp() { //레벨업 함수
cout << "레벨업! HP/MP포션이 하나씩 지급됩니다!" << endl;
Stats[StatType::LV] += 1;
IncreasePotion(); //레벨업 후 포션 지급
}
void Status::IncreasePotion() { //포션 지급 함수
Stats[StatType::HPPT] += 1;
Stats[StatType::MPPT] += 1;
}
int Status::GetStat(StatType name) {//스탯 반환 함수
return(Stats[name]);
}
요구사항대로 기능의 기초적인 기능을 만들었다.
- UseHPPotion()/UseMPPotion():
- 각각의 포션사용/스텟회복을 수행한다.
- IncreaseAtk()/IncreaseDfs()
- 공격력과 방어력을 2배로 설정한다.
- 보면 한쪽에선 복합 대입 연산자(*=)를 사용하며, 한쪽은 쉬프트 연산자(«)를 사용한다. 2의 제곱수로 곱하거나 나누는 작업에 작업에서 두 연산이 동등하게 쓰일 수 있음을 보이는 구간이다.
- LevelUp()
- 레벨을 상승시키고 각각의 포션을 충전한다.
- IncreasePotion()
- 포션을 1개씩 충전한다. LevelUp()과 후에 만들 다른 기능에서 쓰기 위해 분리했다.
이제 위 5개의 함수와 무한반복문을 연결해 메인 함수에서 일일히 하던 함수 호출을 대체하면, 선택지를 바탕으로 계속 플레이 할 수 있을 것이다.
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
//PlayManager.cpp
#include <iostream>
#include <string>
#include "Playmanager.h"
using namespace std;
void PlayManager::SetStatus() {
int HP, MP, Atk, Dfs;
//HP, MP 입력
while (true) {
cout << "HP와 MP를 입력하세요 : ";
cin >> HP >> MP;
//입력 검사후 올바르지 않으면 반복문 다시 실행
if (HP <= 50 || MP <= 50) {
cout << "HP 혹은 MP가 50 이하입니다. 다시 입력해주세요." << endl;
continue;
}
//올바르면 반복문 종료
else {
break;
}
}
//공격력/방어력 입력
while (true) {
cout << "공격력과 방어력을 입력하세요 : ";
cin >> Atk >> Dfs;
//입력 검사후 올바르지 않으면 반복문 다시 실행
if (Atk <= 0 || Dfs <= 0) {
cout << "공격력 혹은 방어력이 0 이하입니다. 다시 입력해주세요." << endl;
continue;
}
//올바르면 반복문 종료
else {
break;
}
}
//스탯 초기화
Stats = Status(HP, MP, Atk, Dfs);
cout << "환영합니다! HP/MP포션이 다섯 개씩 지급됩니다!\n"
"= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = " << endl;
}
void PlayManager::ShowStats()
{
int i = 0;
for (i; i < static_cast<int>(StatType::COUNT) - 1; i++) { //enum에 기반한 각 스탯 출력
cout << StatName[i] << ": " << Stats.GetStat(static_cast<StatType>(i)) << ", ";
}
cout << StatName[i] << ": " << Stats.GetStat(static_cast<StatType>(i)) << endl;
}
void PlayManager::Play() {
//초기 스탯 설정
SetStatus();
//무한 반복문
bool IsPlaying = true;
while (IsPlaying) {
cout << "원하는 선택지를 입력하세요!\n"
"0: 종료합니다.\n"
"1: HP포션을 사용합니다.\n"
"2: MP포션을 사용합니다.\n"
"3: 공격력을 2배로 올립니다.\n"
"4: 방어력을 2배로 올립니다.\n"
"5: 현재 정보를 봅니다.\n"
"6: 레벨업을 합니다.\n"
"=> ";
int Selection;
cin >> Selection;
switch (Selection) {
case 0: //종료
cout << "프로그램을 종료합니다! 안녕히가세요!" << endl;
IsPlaying = false;
break;
case 1: //HP포션
Stats.UseHPPotion();
break;
case 2: //MP포션
Stats.UseMPPotion();
break;
case 3: //공격력
Stats.IncreaseAtk();
break;
case 4: //방어력
Stats.IncreaseDfs();
break;
case 5: //정보
ShowStats();
break;
case 6: //레벨업
Stats.LevelUp();
break;
default: //입력오류
cout << "입력값이 잘못되었습니다!" << endl;
break;
}
cout << "= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = " << endl;
}
}
PlayManager 헤더파일에서는 수정 없이, Play()함수의 구현부에서만 간단한 초기화/출력 코드대신 무한 반복문과 입력, 그리고 그 입력에 따라 switch문으로 올바른 동작으로 연결되도록 구성했다. 실행해보면 포션사용과 스탯의 변화, 레벨업등 기능들이 잘 작동함이 확인된다.
지금 정도만 해도 요구사항에는 충분히 적합한 코드가 되었다. 스탯을 처음 설정하고, 사용자가 정해진 선택지에 따라 값들을 조절할 수 있다. 단순히 끝내기엔 아쉬운 편이라 생각한다면, 지금까지 구현한 것을 바탕으로 어떻게 더 활용할 수 있을까?
스탯 시스템 활용: 포션 거래하기
상점이나 NPC를 통해 포션을 구매해야 하는데, 레벨에 따라 가능 여부가 달라지고, 높은 공격력이면 일정 확률로 포션을 추가로 얻는 기능을 만든다고 해보자. 전에 구현한 GetStat()함수를 사용하면 스탯 값 정보를 얻어와 그에 맞는 행동을 보여줄 수 있을 것이다. 복잡하고 분업화된 게임 개발 과정이라면, 이런 이벤트나 퀘스트 또한 별도의 객체로 다룸이 적절하겠다. 기획자나 작가가 원하는 조건에 맞추어 퀘스트의 데이터를 제공했을 때, 프로그래머가 일일히 그에 대해 프로젝트에 하드코딩하는게 아니라, 데이터에 기반하여 퀘스트를 자동적으로 생성하고 동작하도록 하는게 재사용성이나 개발 비용에 있어서 효율적일테니 말이다. 다만 여기선 이 시스템을 어떻게 활용할 수 있을지 방향성과 가능성을 확인하는 차원에서 PlayManager안에 거래하는 상황을 묘사한 함수를 하나 구현해보는 것으로 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//PlayManager.h
#include <string>
#include "Status.h"
class PlayManager {
private: //Play()반복문 함수를 통해서만 Status 접근 가능
Status Stats;
//스텟 설정
void SetStatus();
//스탯 출력
void ShowStats();
public:
PlayManager() = default;
~PlayManager() = default;
//선택지 무한 반복
void Play();
//물약 거래 함수
void TradePotion();
};
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
//PlayManager.cpp
//일부 생략
void PlayManager::Play() {
//초기 스탯 설정
SetStatus();
//무한 반복문
bool IsPlaying = true;
while (IsPlaying) {
cout << "원하는 선택지를 입력하세요!\n"
"0: 종료합니다.\n"
"1: HP포션을 사용합니다.\n"
"2: MP포션을 사용합니다.\n"
"3: 공격력을 2배로 올립니다.\n"
"4: 방어력을 2배로 올립니다.\n"
"5: 현재 정보를 봅니다.\n"
"6: 레벨업을 합니다.\n"
"7: 상점에서 물약을 얻습니다.\n"
"=> ";
int Selection;
cin >> Selection;
switch (Selection) {
case 0: //종료
cout << "프로그램을 종료합니다! 안녕히가세요!" << endl;
IsPlaying = false;
break;
case 1: //HP포션
Stats.UseHPPotion();
break;
case 2: //MP포션
Stats.UseMPPotion();
break;
case 3: //공격력
Stats.IncreaseAtk();
break;
case 4: //방어력
Stats.IncreaseDfs();
break;
case 5: //정보
ShowStats();
break;
case 6: //레벨업
Stats.LevelUp();
break;
case 7: //레벨업
TradePotion();
break;
default: //입력오류
cout << "입력값이 잘못되었습니다!" << endl;
break;
}
cout << "= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = " << endl;
}
}
void PlayManager::TradePotion() {
if (Stats.GetStat(StatType::LV) < 5) { //레벨 제한
cout << "상점 주인: 5레벨은 되야지 여길 이용할 수 있어. 꼬맹이는 안돼!\n"
"상점 주인은 거래하기 싫은 듯 합니다." << endl;
return;
}
cout << "상점 주인: 물약 필요해서 왔나? 가져가시게나.\n" //포션 제공
"상점 주인은 포션 한쌍을 카운터에 올려줍니다." << endl;
Stats.IncreasePotion();
if (Stats.GetStat(StatType::ATK) > 200) { //200 이상 공격력이면 포션 추가 제공
cout << "상점 주인: 강해보이는걸 보니 듬직 하군! 이거 하나 더 챙겨가시게!\n"
"상점 주인은 맘에 들었는지 포션 한쌍을 더 챙겨줍니다." << endl;
Stats.IncreasePotion();
}
}
물약을 거래하는 TradePotion()함수를 만들어 안에서 조건문으로 포션을 얻을 수 있을 지 없을 지 제어하고 있다. 레벨 제한에 따라 너무 낮으면 상점 주인이 쫒아내는 택스트를 출력하고, 레벨이 높으면 포션을 1쌍 주게 된다. 공격력이 200보다 높으면, 상점 주인이 맘에 들어 한다며 포션을 1쌍 더 주도록 한다. 간단하게 조건을 만들어 보았는데, 스텟에 따라 랜덤으로 발생하는 이벤트나, 여러 스텟을 동시에 요구하는 이벤트까지도 필요에 따라 구현한다면 더 매력적인 예시가 될 것이다.
개선점: 포인터 남용?
위 코드들에는 작동하는 코드만 넣어놨지만, 작성 도중에는다음과 같이 PlayManager를 구성했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PlayManager {
private: //Play()반복문 함수를 통해서만 Status 접근 가능
Status* Stats;
//스텟 설정
void SetStatus();
//스탯 출력
void ShowStats();
public:
PlayManager() = default;
~PlayManager() = default;
//선택지 무한 반복
void Play();
//물약 거래 함수
void TradePotion();
};
Stats을 포인터로 선언하고, Play()로 사용자가 입력하기 시작하면 그 입력 값을 바탕으로 나중에 초기화 하는 형식으로 구현했다. 작동 자체는 객체로 선언하나 포인터로 선언하나 큰 차이는 없겠으나, 배워온 것이 후자가 더 익숙해 평소처럼 선언했는데, 0을 입력해 프로그램이 종료될 때 디버거가 강제로 멈췄다.
1
A breakpoint instruction (__debugbreak() statement or a similar call) was executed in NBC1.exe.
반복문 동작과 종료 메세지까지도 터미널에 출력된 것으로 보아 동작에 이상보다는 종료하는 과정에서 정상적이지 않은 참조 때문인 듯 했다. 검색해보니 대부분 범위를 벗어난 인덱스에 관한 얘기가 많았는데, 인덱스가 범위를 벗어나는 부분은 만들지 않은걸 여러번 확인했기에, 그것보단 객체 참조 문제인가 싶었다.
메인 함수에서는 간단한 포인터 생성이라 별 문제 없을 것 같았고, 다른 유일했던 포인터인 Playmanager에서 Status* Stats 에서 Status Stats로 일반 객체로 바꾼 후엔 다른 컴파일 에러가 나왔다.
1
2
3
4
5
6
7
main.cpp(5,39): error C2280: 'PlayManager::PlayManager(void)': attempting to reference a deleted function
PlayManager.h(20,1):
compiler has generated 'PlayManager::PlayManager' here
PlayManager.h(20,1):
'PlayManager::PlayManager(void)': function was implicitly deleted because a data member 'PlayManager::Stats' has either no appropriate default constructor or overload resolution was ambiguous
PlayManager.h(9,9):
see declaration of 'PlayManager::Stats'
처음에 무슨 에러인지 잘 이해가 안됬으나, Stats에서 기본 생성자가 없다는 내용의 에러였다. 포인터에서는 주소만 저장하기에 생성자가 바로 호출되지 않으나, 객체로 선언했을 때는 생성자가 바로 호출되기 때문에 적절한 생성자를 필요로 한다. 지금 구조에서는 Play()함수를 통해 입력을 받기 시작하면 생성하도록 만들었는데, 포인터에서 객체로 바뀌면서 바로 생성자를 호출하게 된 것이다. 그래서 기본 생성자를 Status 클래스에도 추가해주니 깔끔히 작동하게 됬다.
일단 코드 전체적으로 동적할당이 필요했던 상황은 아니였다. 굳이 쓰겠다고 한다면 보다 확실한 생성 소멸 관리를 해줄 수 없을 때 스마트포인터라는 선택지도 있었으니, 어설픈 포인터 사용이 괜히 에러를 일으키는게 아니라는걸 다시 체험한 것 같다. 동적할당은 되도록이면 필요할 때만 사용하고, 필요할 때는 스마트 포인터를 통한 생성을 고려하자.