회사에서는 네이버나 루리웹같은 노는 사이트(...)를 못들어가게 차단한 상태라 저녁에나 이곳에 들리게 되네요.
역시 시작전에는 블로그 광고 드리겠습니다.
http://zeprod.org/
이곳은 제 개인 블로그구요. 제가 실제로 코딩하면서 겪었던 어려움들 대부분 적어놨고 또 상당량 오픈해놓았으니 어려운부분이 생기셨을때 참고하시면 좋겠습니다. 이 강좌도 블로그에 병행 연재중입니다.
http://creaty.net/
이곳은 제가 개인적으로 프로젝트를 진행하는 사이트입니다. 지금은 프로모션사이트만 보이지만, 서버내부에서 막대한 공사중입니다.
앞으로는 누구나 자유롭게 노하우나 작품들을 공유하고, 디지털 창작물에 대한 지식들을 모아 제공할 예정입니다.
자 그럼 시작하겠습니다.
오늘은 지난 편 마지막에 말씀드린 대로 메모리 동적할당에 대하여 알려드리겠습니다. 게임에 관심이 많으신 분들이라면 메모리 관리가 어떻네 하는 이야기를 지나가는 이야기로라도 들어보셨을 것입니다.
보통 메모리는 우리가 변수를 선언할때 자동적으로 할당되고, 변수가 범위를 벗어나게 되면(필요가 없어지면), 알아서 다른 프로그램이 사용할 수 있도록 할당을 풀어버리게 됩니다.
하지만 동적할당의 경우에는 포인터를 이용해 직접 메모리 공간을 다루게 되어 조금은 효율적인 메모리 사용을 가능하게 하는 방법입니다.
이번에는 지난번과 다르게 게임특성에 맞춘 예제를 가지고 그에대한 설명을 붙이는 쪽으로 가겠습니다.
오늘의 예제는 몬스터 클래스를 간단하게 만들어보고, 일정시간마다 리젠이 되는 시스템을 만들어보도록 하죠.
물론 게임적인 요소를 반영하도록, 먼 옛날 머드게임처럼 텍스트 기반으로 공격 / 사냥이 가능하도록 해보겠습니다. 하는김에 경험치도 쌓아보도록 하죠.
우선 간단하게 몬스터 클래스와 경험치가 저장될 클래스를 만들어보겠습니다.
class Monster
{
int m_key;
int m_hp;
int m_exp;
public:
Monster(int key)
{
m_key = key;
m_hp = rand() % 100;
m_exp = m_hp / 2;
cout << "몬스터 " << key << "가 생성되었습니다." << endl;
}
~Monster() { cout << "몬스터 " << key << "가 파괴되었습니다." << endl; }
int attack(int damage) {
m_hp -= damage;
if (m_hp < 0) {
return m_exp;
}
else return 0;
}
}
이 클래스에서는 math 라이브러리의 rand()함수와 iostream 라이브러리의 cout이 사용되었으므로 적절히 상단에 include 명령으로 라이브러리를 추가해주셔야 할겁니다.
우선 생성자에서는 이 몬스터에 대한 판별을 할 수 있는 key를 저장하고, 체력을 보관하는 m_hp와 경험치를 적절히 계산하여 넣습니다.
(경험치에 체력의 반을 저장하고 있는데, 이것은 그냥 몬스터에 강함에 따라서 경험치가 달라질 수 있도록 임의로 정한 것입니다.)
또한 생성자와 파괴자는 표준 출력 스트림(cout)을 이용해 커맨드창에 메세지를 출력할 것입니다.
(예제인 만큼 커맨드라인 프로그램을 만들게 되었지만, 여기에 GUI 프로그래밍만 추가한다면 그럴싸하게 보이는 게임으로 둔갑시키는 것도 가능합니다.)
자 그럼 위의 클래스를 사냥했을때 데이터가 저장되는 클래스도 간단하게 만들어보겠습니다.
class User_info
{
int m_exp;
int m_level;
public:
User_info();
~User_info();
bool add_exp(int exp) { m_exp += exp; lvUp_check(); }
private:
bool lvUp_check() {
while((m_level * m_level) * 50 < m_exp) { // 필요경험치보다 경험치가 많으면?
m_exp -= (m_level * m_level) * 50; // (m_level * m_level) * 50 이만큼이 필요경험치
m_level++; // 레벨업!
cout << "레벨이 " << m_level << "로 올랐습니다!" << endl;
}
}
}
이제는 클래스 설명은 안해도 될까요? 계속해서 설명하자니 중요한 부분이 빠지는 것 같아 일단 빼고 진행하겠습니다. 궁금하시다면 덧글로 남겨주세요.
간단하게 레벨과 경험치를 넣고, add_exp 함수를 통해 경험치를 넣으면 레벨업이 되는 부분까지 작성이 된 것입니다.
이때 레벨업 체크는 private로 User_info 클래스 내부에 있는 함수만 호출이 가능하다는점 숙지하세요.
그럼 이젠 몬스터와 경험치 / 레벨을 프로그램상에 정의한 것이죠. 그럼 이것을 이용해 몬스터를 왕창 만들어보겠습니다.
기존의 방식이라면 배열을 통해 여러개를 선언하겠죠?
Monster Monster_List[100];
이런식으로 100마리를 한번에 선언하게 되는 것이죠. 하지만 이렇게 처리한다면 100마리가 모두 메모리에 생성되는 것이며,
맵에 수천마리 수백만마리씩 몬스터가 선언되는 MMORPG같은 경우에는 메모리 공간도 부족할 뿐만아니라 관리도 힘들어질 수밖에 없습니다.
그래서 새로운 방식을 사용하도록 하겠습니다.
Monster* Monster_List;
Monster_List = new Monster(10);
새로운 키워드의 등장입니다. new! 이것은 생성자를 통해 직접 클래스의 메모리 할당을 수행하고 해당 메모리의 주소를 반환합니다.
(생성자의 인자로 10이 들어가면 이것이 생성자에서 선언한대로 key로 저장되게 됩니다.)
하지만 이렇게 동적 할당한 메모리는 직접 delete 명령어로 해제를 해주어야만 다른 프로그램이 사용할 수 있게 됩니다.
delete Monster_List;
이렇게 해제를 해주지 않는다면 아무도 사용할 수 없는 메모리가 생기게 되며, 이것을 메모리 누수(Leak)라고 합니다.
(몇몇 게임 실행시 메모리 사용량이 점점늘어나다가 결국 다운 되는것이 이런 메모리 누수때문입니다. 때문에 이런 메모리 조작은 신경써서 처리해야만 합니다.)
위와 같은 방식을 이용하면 아래와 같은 응용도 좋겠죠?
Monster* Monster_List[100];
이렇게 되면 메모리 주소만 100개를 기억하게 되는 셈으로, 직접 클래스를 메모리에 올려두고 있을때보다 메모리 사용량이 줄어들게 됩니다.
그리고 만약 몬스터가 필요하게 된다면,
for (int i = 0; i < 50; i++)
Monster_List[i] = new Monster(i);
이렇게 생성하여 사용하면 되죠. 위의 예제는 0번부터 49번까지 50개의 몬스터를 생성했네요.
(생성자에 따라 0,1,2,3,4,5,.....49번의 몬스터가 생성되었다는 메세지가 도배되겠네요. ^^)
그럼 실시간 리젠을 구현하기 위해 게임 루프를 구현하겠습니다.
User_info uinfo; // 정적선언으로 인자없는 생성자를 이용해 선언된다. 경험치저장용
while(true)
{
re_generator(); // 이부분에서 몬스터 리젠을 처리해보죠.
cin >> -nput;
int target;
scanf("%d", &target);
uinfo.add_exp(Monster_List[target].attack()); // 공격해서 얻은 경험치를 자신에게 추가
}
간편한 형식 포메팅을 위해 C함수인 scanf를 도용하였습니다(...)
먼저 경험치 저장용 정보를 하나 생성하고 게임루프로 들어가면, 몬스터 리젠함수가 있고, 공격대상을 입력받으면 그것을 공격해 얻게되는 경험치를 유저정보에 추가하게 됩니다.
그럼 간단한 게임 구색은 갖춘거죠? 입력을 받아 대상을 공격하고 경험치를 쌓고 레벨업을 하고...
오늘의 핵심과제인 리젠 함수를 만들어 보도록 하겠습니다. 우선 루프를 통해 계속해서 리젠 함수는 주기적으로 호출될 것이며, 그 사이에 유저의 공격으로 몬스터가 줄어들 수 있다는 점을 상기하고 넘어가겠습니다.
Monster* Monster_List[100]; // 몬스터가 저장될 메모리 주소들
void re_generator()
{
for(int i = 0; i < 100; i++) {
if (Monster_List[i] == null) { // 만약 메모리가 비어있으면
for (int j = i; j < 100; j++) { // 뒤쪽 공간중에서
if (Monster_List[i] != null) { // 안비어있는 공간을 찾아
Monster temp; // 교체합니다.
temp = Monster_List[j];
Monster_List[j] = Monster_List[i];
Monster_List[i] = temp;
}
}
}
// 위 과정은 그냥 간단하게 배열에 빈 공간을 없애는 과정입니다.
int count = 0;
for (int i = 0; i < 100; i++) { // 배열 모든 인자를 확인하면서
if (Monster_List[i] != null) // 비어있지 않으면
count++; // 셉니다.
}
// 위 과정은 배열 인자의 총 갯수를 알아내는 과정이죠.
// 그럼 위에서 구한 총 갯수를 통해 리젠을 해볼까요?
static int key = 0; // 몬스터 고유의 키값을 나타내줄 고마운 변수입니다.
for (int i = 0; i < 100; i++) { // 모든 배열 인자에 접근하면서
if (Monster_List[i] == null && // 메모리가 비었고
count < 80) { // 몬스터가 80마리 이하라면
if (rand() % 100 < 20) { // 20%의 확률로
Monster_List[i] = new Monster(key++); // 고유의 키값을 설정하여 몬스터 생성
count++; // 새로 추가한 몬스터도 센다.
}
}
}
}
리젠 함수 내용이 좀 길죠? 그래도 한줄마다 주석을 달아놓았으니 보기 어렵진 않으실 겁니다.
예제용이라 너무 비효율적인 부분이 많지만, 조금만 생각해보시면 금방 개선해볼 수 있을 것 같으니, 한번 도전해보시는 것은 어떨까요?
개선할만한 부분은 많이 있지만, 가장 쉬운 것으로 한가지 힌트를 드리자면 매 루프마다 count를 통해 몬스터의 갯수를 세고 있는 점이죠.
이것은 프레임당 크게 유동적인 부분이 아니므로, 유저의 공격으로 죽은 몬스터의 수만 추가로 고려하면 몬스터 수가 변화할때만 변화한 양을 저장할 수 있겠죠.
그리고 또하나, 프로그램 종료를 할 수 있는 방법이 없고, 메모리 해제도 해주지 않았습니다. 이것은 어떻게 처리하면 될까요?
이것 외에도 위의 코드에는 무수한 문제들이(...) 존재합니다. 이것들이 무엇인지 찾아보는 것도 좋은 공부가 될 것입니다.
이렇게 채 100줄도 안되는 위의 간단한 코드들로 동적 메모리할당, 경험치 체크 레벨업, 몬스터 리젠 등등 다양한 잡지식을 얻을 수 있었습니다.
다음시간에는 몬스터와 유저를 만들어낸 위의 코드에 이어서, RPG의 감초 NPC를 만들어보는 것에 도전해보도록 하겠습니다.