객체지향프로그래밍(10) - 객체지향 스타일로 포켓몬 다시 만들어보기

지금까지 배운 객체지향을 통해 포켓몬 다시 만들어보기

Featured image

🔚 짧게 하는 복습

✅ 1. const가 무엇인지 알고, 어떻게 사용하는지 안다.

✅ 2. static이 무엇인지 알고, 어떻게 사용하는지 안다.

✅ 3. 범위 지정자 ::이 무엇인지 알고, 어떻게 사용하는지 안다.

✅ 4. 이름 공간과 using namespace를 이해한다.

혹시 기억이 안 난다면, 다시 돌아가자


우리가 제일 처음 객체지향 프로그래밍을 도입했던 이유는, 실제 세계에서 우리의 사고방식과 비슷하게 코딩하기 위해서였다.

지금까지는 객체지향 프로그래밍에서 가장 작은 단위인 클래스와 객체를 만드는 방법만 배웠다.

이번 강의는 아예 한 개의 강의를 이용해서, 직접 객체지향으로 프로그램을 짜보자.


돌아온 포켓몬

우리는 객체지향프로그래밍(2)-객체지향프로그래밍(oop)란 강의에서 이미 포켓몬을 만드는 것을 다룬 적이 있다.

이번에는 아예 객체지향 스타일로 다시 작성해볼 것인데, 아래의 규칙과 같다. (규칙을 조금 수정함)

3마리의 포켓몬은 각각 물, 불, 풀의 상성을 가진다.

물 -> 불, 불 -> 풀, 풀-> 물 순으로 서로에게 유리한 상성을 가진다.

또한 유리한 상성이 공격할 때는 1.2배의 데미지가 들어가고 불리한 상성이 공격할 때는 0.8배의 데미지만 들어간다.

포켓몬은 각각 이름, 체력, 공격력, 방어력, 이동속도를 가진다.

기본적으로 한쪽 포켓몬의 체력이 0 이하로 떨어질 때까지 턴제로 진행된다.

처음 공격은 이동속도가 높은 쪽이 선공권을 가지게 된다.

포켓몬은 각각 스킬을 가지며, 스킬은 위력과 명중률을 가진다.

자신의 순서가 되면 포켓몬은 스킬을 사용할 수 있다.

스킬을 사용 시 위력 X 공격력만큼의 데미지를 줄 수 있다.

하지만 명중률만큼의 확률을 가지고, 공격이 빗나갈 수도 있다.

공격을 받는 포켓몬은 실제로 입는 데미지는 데미지/방어력을 계산한 후, 총 받을 데미지가 1 미만이라면 데미지는 1이 된다.


OOP가 아닌 OOD가 먼저!

OOP(Object Oriented Programming)객체지향 스타일로 프로그래밍을 하는 것이고, OOD(Object Oriented Design)객체지향 스타일로 설계를 하는 것이다.

우선은 어떤 요소가 우리의 프로그램에 필요한지 생각해보자.

단순하게 생각했을 때 포켓몬 클래스가 무조건 필요하다. (OOD는 단순하고 직관적일수록 좋다)

포켓몬 클래스의 변수는 이름, 체력, 공격력, 방어력, 이동속도, 주 메소드는 공격 입기, 받기 두 개이다.

디폴트로 생성 가능한 게 좋다 보니, 브케인/리아코/치코리타를 3개의 다른 클래스로 만들자. (전역 변수를 통한 참조를 줄일 수 있다.)

이것 말고 뭐가 있겠냐고 생각할 텐데, 여기서 진행되는 게임 자체도 하나의 객체가 될 수 있다.

무슨 말이냐면, 게임이라는 것도 , 난수라는 변수를 가지고 순서마다 실행, 난수 추출이라는 메소드를 가지는 객체로 볼 수 있다는 것이다.

이해가 안 된다면, 단순하고 직관적으로 생각해보자.

확실히 상성을 비교하거나, 난수를 뽑는 것은 포켓몬 객체 자체가 가지는 것이 더 이상하다.

