이 글을 보는 여러분들, 그간 격조하셨는지 모르겠다. 나로 말할 것 같으면 옆으로 누운 사랑니를 하나 빼느라 지독하게 고생을 한 덕에 TV나 보면서 끙끙대느라 다른 공부를 할 여지가 없었다. 변명이라면 변명이고... 더워서 진짜 뭐 할 의욕이 나야 말이지.


일단 갤러그 제작은 잠시 중단했다. 레이저를 뿅 쏘는 과정까지는 성공했지만, 한 발 쏘고 나면 다음 레이저가 날아가질 않아서..... 그를 해결하기 위해 아픈 잇몸을 부여잡고 땀을 찔찔 흘리면서 연구를 거듭해 봤지만 머리가 안 도는건지 그냥 무식해서인지 아무튼 되질 않았다. 공부를 더 할 필요가 있었다.

그래서 아무튼 그닥 길지도 않은 휴가를 맞아 좀 더 많은 공부를 하기 위해 책을 샀다.

좋은 책을 고르기 위해 꼬박 하루를 써서 종로의 교보문고까지 가 봤지만(내가 아는 한 가장 큰 곳이 거기다) 결국 여러 군데서 찾을 수 있는 이 책이 가장 적합하다고 느꼈다. 일단 c나 c++의 아주 기초적인 부분을 설명하느라 페이지를 할애하지 않고, 처음부터 간단한 콘솔창 게임 제작부터 들어가는 게 딱 내가 원하던 바이다. 작가는 버철온 같은 걸 만든 전문 프로그래머라고 하니까, 뭐 믿을 수 있겠지(그래서인지 다이렉트3D 예제 화면은 버철온과 비슷한 구도다)
그래서 이번에는 책에서 나와 있는 맨 처음 소스 코드를 그대로 분석해가면서 어떻게 돌아가는 원리인지 후벼보기로 했다. 일단 한 번 쓱 배껴보기는 했는데 핵심적인 부분이 이해가 안 가서, 내가 책 내용과 소스코드를 보면서 혼자 중얼중얼거리며 이건가? 저건가? 하고 장님 코끼리 만지듯 훑어볼 것이다. 조언을 해 주신다면 감사히 배우도록 하겠다.
만약 이 소스코드와 책 내용의 공개가 저작권 등의 문제가 된다면 바로 삭제하겠다.(소스코드 자체가 문제가 될 것 같진 않다. 공개 페이지에다가 책에 나오는 걸 다 올려놨으니까.)

요는 이런 것을

