우리의 목표.
지난 시간에 MUD(Multi User Dungeon)을 만들겠다고 선언했습니다.
뜬금없이 MUD를 만드는 목적은 MUD 그 자체가 목적이 아닙니다.
최종 목적은 다수의 유저가 접속하고 게임 서비스를 진행해주는 C/S 시스템을 만드는 것인데,
그것을 위해 기본이 되는 컨텐츠가 필요하기에 MUD를 만드는 것입니다.
다행히 MUD는 상당수의 MMORPG와 비슷합니다.
MMORPG도 MUD에 결국 그래픽이 덮씌워진 것입니다.
물론 그래픽을 비롯해 여러 최신 기법들이 적용되어 있고 언젠가 그것들을 이해해야 합니다.
하지만 처음부터 복잡한 원리를 억지로 이해할려고 하는 것보다 먼저 그게 왜 필요했는지를 알면 더 좋을 거라 생각듭니다.
이번 튜터리얼을 통해 MUD를 만들며 거기에 대한 해답을 얻을수 있었으면 합니다.
1. RPG 게임의 기본 요소, 혹은 객체MUD를 구현하기에는 RPG가 가장 알맞으니까 RPG를 만들 것입니다.
소드 아트 온라인을 만들고 싶다!!!!
그럼 반도의 흔한 양산형(^^;;) RPG에는 어떤 요소들이 필요할까요?
1) 공간
지형이죠. 땅은 물론이고 하늘과 바다같은 공간입니다.
공간이 있어야 플레이어와 몹이 뛰어다니고 날라다닐수 있습니다.
특별한 경우를 제외하면 지형 이나 공간 자체가 객체가 되는 일은 없습니다만
공간이야말로 캐릭터들을 위한 가장 기본적인 데이터가 됩니다.
위엄돋는 프로브
대부분의 땅은 2차열 배열로 충분합니다.
물론 게임이 커지며 쿼드트리, 옥트리나 분할타일...등등 다양한 방식들이 존재하는데 지금은 굳히 어려운 기법으로 구현할 필요 없습니다.
필요할때 공부하면 됩니다. 사실 클라이언트와 결합되면 이런 것들만 소개해도 책 몇권 나올겁니다. 그래서 우리는 엔진을 선택하죠. ㅠ.ㅠ;
2차원 배열, 다시 말해 타일앱(Tilemap)은 게임에 있어서 뼈대와 같습니다.
아니 2D 게임이라면 죄다 타일맵입니다. 배열을 보여줄때 어떤 꽁수를 부리느냐에 따라 사각형이나 마름모가 될뿐입니다.
이것도 2차열 배열
요것도 2차원 배열
심지어 이것도 2차원 배열~
2차원 타일앱에 대해 더 알고 싶다면,
http://kangcom.com/sub/view.asp?sku=200107040021 과
http://kangcom.com/sub/view.asp?sku=200407060008 책을 추천합니다.
좀 고전적인 내용이긴 합니다만 그만큼 기초적이며 필수적인 내용들을 다루고 있습니다.
꽤 오래된 책이긴 하죠? ^^
사실 Isometric Game Programiing with DirectX는 쓸때없이 두껍습니다.
정작 필요한 챕터는 몇십페이지가 안됩니다.
반면 DirectX9를 이용한 전략 게임 프로그래밍은 길찾기부터 전반적인 내용이 다들어가고 있고
정말 좋아했던 번역자분이 번역하신 책입니다. 좋은 번역이 무엇인지를 제대로 보여주는 몇안되는 기술서적이죠.
너구리 같은 사이드뷰(side view) 방식, 혹은 장기같은 탑뷰(top view)타일맵의 기본은 정말이지 너무 간단합니다.
사이즈에 맞는 2차원 배열을 만들고 for() 문 두개면 뚝딱 나옵니다.
일단 가로x세로, 각각 10칸씩 차지하는 맵을 만들어봅시다.
int map[MapSizeX][MapSizeY] =
{
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};
네 이것으로 다입니다. ^-^;;;
배열 처음 배울때 배운 것 그대로입니다.
땅을 화면에 표현해볼까요?
void DrawAllMap()
{
for(int MapX = 0; MapX < MapSizeY; MapX++)
{
for(int MapY = 0; MapY < MapSizeX; MapY++)
{
cout << MapY << "," << MapX << ": " << map[MapX][MapY] << " ";
}
cout << endl;
}
}
드디어 첫선을 보이는 소드 아트 온라인(?) 머드
몇 바이트 안되는 자료구조로 캐릭터가 뛰어놀 땅이 생긴 것입니다.
각각의 배열은 좌표계를 의미합니다.
그래픽 게임이라면 각 칸을 이동하는 애니메이션이 추가되면 됩니다.
하지만 이건 MUD니까 그냥 "~로 이동했다" 정도로 표시하면 될 것입니다.
드디어 첫선을 보이는 소드 아트 온라인(?) 머드
만약 공간 구현에 있어 데이터 최소사이즈 덕후가 되어 보고 싶다면 이 책을 추천합니다.
http://kangcom.com/sub/view.asp?sku=200205090004&mcd=571제가 아주 좋아하는 책인데 지형뿐만 아니라 앞으로 다룰 플레이어 객체와의 상호작용까지 다룹니다.
각종 게임회사 입사 퀴즈 문제가 이 안에 있음.
2) 캐릭터 - PC/NPC
보통 플레이어와 몹을 따로 생각하는 경향이 있지만 결국 같은 액터(actor)라고 봐도 무방합니다.
보통 유저가 직접 조작할수 있는 PC(Playable Character)와 NPC(None Playable Character)로 나뉩니다.
NPC는 상점 주인이나 몹이 대표적입니다.
PC와 NPC의 구분이란?
경우에 따라 특정 아이템이나 장치도 NPC가 될수 있습니다.
상자를 열자마자 나타나는 저주받은 칼이라던가 두동강 내는 절단 장치 등도 게임에 따라 내부적으로 NPC로 구현된 경우가 있습니다.
이런 캐릭터야말로 OOP방식으로 게임을 만든다면 가장 기본적인 객체로 구분할 수 있을 것입니다.
물론 이동, 공격, 스킬도 캐릭터의 가장 중요한 속성입니다.
우선 싱글 위주로 구현을 해보죠.
원래는 C++ 없이 C만으로도 게임 만들기 쉽다는 것을 보여주기 위해 순수 C로 만들어볼 생각이었지만,
역시 C++을 안쓰니 제가 불편해 구조체 대신 C++ 클래스로 만들었습니다.
우리의 캐릭터, 아니 플레이어 객체가 가지고 있어야할 가장 기본적인 데이터는 무엇일까요?
1) 클래스 자체의 정보 - RTTI를 위해
2) 현재 위치 - 플레이어가 뛰어다닐 공간은 2차원 배열이니까 2차원 배열의 X, Y값이 들어갑니다.
3) 이름, 레벨, 경험치...
4) 소유 아이템
5) 가능한 퀘스트(이벤트)/현재 퀘스트(이벤트)
이중 우선 움직이는게 관건이니 2)의 현재 위치를 먼저 구현하겠습니다.
나머지는 다중 사용자와 컨텐츠가 붙으며 천천히 구현해도 될 것 같네요.
1)의 클래스 자체의 정보는 고급 토픽입니다.
일단 상속과 다른 캐릭터들이 추가되며 그때 다시 이야기해보지요.
enum _DIRECTION
{
NORTH = 0,
EAST = 1,
SOUTH = 2,
WEST = 3
};
typedef struct _GamePosition
{
int iPosX;
int iPosY;
_DIRECTION Direction;
} GamePosition;
class CGamePlayer
{
public:
CGamePlayer(GamePosition& playerPos);
~CGamePlayer(void);
int SetPosition(GamePosition& playerPos);
GamePosition GetPosition();
private:
GamePosition m_playerPos;
};
일단 위치만 넣었습니다만 위치 변수를 int형 X, Y를 그대로 사용하지 않고 GamePosition이라는 구조체를 만들어 썼습니다.
중요한 것은 위치정보 안에 _DIRECTION이라는 방향정보까지 들어가 있다는 점이죠.
게임을 만들때 위치가 있으면 대부분 바라보는 방향도 있으니까요.
이렇든 구현을 하다보면 자체의 데이터를 정의하기 위한 구조체나 클래스들이 어쩔수 없이 많이 나오게 됩니다.
그리고 이런 것들을 잘 쓰는게 OOP의 은닉화, 혹은 캡슐화(encapsulation)입니다.
아뭏든 상기의 플레이어 클래스를 보니 현재 이 게임은 10x10의 공간 속에 방향을 가지고 위치를 이동하는 게임이라는 것이 명확해졌습니다.
여담으로 며칠전 간만에 학부생들이 한가득한 어떤 프로그래밍 게시판을 들렀습니다(후빨 받을려고 간거 아님).
언제나처럼 불변의 진리로 "숙제 때문에" 클래스와 인터페이스 설계 떡밥은 가라앉질 않더군요.
학교에서 내주는 클래스 설계 숙제나 초보용 예제는 그 힘든 OOP 개념을 설명하긴 설명하긴 해야겠는데,
억지로 간단한 수준에서 구현할려보니 도리어 더 헛갈린다고 생각합니다.
사실 상속이나 인터페이스는 학부 교재 수준의 단순한 코드를 위해 만들어진건 아닙니다.
실제 캐릭터들간의 교과서적인 상속같은 것 역시 별로 중요한 문제도 아닙니다.
개념적인 상속을 받지 않고도 구현할 수 있고 다양한 방법들이 많습니다.
예를 들면 클라이언트라면 코드의 재사용이 아닌 렌더러를 타기 위해 상속을 받거나, 상속 자체보다 통보 메시지를 일시에 받기위한 다형성(polymorphism)이 보다 중요합니다.
이런 이야기 역시 천천히 진행해볼까 합니다. 물론 여기는 현업 개발자마다 프로젝트마다 다른 방법이 있고 치고 박고 싸울 떡밥은 충분히 됩니다만, 저라면 제일 중요한게 "라이브할때 편한 방식"입니다. 라이브팀에게 "이거 만든 넘이 너지?!"하면서 등에 칼맞긴 싫거든요.
3) 돈/아이템
돈과 아이템이 없는 게임이 있을까요.
아이템은 캐릭터의 상태와 속성을 변화시키고 수치를 바꾸고 이벤트를 발생시킬수도 있습니다.
아이템은 C/S 환경에서 더욱 복잡해집니다.
언젠가부터 필수 요소인 경매장도 아이템 설계에 많은 변형을 가져와야 했으며 복사템 문제는 여전히 골치를 앓게 합니다.
이 부분은 구현 이전에 알아야할 다른 문제들이 있으니 조금 더 나중으로 미루겠습니다. ^^
여담으로 최근 가장 관심이 갔던 아이템은 디아블로3의 "소멸의 염료"였습니다. 하의는 항상 소멸의 염로로 염색한다!
캐릭터 객처와 아이템 객체간 상호작용에 대한 아주 좋은 사례
(출처 : Bump of Romance
http://djrtoddl.egloos.com/3328165)
4) 퀘스트/이벤트사람과 팀에 따라 구현방식이 달라지겠지만,
저는 퀘스트와 이벤트도 하나의 객체로 취급합니다.
이런 개념의 차이는 게임 구현에 큰 영향을 끼치게 됩니다.
아이템과 함께 퀘스트/이벤트는 캐릭터와 끊임없는 상호 작용을 거치게 됩니다.
NPC를 통해 퀘스트를 해결하는 좋은 사례
아이템과 같이 일단 긴 설명은 시기상조입니다만 상기의 네가지들은 게임 컨텐츠를 만드는데 있어서
제가 뽑은 가장 기본적인 필수요소들이라 생각합니다.
있어 보일려고 한다면 더 적을수도 있겠지만 정말이지 이 네가지면 기본은 충분합니다.
3. 움직이자!!
아무리 좋은 동영상도 단순한 게임 그래픽보다 못합니다. 게임은 입력을 받아 인터랙티브하게 움직여 주니까요.
이제 움직여야죠.
플레이어를 움직이게 하는 것은 간단히 getchar()나 cin이면 충분합니다. 오오~ CUI 창의 위대함이여~
C 첫 시간에 배운 순환문과 입력함수면 게임 루프를 만들수 있습니다.
여기에서 한가지 기억해둬야 할 것은 사용자가 여러개의 입력을 한 경우입니다.
아쉽게도 C/C++ 표준함수만으로는 깔끔하게 한글자만 정확히 입력받을순 없습니다.
아래 보시면 while문이 있는데 첫번째 글자 이후로는 리턴시킵니다.
움직임은 플레이어의 위치 값만 바꿔주면 됩니다.
int SetMovePlayer(CGamePlayer* pGamePlayer)
{
cout << "어디로 이동할 것인가?" << endl;
cout << "E: 동쪽 W: 서쪽 S: 남쪽 N: 북쪽 .: 종료" << endl;
char ch;
while((ch = cin.get()) != '.')
{
ch = tolower(ch);
switch(ch)
{
case 'e' :
{
GamePosition pos = pGamePlayer->GetPosition();
pos.iPosY++;
if(pos.iPosY >= MapSizeY)
{
pos.iPosY--;
cout << "그곳으로는 이동할 수 없다." << endl;
}
else
{
cout << "키리토는 동쪽으로 이동하였다." << endl;
pGamePlayer->SetPosition(pos);
}
}
break;
// 생략
default:
cout << "잘못된 명령입니다." << endl;
}
while(cin.get() != '\n')
continue;
SetDirectionPlayer(pGamePlayer);
}
return 1;
}
흠....2차원 배열이라는게 깔끔하게 직관적인 X, Y 좌표계가 아니라서 이동이 좀 헛갈리겠군요.
그림으로 그려보니 이렇게 동남쪽을 향하는 좌표계가 되었습니다.
SAO의 제멋대로 좌표계
논리적 좌표계는 게임마다 다른거니 귀찮아서 수정하지 않고 이대로 갈까합니다.
(이미 Move함수를 만들어 두었으니 또써먹으면 되니까요.)
위의 소스 정말 쉽죠?
방향에 따라 X, Y 위치만 바꿔주면 정말로 이동합니다, 아니 이동하는 것처럼 됩니다.
그리고 중요한 부분이 있죠, 바로 화면 밖으로 나가지 못하도록 하는 클리핑(clipping)입니다.
너무나 중요해서 다시 적습니다, 이 부분이죠, 이 부분.
pos.iPosY++;
if(pos.iPosY >= MapSizeY)
{
pos.iPosY--;
cout << "그곳으로는 이동할 수 없다." << endl;
}
그런데 코드를 보니 일단 이동한번 해봤다가 조건에 걸리면 뒤로 빼는군요.
아!!!!!!!!!!!!!!!!!!
이래서 게임을 하다보면 오브젝트에 끼워 왔다리 갔다리 화면이 덜덜덜덜 거리는 현상이 일어나는가 봅니다!!!
다음 시간에 다룰 것이지만 이 루틴에 조건들만 추가하면 그럴싸한 충돌처리가 됩니다.
아래 부분에 while()과 continue 가 있습니다.
이 부분의 역활은 무엇일까요?
사용자가 방향이라는 한 단어 대신 "eeeeeeeeeeeeeeeeee"식으로 여러개의 명령이 왔을때 skip하는 부분입니다.
이런 입출력에 대한 예외처리는 이런 단순한 게임이라도 반드시 들어가며 나중에 완성도를 좌지우지하는 중요한 부분입니다.
아직 공부 중이라 작은 소품들을 만드는 것은 아주 훌륭합니다만,
이런 예외처리를 놓치지 마세요.
거대한 프로젝트라도 바로 이런 부분에서 새어버리면 망하는 지름길이 됩니다.
그런데 일부러 코드에서 이 부분을 빼버리면 어떨까?
흠.................. 봇?
4. 사실같은 그래픽
마음의 눈으로 보면 사실같은 그래픽이 됩니다.
사실같은 그래픽까지는 바라지도 않고 원작같은 그래픽 뽑고 싶습니다!!!!!!!!!!!
...는 무리고 현재 배열과 플레이어의 위치를 능력껏 최대한 화려한 그래픽으로 뽑아봅시다.
배열에 따라 땅과 플레이어의 방향을 표시할 것입니다.
이번에도 for()문 두개면 끝납니다.
조건은 플레이어의 위치와 방향입니다. 아무 것도 없는 땅은 "▦"로 표시합니다.
void DrawAllMapWithPlayer(CGamePlayer* pGamePlayer)
{
for(int MapX = 0; MapX < MapSizeY; MapX++)
{
for(int MapY = 0; MapY < MapSizeX; MapY++)
{
if((pGamePlayer->GetPosition().iPosX == MapX) && (pGamePlayer->GetPosition().iPosY == MapY))
{
switch(pGamePlayer->GetPosition().Direction)
{
case EAST :
cout << "▶";
break;
case WEST :
cout << "◀";
break;
case SOUTH :
cout << "▼";
break;
case NORTH :
cout << "▲";
break;
default:
break;
}
}
else
{
cout << "▦";
}
}
cout << endl;
}
}
그럭저럭 이번 원고 완성 ㅜㅜ
자, 몇줄 안되는 코드로 나름 MUD같은게 나왔습니다. 정확히 말하자면 싱글게임이지만요.
이번 화에서 강조하고 싶은 것은 게임 컨텐츠를 만드는데 엄청난 실력까지 필요없다는 것입니다.
소스를 보면 아시겠지만, 대입, 순환, 조건과 너무나 간단한 기본 입출력으로 게임틱한 프로젝트를 만든 것입니다.
"지형, 플레이어, 입력, 이동", 거기에 예외처리까지 이번 화속에 다 들어있습니다.
화려한 상용게임도 크게 다르지 않습니다.
그야말로 존경하옵는 다익스트라님의 증명이 다시한번 이뤄지는 순간입니다.
다음 시간에는 NPC와 전투를 다뤄볼까 합니다. 조금씩 덩치가 커지며 필요한 클래스들도 추가될 것 같습니다.
이제 추석 연휴군요, 추석 동안 살찌지 말고 친척분들에게 정신공격 당하지도 공격하지도 않으시길 바랍니다.