그래서 우리는 총 4개의 클래스를 이용해서 다시 프로그램을 짜보자.


실습

코드의 길이가 길어져서, 중간 중간 주석으로 처리하고 나중에 한 번에 설명하는 것으로 하겠다.

#include <iostream>
#include <random>

using namespace std;

enum { Fire, Water, Grass }; // type enum화

struct skill_info {
	int power; // 위력
	int accuracy; // 정확도
}; // 스킬 정보

class bukein {
private:
	// bukein 클래스의 private 변수
	string name;
	int type;
	int hp;
	int attack;
	int defend;
	int mobility;
	struct skill_info skill;

public:
	bukein() : name("브케인"), type(Fire), hp(450), attack(45), defend(10), mobility(20), skill({ 8, 50 }) {}
	// 디폴트 생성자

	bukein(string _name) : name(_name), type(Fire), hp(450), attack(45), defend(10), mobility(20), skill({ 8, 50 }) {}
	// 1변수 오버로딩 생성자

	bukein(const bukein& _target) : name(_target.name), type(_target.type), hp(_target.hp), attack(_target.attack),
		defend(_target.defend), mobility(_target.mobility), skill(_target.skill) {
		cout << name << "이(가) 복사되었다" << endl;
	}
	// 복사 생성자

	~bukein() { cout << name << "은(는) 쓰러졌다" << endl;}
	// 소멸자

	void info() const{
		cout << name << "의 hp : " << hp << endl;
	}// 현재의 포켓몬 hp 정보를 보여준다

	string get_name() const{
		return name;
	}

	int get_type() const{
		return type;
	}// 타입 값 반환

	int get_hp() const{
		return hp;
	}

	int get_defend() const{
		return defend;
	}// 방어력 반환

	int get_mobility() const{
		return mobility;
	}// 이동 속도 반환

	int get_accuracy() const{
		return skill.accuracy;
	}// 정확도 값 반환

	int get_skill_damage() const{
		return skill.power * attack;
	}// 스킬 데미지 반환


	void recv_attack(const int damage) {
		hp -= damage;
	}// 데미지 입으면 체력 손실
};

class riako {
private:
	// riako 클래스의 private 변수
	std::string name;
	int type;
	int hp;
	int attack;
	int defend;
	int mobility;
	struct skill_info skill;

public:
	riako() : name("리아코"), type(Water), hp(500), attack(35), defend(20), mobility(10), skill({ 5, 55 }) {}
	// 디폴트 생성자

	riako(std::string _name) : name(_name), type(Water), hp(500), attack(35), defend(20), mobility(10), skill({ 5, 55 }) {}
	// 1변수 오버로딩 생성자

	riako(const riako& _target) : name(_target.name), type(_target.type), hp(_target.hp), attack(_target.attack),
		defend(_target.defend), mobility(_target.mobility), skill(_target.skill) {
		cout << name << "이(가) 복사되었다" << endl;
	}
	// 복사 생성자

	~riako() {cout << name << "은(는) 쓰러졌다" << endl;}
	// 소멸자

	void info() const{
		cout << name << "의 hp : " << hp << endl;
	}// 현재의 포켓몬 hp 정보를 보여준다

	string get_name() const{
		return name;
	}

	int get_type() const{
		return type;
	}// 타입 값 반환

	int get_hp() const{
		return hp;
	}

	int get_defend() const{
		return defend;
	}// 방어력 반환

	int get_mobility() const{
		return mobility;
	}// 이동 속도 반환

	int get_accuracy() const{
		return skill.accuracy;
	}// 정확도 값 반환

	int get_skill_damage() const{
		return skill.power * attack;
	}// 스킬 데미지 반환

	void recv_attack(const int damage) {
		hp -= damage;
	}// 데미지 입으면 체력 손실

}; //riako 클래스

class chicorita {
private:
	// chicorita 클래스의 private 변수
	std::string name;
	int type;
	int hp;
	int attack;
	int defend;
	int mobility;
	struct skill_info skill;

public:
	chicorita() : name("치코리타"), type(Grass), hp(550), attack(40), defend(30), mobility(0), skill({ 5, 60 }) {}
	// 디폴트 생성자

