카테고리 없음

[펌] 모바일 3D 게임을 만들자③

achivenKakao 2005. 3. 12. 19:59
모바일 3D 게임을 만들자] ③ 코드 분석
조종근, 구병기 (마이크로소프트웨어 필자)
2004/08/12
이번에는 본격적으로 게임 코드를 살펴 본다. 백문이 불여일견이라고 실제 코드를 직접 작성하고 실행해 봐야 어떤 식으로 3D 게임이 제작되는지를 이해할 것이다. 3D 게임이 겉으로는 다소 화려해 보일지 몰라도 실제 제작은 그리 화려하지 않다. 오히려 많은 수작업을 필요로 한다. 하지만 그렇다고 해서 작업 자체가 지루하진 않다. 하나씩 완성되어가는 모습을 확인할 때마다 새로운 힘이 솟아나기 때문이다.

이번 주제의 마지막 글에 이러한 기쁨을 싣지 못하는 아쉬움이 있다. 왜냐하면 디버깅과 하나씩 기능이 추가되는 것을 드라마틱하게 표현하는 것은 필자의 능력 이상의 것이라는 생각이 들기 때문이다. 작업하는 것을 누군가 조금씩 취재하여 정리하는 것이라면 모를까, 오래 전의 개발 과정을 하나하나씩 음미해 보면서 다시 정리하기란 쉽지가 않다. 그래서 생각한 것이 그냥 지루하지만 있는 소스를 설명해 보기로 결정하였다. 소스 코드는 그렇게 어렵게 작성되어 있지 않으며, 누구나 쉽게 이해할 수 있는 수준이다.

하지만 모바일 환경과 3D 환경을 동시에 이해하고 프로그래밍한다는 것이 그리 쉬운 일만이 아니다. 특히 카메라 작업을 구현하는 것은 매우 수학적인 이해를 바탕으로 해야 가능한 일이다. 물론 PC 환경의 많은 게임 개발 SDK에서는 너무나 다양한 기능들을 쉽게 사용할 수 있도록 제공해주고 있기 때문에 수학적 실력보다는 툴 사용법이나 시나리오에 더 신경을 쓸 수 있게 해주기도 한다. 그러나 모바일은 일단 제한적인 환경에서 행하는 작업이며, 앞서 선험적으로 개발된 모바일 3D 게임도 전무한 형편이다. 따라서 없으면 만들어서 한다는 생각을 기본적으로 가지고 있어야 한다. 또한 기능적으로 구현이 가능하다고 하더라도 성능이나 다른 제약이 있으면 그 제약 안에서 프로그래밍을 해야 한다.

모바일 환경이 가지고 있는 약점에 대한 푸념은 이 정도로 해두자. 이제 지루한 코드 설명에 돌입해 보자. 일단 헤더 파일을 살펴보고 메인 루프의 구성, 게임 초기화, 키보드 액션, 그래픽 액션 순으로 코드를 설명해 보겠다. 그리고 최종적으로는 실행 결과를 설명하는 것으로 연재를 마무리한다(이와 관련된 질문 사항은 www.g3d.co.kr에 접속하거나 e메일로 주시면 힘닿는 대로 답장으로 드리도록 하겠다).

G3 SDK를 이용한 스페이스큐브의 전체적인 게임 작성 구성도는 <그림 1>과 같다. <그림 1>의 내용을 순서대로 설명해 보면 다음과 같다.

<그림 1> G3 SDK를 이용한 전체적인 게임 구성 흐름도

1. 게임 리소스를 제작한다.
2. 게임에 사용하는 변수 및 함수를 작성한다.
3. 텍스쳐 및 이미지를 셋팅한다.
4. 게임 메인 루틴을 작성한다.
5. 게임 메인 루프의 사용자가 입력하는 키 이벤트를 처리한다.
6. 게임 운영에 필요한 충돌 및 루틴을 작성한다.
7. 게임 상에 보여줄 그래픽 루틴을 설계하고 작성한다.
    polygon/3d plane Generation
    - 스페이스 큐브 상에서 보여주는 물체와 평면을 보여주는 부분을 작성한다.
    normal vector Generation
    - 화면에 보여줄 평면의 방향을 설정한다.
    Texture Binding(Texture Index)
    - 화면에 보여줄 평면에 입혀질 텍스쳐를 선택한다.
    Texture Coordinate
    - 화면에 보여줄 평면에 텍스쳐를 입힌다.
    DrawElements(3D 공간에 적용)
    - 3D 공간에 앞에 설명한 데이터들을 적용하고 그려준다.
    OpenGL과 달리 OpenGL ES에서 물체를 그릴 때 사용하는 함수이다.
    3D Buffer Flipping
    - 3D 영역 버퍼를 LCD 화면에 그려준다.
    2D Image Blitting
    - 로딩된 2D 이미지를 필요한 부분만 LCD에 뿌려준다(UI 부분).