이렇게 나오도록 하는 소스코드다. 창고정리 게임 같은 거다. 소코반은 안 해봤지만.
일단 전체 소스코드를 먼저 써 보겠다. 책에 것을 배껴 적으면서 약간의 어레인지를 가했다. 이 코드를 자세히 잘 보고 싶으신 분은 컴파일러에 복사해서 보시는 게 편할거다.
#include <iostream>
using std::cout; using std::cin; using std::endl;
const char gStageData[] = "\
########\n\
# .. p #\n\
# oo #\n\
# #\n\
########";
const int gStageWidth = 8;
const int gStageHeight = 5;
enum Object {
OBJ_SPACE,
OBJ_WALL,
OBJ_GOAL,
OBJ_BLOCK,
OBJ_BLOCK_ON_GOAL,
OBJ_MAN,
OBJ_MAN_ON_GOAL,
OBJ_UNKNOWN,
};
void initialize( Object * state, int w, int h, const char * stageData);
void draw( const Object * state, int w, int h);
void update( Object * state, char input, int w, int h);
bool checkClear( const Object * state, int w, int h);
int main() {
Object * state = new Object[ gStageWidth * gStageHeight ];
initialize( state, gStageWidth, gStageHeight, gStageData );
while(true){
draw(state, gStageWidth, gStageHeight);
if(checkClear(state, gStageWidth, gStageHeight)){
break;
}
cout<<"a:left d:right w:up s:down. command?"<<endl;
char input;
cin >> input;
update(state, input, gStageWidth, gStageHeight);
}
cout<<"힘세고 강한 아침!"<<endl;
delete [] state;
state=0;
return 0;
}
void initialize(Object * state, int w, int h, const char * stageData){
const char * d = stageData;
int x=0; int y=0;
while(*d != '\0') {
Object t;
switch(*d) {
case '#' : t = OBJ_WALL; break;
case ' ' : t = OBJ_SPACE; break;
case 'o' : t = OBJ_BLOCK; break;
case 'O' : t = OBJ_BLOCK_ON_GOAL; break;
case '.' : t = OBJ_GOAL; break;
case 'p' : t = OBJ_MAN; break;
case 'P' : t = OBJ_MAN_ON_GOAL; break;
case '\n' : x = 0; ++y; t = OBJ_UNKNOWN; break;
default : t = OBJ_UNKNOWN; break;
}
++d;
if(t != OBJ_UNKNOWN) {
state [y*w + x] = t; ++x;
}
}
}
void draw(const Object * state, int width, int height) {
const char font[] = {' ', '#', '.', 'o', 'O', 'p', 'P'};
for(int y=0; y<height; ++y){
for (int x=0; x<width; ++x){
Object o = state[y*width+x];
cout<<font[o];
}
cout<<endl;
}
}
void update(Object * state, char input, int w, int h) {
//===================================================
//이동 관련
int dx = 0; int dy = 0;
switch(input) {
case 'a' : dx = -1; break;
case 'd' : dx = 1; break;
case 'w' : dy = -1; break;
case 's' : dy = 1; break;
}
//=======================================================
//캐릭터 좌표검색
int i;
for(i=0; i<w*h; i++) {
if (state[i] == OBJ_MAN || state[i] == OBJ_MAN_ON_GOAL) {
break;
}
}
int x = i%w;
int y = i/w;
//======================================================
//이동 후 좌표
int tmpX = x + dx;
int tmpY = y + dy;
if( tmpX<0 || tmpY<0 || tmpX>=w || tmpY>=h) {
return;
}
//=======================================================
//이동할 목적지와 상자 이동 등등에 관하여
int cp = y*w+x; //cp == Current Position
int tp = tmpY * w + tmpX; //tp == TargetPosition
if(state[tp] == OBJ_SPACE || state[tp] == OBJ_GOAL) {
state[tp] = (state[tp] == OBJ_GOAL) ? OBJ_MAN_ON_GOAL : OBJ_MAN;
state[cp] = (state[cp] == OBJ_MAN_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;
} else if (state[tp] == OBJ_BLOCK || state[tp] == OBJ_BLOCK_ON_GOAL) {
int tx2 = tmpX+dx;
int ty2 = tmpY + dy;
if(tx2<0 || ty2<0 || tx2>=w || ty2 >=h) {
return;
}
int tp2 = (tmpY+dy) * w + (tmpX+dx);
if(state[tp2] == OBJ_SPACE || state[tp2] == OBJ_GOAL) {
state[tp2] = (state[tp2] == OBJ_GOAL) ? OBJ_BLOCK_ON_GOAL : OBJ_BLOCK;
state[tp] = (state[tp] == OBJ_BLOCK_ON_GOAL) ? OBJ_MAN : OBJ_MAN_ON_GOAL;
state[cp] = (state[cp] == OBJ_MAN_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;
}
}
}
bool checkClear(const Object * state, int w, int h) {
for(int i = 0; i<w*h ; ++i){
if(state[i] == OBJ_BLOCK)
return false;
}
}
일단 나같은 초보에게는 처음부터 조금 당혹스러운 코드다. 일단 c++ 스타일을 쓰면서도 클래스 하나 선언하지 않는 절차지향적 코드다. 물론 저자는 이 코드를 '좋은 코드'라고 소개하지는 않았다. 하지만 저자가 밝혔듯, 배울 점은 많아 보인다.
맨 처음 줄부터 차근차근 디벼보자
const char gStageData[] = "\
########\n\
# .. p #\n\
# oo #\n\
# #\n\
########";
const int gStageWidth = 8;
const int gStageHeight = 5;
딱 감이 오신 분들은 알겠지만 메인 스테이지 지도이다. 지도 데이터는 2차원 배열만 알고 있었는데 이건 1차원으로 간단하게 처리해 버린다. 요는 '그려지면 장땡'
p가 주인공이고, o가 상자, '.'이 상자를 옮길 장소. 이 개체들을 따로 그리도록 하는 게 아니라 미리 그려넣는다....
기묘한 것은 \n 다음에 또 나온 \인데, 코드를 보기 쉽도록 줄을 바꿔준 탓에 \를 넣어주지 않으면 컴파일이 안 된다.
enum Object {
OBJ_SPACE,
OBJ_WALL,
OBJ_GOAL,
OBJ_BLOCK,
OBJ_BLOCK_ON_GOAL,
OBJ_MAN,
OBJ_MAN_ON_GOAL,
OBJ_UNKNOWN,
};
다음 난관은 enum 열거형. 아주 초장부터 사람 힘들게 하는데, 나처럼 실무 경험 없는 초보에게 enum은 접할 기회가 많지 않기 때문. 게다가 이 사람, 이거 클래스처럼 쓰다가도 클래스처럼 안 쓴다.
void initialize( Object * state, int w, int h, const char * stageData);
void draw( const Object * state, int w, int h);
void update( Object * state, char input, int w, int h);
bool checkClear( const Object * state, int w, int h);
함수 프로토타입이다. 이 첫 예제는 문법은 c++을 썼지만 객체 지향은 거의 쓰이지 않았다. 일반적으로 메인 함수 내에서 함수 사용은
while(1){
그리기();
입력();
갱신();
}
과정을 거치지만 저기에는 입력 함수가 없는데, 매개변수 보면 update에 밀어 넣었다.
메인 함수를 보자
int main() {
Object * state = new Object[ gStageWidth * gStageHeight ];
initialize( state, gStageWidth, gStageHeight, gStageData );
while(true){
draw(state, gStageWidth, gStageHeight);
if(checkClear(state, gStageWidth, gStageHeight)){
break;
}
cout<<"a:left d:right w:up s:down. command?"<<endl;
char input;
cin >> input;
update(state, input, gStageWidth, gStageHeight);
}
cout<<"힘세고 강한 아침!"<<endl;
delete [] state;
state=0;
return 0;
}
Object를 클래스처럼, 그것도 동적할당까지 해 줬다. 8*5 = 40 만큼이 딱 맵 넓이이다.
책에서 상하좌우 이동키는 wzas로 하고 있는데, 일본애들은 fps 많이 안 하니까 이게 익숙한가보다. 나는 임의로 wasd로 바꿔줬다.
실시간 처리를 할 필요 없는 게임이기 때문에 cin을 바로 넣어 처리한다. 방향키 누르고 엔터를 일일이 쳐줘야 한다. 재밌는 건, s를 두 번 누르고 엔터를 치면 두 칸 이동함
코드 끝에 안 쓰는 포인터에 null을 넣는 습관을 들이는 게 좋다고 한다.
초기화 함수이다
void initialize(Object * state, int w, int h, const char * stageData){
const char * d = stageData;
int x=0; int y=0;
while(*d != '\0') {
Object t;
switch(*d) {
case '#' : t = OBJ_WALL; break;
case ' ' : t = OBJ_SPACE; break;
case 'o' : t = OBJ_BLOCK; break;
case 'O' : t = OBJ_BLOCK_ON_GOAL; break;
case '.' : t = OBJ_GOAL; break;
case 'p' : t = OBJ_MAN; break;
case 'P' : t = OBJ_MAN_ON_GOAL; break;
case '\n' : x = 0; ++y; t = OBJ_UNKNOWN; break;
default : t = OBJ_UNKNOWN; break;
}
++d;
if(t != OBJ_UNKNOWN) {
state [y*w + x] = t; ++x;
}
}
}
화면에 그림을 뿌리는 건 draw가 하는 일이고, 이건 말 그대로 초기 데이터의 지정이다. 무진장 어렵다. 근데 뒤는 더 어렵다.
일단 매개변수로 받은 맵 데이터 주소를 변수 d에 대입.
x와 y의 존재가 거슬리는데, 이 x와 y 주소를 훑어가면서 데이터를 비교하는 게 아니라 데이터를 차례로 훑어가면서 x와 y 값을 변형시킨 뒤 다시 주소를 훑어가는 방법을 사용하기 때문이다. 갤러그 만들 때 이 주소 따라서 별 삽질을 다 하던 나에게는 너무 안 익숙한 방법이다. 안 익숙하다기보단 헷갈린다.
지도 데이터의 끝부분까지 반복을 시키고, 그러는 동안 데이터를 비교한다. 그 전에 먼저 Object를 하나 더 인스턴스한다.... 또?
t는 사실상 int값을 받는 거나 마찬가지인데, 이 t를 나중에 Object형 state에 보내버려야 하므로 Object 형으로 만들어준 거겠지. 아아 enum은 어렵다....
일단 이렇게 해서 맵 데이터 하나하나, \n 하나 넣은 것마저도 모조리 값 대입을 해준 뒤 그걸 40개 만든 state로 보내는 거다. 당연히 \n 같은 부분은 무시해버린다.
switch와 if에서의 x와 y의 값 처리를 잘 볼 필요가 있겠다. 이런 상황에 따른 정교한(?) 위치 처리는 내게 아주 많이 부족한 스킬이다.
그리고 잘 보면 h인자는 써먹지 않았다. 함수들 모양새의 일관성을 유지시키려고 그냥 넣은 건지도 모르겠다.
void draw(const Object * state, int width, int height) {
const char font[] = {' ', '#', '.', 'o', 'O', 'p', 'P'};
for(int y=0; y<height; ++y){
for (int x=0; x<width; ++x){
Object o = state[y*width+x];
cout<<font[o];
}
cout<<endl;
}
}
초기화 함수가 대충 이해가 됐다면 draw는 쉬울 것 같지만, 도대체가 호락호락하지 않다. 일단 또 배열을 하나 만들고 있다. 그림을 그린 걸 숫자로 만들고, 숫자로 만든 걸 그림으로 뿌리고... 말은 간단하지만 벌써 머리가 깨질 것 같다. 이렇게 하는 것도 다 나중에 맵 데이터를 바꿀 때를 위해서일 것이다.
for문에서 재밌는 점은 y++이 아니고 ++y라는 점이다. x도 마찬가지. 바꿔서 컴파일 해봤지만 다른 점은 없었다. 일본인들하고 스타일 차이려나
그리고 또 Object를 인스턴스한다. 또? 앞서 말했듯 o는 int값(숫자값)이나 마찬가지라서 바로 font 안으로 넣어 순번대로 꺼내 보낸다. 이렇게 되면 딱 우리가 그린 모양 그대로 출력된다.
짧지만 혼란스러운 방법인데, 그래서 저자는 대안을 제시하고 있다.
void draw(const Object * state, int width, int height) {
for(int y=0; y<height; ++y){
for (int x=0; x<width; ++x){
Object o = state[y*width+x];
switch(o) {
case OBJ_SPACE : cout<< ' '; break;
.
.
.
.
}
}
cout<<endl;
}
}
뭐 이것도 나한테 쉬운 건 아니다. 요는 Object 인스턴스들이 실제 컴파일에선 어떻게 변환되는가를 간파해야 한다.
다음은 충격과 공포의 update 함수이다.
void update(Object * state, char input, int w, int h) {
//===================================================
//이동 관련
int dx = 0; int dy = 0;
switch(input) {
case 'a' : dx = -1; break;
case 'd' : dx = 1; break;
case 'w' : dy = -1; break;
case 's' : dy = 1; break;
}
//=======================================================
//캐릭터 좌표검색
int i;
for(i=0; i<w*h; i++) {
if (state[i] == OBJ_MAN || state[i] == OBJ_MAN_ON_GOAL) {
break;
}
}
int x = i%w;
int y = i/w;
//======================================================
//이동 후 좌표
int tmpX = x + dx;
int tmpY = y + dy;
if( tmpX<0 || tmpY<0 || tmpX>=w || tmpY>=h) {
return;
}
//=======================================================
//이동할 목적지와 상자 이동 등등에 관하여
int cp = y*w+x; //cp == Current Position
int tp = tmpY * w + tmpX; //tp == TargetPosition
if(state[tp] == OBJ_SPACE || state[tp] == OBJ_GOAL) {
state[tp] = (state[tp] == OBJ_GOAL) ? OBJ_MAN_ON_GOAL : OBJ_MAN;
state[cp] = (state[cp] == OBJ_MAN_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;
} else if (state[tp] == OBJ_BLOCK || state[tp] == OBJ_BLOCK_ON_GOAL) {
int tx2 = tmpX+dx;
int ty2 = tmpY + dy;
if(tx2<0 || ty2<0 || tx2>=w || ty2 >=h) {
return;
}
int tp2 = (tmpY+dy) * w + (tmpX+dx);
if(state[tp2] == OBJ_SPACE || state[tp2] == OBJ_GOAL) {
state[tp2] = (state[tp2] == OBJ_GOAL) ? OBJ_BLOCK_ON_GOAL : OBJ_BLOCK;
state[tp] = (state[tp] == OBJ_BLOCK_ON_GOAL) ? OBJ_MAN : OBJ_MAN_ON_GOAL;
state[cp] = (state[cp] == OBJ_MAN_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;
}
}
}
dx와 dy는 difference 약자라고 한다. direction도 괜찮은 거 같은데.
캐릭터 좌표 검색은 p가 좌표값을 첨부터 가지고 있다가 어디로 전달하고 하는 게 아니고, 오히려 맵 전체를 훑어 그 안에서 p를 찾아 그 좌표값을 구하는 과정이다.
이동 후 좌표는 구한 좌표+dx, dy인데, 그냥 변수에 저장만 해 뒀다. 좌표값을 일단 모조리 다 변수에 저장하는 중이다. 이동 범위는 당연히 맵 범위 내이고, 그 범위를 벗어나면 아예 함수를 과감히 종료시킨다.
그리고 이 게임은 상자도 이동시켜야 하기 때문에 캐릭터 이동과도 연계시켜야 한다. cp는 캐릭터의 현재 서 있는 위치를 가리킨다. 지도 전체가 1차원 배열이기 때문에 위치값도 x와 y를 구분하지 않고 한가지 변수로 끝내버렸다. tp 또한 마찬가지. x값 y값 다 구하고 그걸 변수 하나로 통일한다.
입력을 받고 난 뒤 입력된 목적지의 지도값이 어떻게 돼 있나를 검색하고 삼항연산자를 받는다. 빈 공간이거나 빈공간에 준하는(상자를 보낼 목적지) 위치라면 목적지로 사람값을 그리고, 원래 있던 위치는 빈 공간을 그려넣는다. 저자도 이 코드가 좀 지저분하다고 했는데, 뭐 어찌어찌 함수를 만들어 true나 false를 리턴하게 만들어도 되지.
if(state[tp] == OBJ_SPACE || state[tp] == OBJ_GOAL) { //목적지가 비었거나 골 지점이거나(둘 다 이동 가능위치)
state[tp] = (state[tp] == OBJ_GOAL) ? OBJ_MAN_ON_GOAL : OBJ_MAN; //목적지에다 사람을 그림
state[cp] = (state[cp] == OBJ_MAN_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE; //원래 위치에다 공백을 그림
위 부분은 별로 어려울 게 없는 부분이다. 복잡한 건 아래에 있다.
else if (state[tp] == OBJ_BLOCK || state[tp] == OBJ_BLOCK_ON_GOAL) {
int tx2 = tmpX+dx;
int ty2 = tmpY + dy;
if(tx2<0 || ty2<0 || tx2>=w || ty2 >=h) {
return;
}
int tp2 = (tmpY+dy) * w + (tmpX+dx);
if(state[tp2] == OBJ_SPACE || state[tp2] == OBJ_GOAL) {
state[tp2] = (state[tp2] == OBJ_GOAL) ? OBJ_BLOCK_ON_GOAL : OBJ_BLOCK;
state[tp] = (state[tp] == OBJ_BLOCK_ON_GOAL) ? OBJ_MAN : OBJ_MAN_ON_GOAL;
state[cp] = (state[cp] == OBJ_MAN_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;
}
}
이동할 위치에 박스가 있을 경우이다. tx2,ty2는 또 임시로 만들어지는 변수다. 이 부분은 if문에서도 조건을 달아놓았듯이 이동시킬 상자에 대한 부분이다. 이동시킨 방향에 박스가 있으면 먼저 박스가 이동 가능 범위에 있는지 또한 확인하고, 박스가 캐릭터 이동 방향과 같은 방향으로 움직이고, 박스가 있던 자리에 사람을 그리고, 사람 있던 자리를 공백으로 만든다.
이 부분에 함정이 하나 섞여 있는데, OBJ_WALL에 대한 처리는 쏙 빼놨다. 사각형 맵이니까 그냥 범위 지정만 해준 거겠지.
bool checkClear(const Object * state, int w, int h) {
for(int i = 0; i<w*h ; ++i){
if(state[i] == OBJ_BLOCK)
return false;
}
}
이건 뭐 쉽다. 골인 하지 못한 박스가 있으면 계속하고, 없으면 게임 끝.
함수의 흐름을 살펴보자.
#include <iostream>
맵 그림과 크기를 상수로 받는다;
맵 그림에 해당되는 아이콘들을 받을 열거형을 만든다;
함수();
int main() {
열거형을 맵의 크기만큼 state로 인스턴스한다;
맵 그림 주소를 d 변수로 받는다;
좌표값 변수 두 개를 만든다;
while(d의 끝까지) {
열거형 t를 인스턴스한다;
d를 읽어들여 그에 해당하는 부분을 t에 대입한다;
state에 t를 대입한다;
}
while(break 뜰 때까지) {
state를 하나씩 읽어 해당하는 만큼 그리자;
clear가 됐는지 확인한다;
if(clear됐으면) break;
input에 입력을 받는다.
input값에 따라서 어느 방향으로 움직일지 변수 dx나 dy에 저장;
맵 전체를 훑어 캐릭터의 위치를 찾아내 변수 x와 y에 저장;
이동 후의 위치를 tmpX, Y변수에 각각 저장한다. tmpX = x + dx;
이동 후의 위치가 맵의 범위 내인지 검사;
캐릭터의 위치와 목적지를 x, y값을 이용해 0~39 사이의 숫자로 변환해 변수에 저장;
state[캐릭터 위치]. state[목적지] 데이터를 참조해 조건문을 수행하고 해당 위치에 새로운 값을 대입;
box와 관련된 부분도 마찬가지;
}
게임 끝. 뒷정리;
}
다음 시간에는 이에서 좀 더 심화된 설명으로 이어진다. 덥구만, 빌어먹을....
덧글