	chicorita(string _name) : name(_name), type(Grass), hp(550), attack(40), defend(30), mobility(0), skill({ 5, 60 }) {}
	// 오버로딩 생성자

	chicorita(const chicorita& _target) : name(_target.name), type(_target.type), hp(_target.hp), attack(_target.attack),
		defend(_target.defend), mobility(_target.mobility), skill(_target.skill) {
		cout << name << "이(가) 복사되었다" << endl;
	}
	// 복사 생성자

	~chicorita() { cout << name << "은(는) 쓰러졌다" << endl; }
	// 소멸자


	void info() const{
		cout << name << "의 hp : " << hp << endl;
	}// 현재의 포켓몬 hp 정보를 보여준다

	string get_name() const{
		return name;
	}

	int get_type() const{
		return type;
	}// 타입 값 반환

	int get_hp() const{
		return hp;
	}

	int get_defend() const{
		return defend;
	}// 방어력 반환

	int get_mobility() const{
		return mobility;
	}// 이동 속도 반환

	int get_accuracy() const{
		return skill.accuracy;
	}// 정확도 값 반환

	int get_skill_damage() const{
		return skill.power * attack;
	}// 스킬 데미지 반환


	void recv_attack(const int damage) {
		hp -= damage;
	}// 데미지 입으면 체력 손실

}; //chicorita 클래스

class game {
	int random_number; //난수
	int turn; // 턴

public:
	void decide_turn(const int mobility1, const int mobility2) {
		if (mobility1 > mobility2) {
			turn = 0;
		}
		else {
			turn = 1;
		}
	}; // 짝수번이 왼쪽 포켓몬 턴, 홀수 번이 오른쪽 포켓몬 턴

	int compare_type(const int type, const int enemy_type){
		if (type == enemy_type) return 1; // 무상성

		if ((type == Fire && enemy_type == Water) || (type == Water && enemy_type == Grass) || (type == Grass && enemy_type == Fire)) {
			return 0.8; // 불리한 상성
		}
		else if ((type == Fire && enemy_type == Grass) || (type == Grass && enemy_type == Water) || (type == Water && enemy_type == Fire)) {
			return 1.2; // 유리한 상성
		}
	}// 상성 비교 후 유리한 상성이면 1.2, 불리한 상성이면 0.8, 무상성이면 1 반환

	bool is_hit(int accuracy) {
		srand(time(NULL));  // 시드 설정

		// 0부터 100까지의 범위에서 난수 생성
		random_number = rand() % (101);

		return (random_number <= accuracy);
	} // 공격이 명중하는지 안하는지 반환

	int do_turn(const bukein* mypomon, riako* enem_pomon) {
		if (!is_hit(mypomon->get_accuracy())) {
			cout << mypomon->get_name() << "의 공격은 빗나갔다!" << endl;
		}
		else{
			int damage = mypomon->get_skill_damage() * compare_type(mypomon->get_type(), enem_pomon->get_type()); // 데미지 x 상성
			int real_damage = damage / enem_pomon->get_defend(); // 방어력으로 데미지 경감

			if (real_damage == 0) real_damage = 1; // real_damage가 0보다 작으면 1로 고정
			cout << mypomon->get_name() << "은(는) " << real_damage << "만큼 공격했다!" << endl;

			enem_pomon->recv_attack(real_damage); // 공격 받고 끝

			if (enem_pomon->get_hp() <= 0) {
				delete enem_pomon;
				return -1;
			}
		}
		return 0;
	}

	int do_turn(const bukein* mypomon, chicorita* enem_pomon) {
		if (!is_hit(mypomon->get_accuracy())) {
			cout << mypomon->get_name() << "의 공격은 빗나갔다!" << endl;
		}
		else {
			int damage = mypomon->get_skill_damage() * compare_type(mypomon->get_type(), enem_pomon->get_type()); // 데미지 x 상성
			int real_damage = damage / enem_pomon->get_defend(); // 방어력으로 데미지 경감

			if (real_damage == 0) real_damage = 1; // real_damage가 0보다 작으면 1로 고정
			cout << mypomon->get_name() << "은(는) " << real_damage << "만큼 공격했다!" << endl;

			enem_pomon->recv_attack(real_damage); // 공격 받고 끝

			if (enem_pomon->get_hp() <= 0) {
				delete enem_pomon;
				return -1;
			}
		}
		return 0;
	}