8. Sound 루틴 처리
    - 키 이벤트 및 게임 루틴의 변화에 따른 사운드를 플레이시킨다.
9. 게임 종료한다.
10. 리소스를 해제한다.
11. 프로세스를 종료한다.

이와 같은 절차로 게임을 작성한다. 특히 2D 게임이 아니라 3D 게임인 경우에는 화면에 3D 물체를 어떻게 만들어가며, 만들어진 물체에 이미지를 어떤 식으로 입혀가는지에 대해서 집중적으로 알아보기 바란다. 이번 호에서도 이 부분에 대해서도 충분히 언급했기 때문에 도움이 될 것이다.

헤더 파일
자, 우선 헤더 파일을 살펴보자. 헤더 파일에는 게임에 필요한 게임 스텝의 상수, 타일 맵의 속성, Vertex, Coords, Index, Normal 등의 각종 정보가 들어 있다. 헤더 파일만 보더라도 게임 프로그래밍 실력이 뛰어난 독자들은 어느 정도 게임의 수준을 파악할 수 있을지도 모르겠다. 상수는 게임의 상태를 나타내는 것으로, 인트로(Intro), 캐릭터 선택, 실제 게임, 게임 종료 등의 상태를 정의하며 게임의 전반적인 흐름을 제어하게 된다.

<리스트 1>의 FirstStage[8][8]에는 타일 맵에 필요한 정보 데이터가 들어 있다. 이름에서 알 수 있듯이 첫 번째 스테이지에 대한 타일 정보 데이터를 가지고 있는데, 각 타일마다 다른 값들을 설정함으로써 맵을 구성할 수 있다. 타일에 적용되는 속성은 다음과 같다.

 <리스트 1> 첫 번째 스테이지의 타일 맵

static short FirstStage[8][8] = { 0,0,0,0,0,0,0,0,
                                                 0,9,2,9,9,2,9,0,
                                                 0,2,0,2,4,0,2,0,
                                                 0,2,0,2,2,2,2,0,
                                              0,2,0,2,0,2,0,0,
                                                 0,2,2,0,2,2,2,0,
                                                 0,0,2,2,2,0,2,0,
                                                 0,0,0,0,0,0,0,0 };

0 = 위험 지역
1 = 정복한 지역
2 = 정복해야 할 지역
4 = 비활성화된 스테이지 클리어 지역
5 = 활성화된 스테이지 클리어 지역
9 = 유효 지역(사전 모양의 텍스처)

타일 맵에 대한 속성들을 정의했으니 이제 타일 맵에 대한 스테이지 맵들의 vertex 위치 정보를 정의해 보자. 스테이지 맵의 정점들의 위치는 cubeVerticesXX[][]를 통해서 할 수 있다. 즉 cubeVerticesXX는 타일 맵(FirstStage[][])의 vertex 위치를 의미하며, cubeVertices00는 FirstStage[0][0]와 동일하고, cubeVertices01은 FirstStage[0][1]와 동일하다.

 <리스트 2> 각 타일들의 Vertex List

GLfloat cubeVertices00[] = -1.5f, -1.0f, -4.0f, -1.5f, -1.0f, -3.0f, -0.5f, -1.0f, -3.0f, -0.5f, -1.0f, -3.0f, -0.5f, -1.0f, -4.0f, -1.5f, -1.0f, -4.0f

GLfloat cubeVertices01[] = -0.5f, -1.0f, -4.0f, -0.5f, -1.0f, -3.0f, 0.5f, -1.0f, -3.0f, 0.5f, -1.0f, -3.0f, 0.5f, -1.0f, -4.0f, -0.5f, -1.0f, -4.0f