	int do_turn(const riako* mypomon, bukein* enem_pomon) {
		if (!is_hit(mypomon->get_accuracy())) {
			cout << mypomon->get_name() << "의 공격은 빗나갔다!" << endl;
		}
		else {
			int damage = mypomon->get_skill_damage() * compare_type(mypomon->get_type(), enem_pomon->get_type()); // 데미지 x 상성
			int real_damage = damage / enem_pomon->get_defend(); // 방어력으로 데미지 경감

			if (real_damage == 0) real_damage = 1; // real_damage가 0보다 작으면 1로 고정
			cout << mypomon->get_name() << "은(는) " << real_damage << "만큼 공격했다!" << endl;

			enem_pomon->recv_attack(real_damage); // 공격 받고 끝

			if (enem_pomon->get_hp() <=  0) {
				delete enem_pomon;
				return -1;
			}
		}
		return 0;
	}

	int do_turn(const riako* mypomon, chicorita* enem_pomon) {
		if (!is_hit(mypomon->get_accuracy())) {
			cout << mypomon->get_name() << "의 공격은 빗나갔다!" << endl;
		}
		else {
			int damage = mypomon->get_skill_damage() * compare_type(mypomon->get_type(), enem_pomon->get_type()); // 데미지 x 상성
			int real_damage = damage / enem_pomon->get_defend(); // 방어력으로 데미지 경감

			if (real_damage == 0) real_damage = 1; // real_damage가 0보다 작으면 1로 고정
			cout << mypomon->get_name() << "은(는) " << real_damage << "만큼 공격했다!" << endl;

			enem_pomon->recv_attack(real_damage); // 공격 받고 끝

			if (enem_pomon->get_hp() <= 0) {
				delete enem_pomon;
				return -1;
			}
		}
		return 0;
	}

	int do_turn(const chicorita* mypomon, bukein* enem_pomon) {
		if (!is_hit(mypomon->get_accuracy())) {
			cout << mypomon->get_name() << "의 공격은 빗나갔다!" << endl;
		}
		else {
			int damage = mypomon->get_skill_damage() * compare_type(mypomon->get_type(), enem_pomon->get_type()); // 데미지 x 상성
			int real_damage = damage / enem_pomon->get_defend(); // 방어력으로 데미지 경감

			if (real_damage == 0) real_damage = 1; // real_damage가 0보다 작으면 1로 고정
			cout << mypomon->get_name() << "은(는) " << real_damage << "만큼 공격했다!" << endl;

			enem_pomon->recv_attack(real_damage); // 공격 받고 끝

			if (enem_pomon->get_hp() <= 0) {
				delete enem_pomon;
				return -1;
			}
		}
		return 0;
	}


	int do_turn(const chicorita* mypomon, riako* enem_pomon) {
		if (!is_hit(mypomon->get_accuracy())) {
			cout << mypomon->get_name() << "의 공격은 빗나갔다!" << endl;
		}
		else {
			int damage = mypomon->get_skill_damage() * compare_type(mypomon->get_type(), enem_pomon->get_type()); // 데미지 x 상성
			int real_damage = damage / enem_pomon->get_defend(); // 방어력으로 데미지 경감

			if (real_damage == 0) real_damage = 1; // real_damage가 0보다 작으면 1로 고정
			cout << mypomon->get_name() << "은(는) " << real_damage << "만큼 공격했다!" << endl;

			enem_pomon->recv_attack(real_damage); // 공격 받고 끝

			if (enem_pomon->get_hp() <= 0) {
				delete enem_pomon;
				return -1;
			}
		}
		return 0;
	}

	// 속성별 오버로딩