GLfloat cubeVertices02[] = 0.5f, -1.0f, -4.0f, 0.5f, -1.0f, -3.0f, 1.5f, -1.0f, -3.0f, 1.5f, -1.0f, -3.0f, 1.5f, -1.0f, -4.0f, 0.5f, -1.0f, -4.0f

GLfloat cubeVertices03[] = 1.5f, -1.0f, -4.0f, 1.5f, -1.0f, -3.0f, 2.5f, -1.0f, -3.0f, 2.5f, -1.0f, -3.0f, 2.5f, -1.0f, -4.0f, 1.5f, -1.0f, -4.0f

....
....
....

그 외의 게임에 각종 필요한 데이터들을 <리스트 3>과 같이 정의한다.

 <리스트 3> 각 타일의 데이터 값들

// 텍스쳐  맵핑의 Coord의 위치
GLfloat cubeCoords[] = {
           // Coords 2f
           0.0f, 1.0f, 0.0f,  
           0.0f, 1.0f, 0.0f,  
           1.0f, 0.0f, 1.0f,  
           1.0f, 0.0f, 1.0f };
// DrawElements 시 인텍스값
GLushort cubeIndices[] = { 0, 1, 2, 3, 4, 5 }
// Face의 Normal의  값
GLfloat cubeNormals[] = {
           /* Normals 3f*/
           0.0f,-1.0f, 0.0f // Bottom Face
}

// 스테이지 맵의 버텍스 값들의 재배열
GLfloat *VertexName[8][8] =  { cubeVertices00,  cubeVertices01,  cubeVertices02,  cubeVertices03,
.....
.....
cubeVertices70,  cubeVertices71,  cubeVertices72,  cubeVertices73,  cubeVertices74,  cubeVertices75,  cubeVertices76,  cubeVertices77 };

<리스트 4>는 g3main.c에 선언되어 있는 게임 로직에 필요한 전역 변수 및 함수의 프로토타입이다.

 <리스트 4> 게임에 필요한 전역 변수 및 함수 리스트

//======================================================================
//         Variables
//======================================================================
G3DRAWSURFACE   g3Draw[2] ;               // Surfaces
unsigned int      nflip ;                  // Surface Number
unsigned int      nGAMESTATE ;            // Game State
unsigned short   trans_color ;           // temp trans_color
unsigned short   g_wWidth  = 240 ;       // Screen Width
unsigned short   g_wHeight = 176 ;       // Screen Height
int                ExKey ;                   // Key
int                nAvatarNum;              // 캐릭터 갯수
int                nStage = 1 ;             // 현재 스테이지
GLuint            tm_cube[10];             // fire texture
G3IMAGE_16       *pm_cube[10];            // fire texture images
int                nOldArrow = 0;           // 이전 캐릭터 방향
int                nNewArrow = 0;           // 현재 캐릭터 방향
int                nNowPos[2] = 1,1;       // 현재의 포지션
float              MoveCount = 0.0f;       // 캐릭터의 움직인 거리
int                nArrow = 0 ;             // 키보드 장향
int                charmoveX = 0;          // 캐릭터의 좌표 x축
int                charmoveZ = 0;          // 캐릭터의 좌표 z축
int                charmoveY = 0;          // 캐릭터의 좌표 y축
int                nScore = 0 ;             // 스코어
int                nStageCnt = 500;        // 카운트 변수
int                nStageClear = 0;        // 스테이지 완결 변수
int                nDie = 0 ;                // 캐릭터가 죽었을 때
int                nFall = 0 ;               // 캐릭터가 떨어졌을 때
int                nAction = 0;              // 점프 액션 변수
char               ScorePan[6];             // 스코어
char               CountPan[6];             // 카운트
unsigned short   nSelIntroBtn = 0;       // Start 및 exit 선택 변수
unsigned short   nSelChar = 1;            // 캐릭터  선택 변수
//======================================================================
//          Function
//======================================================================
int                InitGame(void);                            // 초기화 부분
void               KeyProcess(int nGameState);              // 사용자 입력에 대한 처리
void               UpdateGame(int nGameState);              // 갱신 부분
void               EndGame(void);                             // End Program
void               LoadTexture();                              // 텍스처 로딩
void               TileBinding(int nStage, int i, int k);  // 타일맵 바인딩
void               ArrowPuppy(int nArrow)                     // 캐릭터 방향 결정
void               checkmap(int nStage);                      // 현재 타일맵의 속성  체크
void               MoveChar(int nArrow);                      // 캐릭터의 이동 거리
int                checkclear(void);                           // 스테이지 완수 체크
int                SynchFrame(DWORD nDelayCount);            // 프레임 제어 부분
GLvoid            ReSizeGLScene(GLsizei width, GLsizei height);                       // Sizing Scene
unsigned char gAddObjectMeshFromFile(unsigned char nObjID, char* pNGOName);       // 3D 객체 정보 얻기
unsigned char gAddObjectAnimFromFile(unsigned char nObjID, char* pNBAName);       // 3D 애니메이션 정보 얻기