	void do_game() {
		int pomon_choice, enem_choice;

		cout << "내 포켓몬을 고르세요 (0. 브케인, 1. 리아코, 2. 치코리타) : "<<endl;
		cin >> pomon_choice;

		cout << "상대 포켓몬을 고르세요 (0. 브케인, 1. 리아코, 2. 치코리타) (중복제외!) :" << endl;
		cin >> enem_choice;

		if (pomon_choice == 0 && enem_choice == 1) {
			bukein* mypomon = new bukein();
			riako* enem_pomon = new riako();

			decide_turn(mypomon->get_mobility(), enem_pomon->get_mobility());

			while (1) {
				cout << mypomon->get_name() << " : " << mypomon->get_hp() << "   " << enem_pomon->get_name() << " : " << enem_pomon->get_hp() << endl;

				if (turn % 2 == 0) {
					if (do_turn(mypomon, enem_pomon)) break; // -1 반환이면 게임 종료
				}
				else {
					if (do_turn(enem_pomon, mypomon)) break; // -1 반환이면 게임 종료
				}
				turn += 1;
			}
		}
		else if (pomon_choice == 0 && enem_choice == 2) {
			bukein* mypomon = new bukein();
			chicorita* enem_pomon = new chicorita();

			decide_turn(mypomon->get_mobility(), enem_pomon->get_mobility());

			while (1) {
				cout << mypomon->get_name() << " : " << mypomon->get_hp() << "   " << enem_pomon->get_name() << " : " << enem_pomon->get_hp() << endl;

				if (turn % 2 == 0) {
					if (do_turn(mypomon, enem_pomon)) break; // -1 반환이면 게임 종료
				}
				else {
					if (do_turn(enem_pomon, mypomon)) break; // -1 반환이면 게임 종료
				}
				turn += 1;
			}
		}
		else if (pomon_choice == 1 && enem_choice == 0) {
			riako* mypomon = new riako();
			bukein* enem_pomon = new bukein();

			decide_turn(mypomon->get_mobility(), enem_pomon->get_mobility());

			while (1) {
				cout << mypomon->get_name() << " : " << mypomon->get_hp() << "   " << enem_pomon->get_name() << " : " << enem_pomon->get_hp() << endl;

				if (turn % 2 == 0) {
					if (do_turn(mypomon, enem_pomon)) break; // -1 반환이면 게임 종료
				}
				else {
					if (do_turn(enem_pomon, mypomon)) break; // -1 반환이면 게임 종료
				}
				turn += 1;
			}
		}
		else if (pomon_choice == 1 && enem_choice == 2) {
			riako* mypomon = new riako();
			chicorita* enem_pomon = new chicorita();

			decide_turn(mypomon->get_mobility(), enem_pomon->get_mobility());

			while (1) {
				cout << mypomon->get_name() << " : " << mypomon->get_hp() << "   " << enem_pomon->get_name() << " : " << enem_pomon->get_hp() << endl;

				if (turn % 2 == 0) {
					if (do_turn(mypomon, enem_pomon)) break; // -1 반환이면 게임 종료
				}
				else {
					if (do_turn(enem_pomon, mypomon)) break; // -1 반환이면 게임 종료
				}
				turn += 1;
			}
		}
		else if (pomon_choice == 2 && enem_choice == 0) {
			chicorita* mypomon = new chicorita();
			bukein* enem_pomon = new bukein();

			decide_turn(mypomon->get_mobility(), enem_pomon->get_mobility());

			while (1) {
				cout << mypomon->get_name() << " : " << mypomon->get_hp() << "   " << enem_pomon->get_name() << " : " << enem_pomon->get_hp() << endl;

				if (turn % 2 == 0) {
					if (do_turn(mypomon, enem_pomon)) break; // -1 반환이면 게임 종료
				}
				else {
					if (do_turn(enem_pomon, mypomon)) break; // -1 반환이면 게임 종료
				}
				turn += 1;
			}
		}
		else if (pomon_choice == 2 && enem_choice == 1) {
			chicorita* mypomon = new chicorita();
			riako* enem_pomon = new riako();

			decide_turn(mypomon->get_mobility(), enem_pomon->get_mobility());

			while (1) {
				cout << mypomon->get_name() << " : " << mypomon->get_hp() << "   " << enem_pomon->get_name() << " : " << enem_pomon->get_hp() << endl;

				if (turn % 2 == 0) {
					if (do_turn(mypomon, enem_pomon)) break; // -1 반환이면 게임 종료
				}
				else {
					if (do_turn(enem_pomon, mypomon)) break; // -1 반환이면 게임 종료
				}
				turn += 1;
			}
		}
		else {
			cout << "You chose two same pokemon" << endl;
		}
	}
};