게임에 이용되는 데이터, 변수, 함수들을 모두 정의하였다. 이제부터는 본격적으로 내부를 들여다 보자.

메인 루프
스페이스큐브의 메인 루프는 상당히 간단하다. InitGame()를 통해서 각종 데이터를 초기화하고, 사용자가 종료키를 누를 때까지 무한루프를 돌면서 KeyProcess() 함수를 통해 사용자의 입력을 받아들이게 되고, 입력된 키 값은 UpdateGame() 함수에서 게임에 반영된다. 이를 G3_SurfaceFlip() 함수를 호출함으로써 LCD 화면에 출력하게 되는 간단한 구조를 지니고 있다. SynchFrame(150)은 150ms마다 사용자 키 입력을 받고, 이를 게임에 반영하는 함수이다. G3_Main() 함수는 <리스트 5>를 참고하기 바라며, 각 함수들에 대해서 자세히 살펴보자.

 <리스트 5> 메인 루프

게임 초기화
게임의 초기화 기능은 다음의 InitGame() 함수에서 처리해 준다. 말 그대로 초기화 함수에서는 PCM 사운드를 초기화하고, 3D 버퍼를 생성하며, 2D 텍스쳐 맵핑과 게임에 이용되는 각종 데이터를 생성하고 초기화한다. <리스트 6>을 보면 알겠지만, 수많은 데이터들을 초기화하여야 한다.

 <리스트 6> 게임 초기화

키보드 액션
다음으로 사용자의 키 입력을 받아서 처리하는 KeyProcess() 함수를 보자. 함수의 인자 값인 nGameState는 현재 게임의 상태에 대한 인자 값이다. nGameState에 따라서 게임의 상태를 변화시킬 수 있다. 만약 nGameState의 값이 CUBE_EXIT라면 게임 종료를 의미하며, 이에 따라 게임을 종료하게 된다.

게임은 CUBE_START, CUBE_CHOOSE, CUBE_EXIT, CUBE_CHOOSE, CUBE_MAIN의 네 가지 상태를 가질 수 있으며, CUBE_START는 게임의 첫 화면(intro)을 뜻하고, CUBE_CHOOSE는 캐릭터를 선택하는 상태, CUBE_MAIN이 실제 게임을 하고 있는 상태, CUBE_EXIT는 게임을 종료하는 상태를 의미하게 되고, 입력받은 키 값은 게임의 상태와 함께 게임에 반영된다.

2, 4, 5, 6, 8 키가 게임에 이용되는데, 2, 4, 6, 8는 방향키 역할을 하게 되고, 5는 확인키와 같은 역할을 한다. <리스트 7>의 KeyProcess() 함수를 보면 5, 6키에 대한 내용이 있는데 2, 4, 8키는 6키와 같은 구조로 이루어져 있기 때문에 포함하지 않았다. 처음 nGameState는 CUBE_START로 게임이 시작되고, G3_KeyGetEx() 함수를 통해서 실제로 사용자의 입력을 받게 된다. 입력받은 키가 6(->)이라면 현재 게임 상태(nGameState)를 체크하고 만약 CUBE_CHOOSE라면 현재 상태가 캐릭터를 선택하는 상태로 인식하게 돼 이를 대한 처리를 하게 된다. 만약 CUBE_MAIN이라면 게임이 실행되고 있는 상태이므로 캐릭터를 오른쪽으로 이동하고, 그에 따른 출력까지 처리하게 된다.

 <리스트 7> 게임의 키 이벤트 관리