int main() {
	game new_game;

	new_game.do_game();
}

어떤가? 코드가 훨씬 직관적으로 바뀌었다.

예를 들어, int main 안에는 new_game이라는 객체가 오직 do_game()만 실행한다.

그러니 ‘새 게임이 만들어지고 진행되는구나…!’라는 사실을 알 수 있다.

그럼 안에 내부 코드를 살펴보자.

포켓몬 클래스는 생성자, 오버로딩 생성자, 복사 생성자, 소멸자 등이 있고, 주된 메소드는 역시 공격받기, 주기이다.

그런데 여기서 받기와 주기는 다른 어떤 것도 알 필요가 없다.

단순하게 주는 데미지 만큼 반환하고, 들어온 데미지만큼 hp를 줄이면 된다.

객체지향의 큰 장점은 이렇게, 단순하고 직관적이라는 점이다.

그 메소드의 구현 과정은 알 필요도 없다. 그냥 만들어진 메소드를 쓰면 될 뿐이다.

턴제로 진행되는 과정 또한 같다.

복잡해 보이지만, 사실 작은 메소드 단위로 만들어놓은 것을 이용했을 뿐이고, 메소드나 변수의 이름만 봐도 직관적으로 이해가 된다.


결과

 포켓몬을 고르세요 (0. 브케인, 1. 리아코, 2. 치코리타) :
0
상대 포켓몬을 고르세요 (0. 브케인, 1. 리아코, 2. 치코리타) (중복제외!) :
2
브케인 : 450   치코리타 : 550
브케인은() 12만큼 공격했다!
브케인 : 450   치코리타 : 538
치코리타은() 1만큼 공격했다!

// 중략

브케인 : 406   치코리타 : 10
치코리타은() 1만큼 공격했다!
브케인 : 405   치코리타 : 10
브케인은() 12만큼 공격했다!
치코리타은() 쓰러졌다

(게임의 밸런스는 일단 무시하자….)

우선 여기까지 따라왔다면 정말 정말 고생이 많았다고 해주고 싶다.

끝까지 작성했든, 못했든 수고가 많았다.

C, 자료구조 강의부터 지금까지 여러분이 기억하는 모든 기술을 총동원해서 작성했을 것이다.

이 경험은 엄청난 도움이 될 것이라고 장담할 수 있다.

이제는 다음 강의로 나가기 위해 피드백을 해보자.


개선할 점

코드를 작성하면서 가장 많이 화가 나는 점은, 어떻게 코드를 짜도 중복이 너무 많다는 점이다.

치코리타, 브케인, 리아코는 사실상 디폴트 값만 다른 객체인데, 거의 똑같은 코드를 중복해서 3개의 클래스를 만들어야 했다.

두 번째는 그렇게 억지로 만들어진 서로 다른 클래스 때문에, 한 번의 턴을 진행하는 do_turn 함수를 6개나 오버로딩을 해야 했다.

지금까지 우리가 배운 능력으로는 이 이상은 어렵기 때문이다.

우리는 앞으로 클래스 간의 관계, 클래스 간 효율적인 코드를 작성하는 방법에 대해 배워나갈 것이다.

이번 강의는 정말 수고 많았다.


✅ 1. OOD(Object Oriented Design)을 통해서 OOP(Object Oriented Programming)를 구현할 수 있다.

✅ 2. 객체지향 프로그래밍의 장점을 이해한다.