그래픽 액션
그래픽 액션은 UpdateGame(int nGameState) 부분에 대한 설명이다. 앞서 설명한 nGameState를 Index로 구별하여 게임의 그래픽 전체를 나타내 준다. 메인 루프에 있었던 void UpdateGame(int nGameState) 부분에서 하는 역할은 다음과 같다. 자세한 내용은 <리스트 8>의 UpdateGame() 함수를 참고하기 바란다.

  • nGameState의 상태에 따라서 swtch문을 각각에 해당하는 case를 실행한다.
  • nGameState가 CUBE_EXIT일 경우에는 게임이 종료되므로 종료에 대한 처리를 해준다. 종료에 대한 처리는 화면을 검은색으로 지우고 ‘SpaceCube 종료!’라는 문자열을 화면에 출력해 준다.
  • nGameState가 CUBE_START일 경우에는 게임이 시작되는 상태이므로 게임 인트로 화면을 보여준다. 게임 인트로 화면에서 4번 키를 눌렀을 경우 start roll over 이미지를 뿌려주고, 6번 키를 눌렀을 경우에는 exit roll over 이미지를 뿌려준다.
  • nGameState가 CUBE_CHOOSE일 경우에는 배경을 클리어하고 타이틀 이미지를 뿌리고 사용할 캐릭터들을 뿌려준다.
  • NGameState가 CUBE_MAIN일 경우에는 3D 버퍼의 화면을 지우고 깊이 버퍼를 초기화한다. 화면에 뿌릴 스테이지 맵들의 face normal 좌표를 읽고 face에 입혀질 texture 좌표도 읽고 8*8 타일맵을 지정된 속성에 맞춰 텍스쳐를 바인딩하여 DrawElements로 3D 공간에 그려준다. 그려준 상태에서 키패드의 번호 1번 키를 누르면 상, 3번 키를 누르면 하, 2번 키를 누르면 우, 4번 키를 누르면 좌로 각각 이동한다.

  •  <리스트 8> 게임의 화면 갱신 부분을 관리

    실행 결과
    열심히 만들어온 게임의 화면을 보도록 하자. <화면 1>에서 <화면 6>은 각 상태 화면이다.

     
    <화면 1> CUBE_START 상태 화면 <화면 2> CUBE_EXIT 상태 화면

     
    <화면 3> CUBE_CHOOSE 상태 화면 <화면 4> CUBE_MAIN(Play) 상태 화면

     
    <화면 5> CUBE_MAIN(fall) 상태에서 캐릭터가 위험지역에 존재하고 있을 때의 화면 <화면 6> CUBE_MAIN(TimeOver) 상태에서 타임카운터가 다 되었을 때의 화면

    게임 제작은 영화 같은 예술
    지금까지 스페이스 큐브라는 비교적 간단한 게임을 구현한 소스코드를 가지고 설명을 해보았다. 사실, 게임이라고 하는 것은 사용자에게 여러 가지 만족감을 제공해야지만 좋은 게임이 된다. 눈에 보기 좋아야 하고, 재미 있기도 해야 하고, 귀도 즐거워야 한다. 이런 측면에서 게임 제작은 기술이라기보다는 영화와 같은 예술에 가깝다고 볼 수 있다. 기획, 디자인, 효과, 편집, 코딩, 배포 등 모바일 게임이 성공하기 위해서는 다양한 분야의 일손들이 필요하다. 특히 모바일 3D는 기존의 2D 게임 제작과는 판이하게 다른 개발 접근 방법을 요구하고 있다.

    따라서 모바일 3D 게임이 일반화되기 위해서는 더 많은 사람들이 제작에 참여하고 관련 인력들이 많이 만들어져야 가능하리라고 본다. 이를 위해서 오늘도 열심히 노력하는 분들에게 감사의 마음을 전하고 싶다. 그리고 그러한 노력 뒤에는 좋은 결과들이 나오기를 기대한다. @