스프라이트란 무엇인가? 하는 문제부터 보죠. 원래 게임그래픽은 타일 기반이었습니다. 타일 아시죠? 화장실 바닥에 붙어 있는 것들... 무슨 예술가들은 색깔 타일로 벽에다 그림도 그린다고 하는데... 그것처럼 각각의 그림이 그려진 타일을 가지고 게임의 배경을 그려냅니다. 640*480 또는 그보다 큰 그림을 잘게 잘게 부숴서 나중에 조합하게 됩니다. 이것은 용량상의 문제로써 하기 때문인데.. 요즘은 타일방식은 점점 사라지는 추세이고, 통짜맵이라고 속칭되는 방법을 사용하죠. 즉, 배경을 그림한장에 모두 그려 넣는 방식입니다. 타일방식보다 좋다면 좋고 나쁘다면 나쁠수 있는 방식이죠.. 뭐, 맵 구성에 대한 잡설은 이쯤에서 그만두고, 스프라이트에 대해서로 돌아가서... 타일로 그려놓은 위에 캐릭터같은 것을 올려놓기 마련입니다. 지난번 만든 슈팅에서도 캐릭터(비행기, 총알)이 있었으니 아실겁니다. 이러한 캐릭터는 쉽게 말하면, 타일위에 스티커를 붙인것이라고 생각하심 됩니다. 스티커를 붙인것인데, 스티커가 네모난것으로만 나온다면 어캐될까요? 모양에 따라서 쓸모없는 부분을 오려야죠? 하지만, 아쉽게도 컴퓨터에서는 네모난 그래픽을 쓸모없는 부분을 오려줄 수 있는 기능이 지원되지 않습니다. 그래서 만든 것이 스프라이트라고 하는데... (외국놈들은 무슨 요정(?) 이라고 부른다죠? --;) 쓸모없는 부분을 투명색(?)으로 처리한겁니다. 투명색이라함은 비닐테이프같은거죠. 실제로는 있는데, 눈으로 보기에는 안보인다는 겁니다. 투명색은 어떤색이라도 지정이 가능하고요... 8비트에서는 팔렛인덱스중 한 값, 그이상의 비트에서는 RGB해당값(즉 그 수많은 색 중에 한가지 색을 쓰지 못한다는 거죠.)입니다. 아궁~ 그림없이 말로만 설명하기 힘드네용 --; 하여간 한마디로 요약하자면, 스프라이트는 투명색이 있는 네모난 그림입니다. 뭐, 요즘들어서는 타일도 투명색이 있지만요.. 둘다 투명색이 있을경우엔, 스프라이트는 크기가 자유롭지만, 타일의 크기는 고정되어있다고 생각하시면 됩니다. 화장실에 가서도 크기가 제각각인 타일을 본 적은 없으실 겁니다. 뭐 대강 설명하면 이렇다는 겁니다. --;
a. 스프라이트 에디터의 포맷선택
지난번에 슈팅게임을 만들어 보았습니다. 그림은 그냥 256색 bmp화일을 이용했지요. 흐음.. 실제적으로 게임을 만들 때, 공용으로 쓰이는 포맷을 쓰지는 않습니다. 물론 쓸경우도 있지만, 안쓰는 경우가 많습니다. 이유는... 뭐 간단합니다. 관리하기 편하게 하기 위해서죠.
슈팅게임에서는 모두 4개의 이미지만을 사용했습니다. 하지만, 폭발도 비행기도 하나이고, 비행기가 애니메이션 되지도 않습니다. 만약, 이런 모든 그림이 다 들어간다면? 그리고 배경까지 몇가지 추가된다면 어떻게 될까요? 파일의 개수가 점점 늘어나겠죠? 흐음.. 10~20개 까지는 뭐 그렇다고 칩시다. 그런데, 만약 RPG를 만든다고 할 때, 만약 이런식으로 모두 하나씩 다른 이미지 파일로 저장한다면, 걷는 그림이 모두 4프레임이고, 방향별로 있다면 한사람당 그림은 16장. 즉 이미지 파일이 16개가 필요합니다. 한명만 나오는 것도 아니고, 적게는 수십명, 많게는 수백명이 나오는 게임에서 이런식으로 사용하면.... 데이터 관리에 헛점이 노출되고 맙니다. 거기다가, 이런 그림들이 모두 파일이름이 틀려야 하고, 어떤 법칙성으로 파일이름을 붙인다고 해도, 그 많은 그림들을 다 로드하는 루틴을 만든다면...
후~ 생각만해도 끔찍합니다. 그래서 조금이라도 자동화 될수 있는 방법을 찾아야 합니다. 간단하게는 index.txt같은 텍스트화일에 사용되는 bmp화일의 이름을 저장한 후에 그것을 읽어서 순차적으로 bmp화일을 읽어도 됩니다. 가장 초보적이고 간단한 방법이죠. 다른 에디터도 필요없고..^^ 즉 ---------------- input.txt-------------------- a-up01.bmp a-up02.bmp a-up03.bmp a-up04.bmp a-up05.bmp ----------------------------------------------- 이런식으로 저장한 후에 읽어들이면 됩니다. 하지만, 이런 경우 큰단점이 있는 것이, 데이터의 해킹우려입니다. 플레이어가 데이터를 잘못 해킹하여, 데이터에 손실을 줄경우가 발생할 수 있기 때문입니다. 그리고, 역시 파일의 개수자체가 오히려 늘어나기 때문에 관리하기 힘들어지고 맙니다. 그런연고로 스프라이트는 한캐릭터당 하나씩의 파일을 생성하는 것이 거의 모든 게임에서 법칙처럼 사용되고 있습니다. 물론 이런 파일들을 최종 마스터시에 한화일로 만드는 등의 수고를 한 게임이 바로 스타크레프트나 디아블로, 포가튼 사가같은 게임들입니다. 그래서 스프라이트는 1열구조로 된 이미지그림들의 연속으로 만들어야 합니다. 조금 개선된 그래픽 엔진을 사용하는 곳은 더블 링크드 리스트 즉, map구조를 사용하고 있는 곳도 있습니다. (가람과바람에서는 map구조를 사용합니다.) 하지만, map구조는 사용하기는 편하지만, 구조가 복잡하여 만들기 어려운 점이 있습니다. 여기서는 링크드리스트를 사용한 1열구조만 사용하겠습니다. 또한, 스프라이트의 특성상 몇가지 추가정보가 들어가야 합니다. 이것은 저장해도 되고, 아니면, 저장된 이미지에서 추출해도 됩니다. 흠흠. 뭉쳐서 만드는 이유를 아셨겠죠? ^^ (안 뭉쳐도 됩니다. 하지만, 만들기 편할려고 뭉치는 거죠.)
b. 그럼 어떻게 뭉치지?
간단하게 만들어보려면 여러 가지 방법이 있습니다. 진짜 프로그램에서 사용하는 방법대로 실제로 이미지를 잘라서 자체포맷으로 저장하는 방법. 두 번째로는 이미지를 잘라서 저장하되, 호환성을 유지할 수 있는 dib나 bmp포맷을 이용하여 여러개의 bmp화일을 하나의 파일에 저장하는 방법. 세 번째로, 이미지화일은 그대로 두고, 이미지파일의 이름정보와 이미지화일의 자름정보, 즉, 사각영역을 순차적으로 가지고 있어서, 이 이미지 파일 하나로 여러개의 스프라이트를 만들어내는 방법. 첫 번째 방법은 일반 도스용 게임에서 사용되던 방법으로 그후 지금도 여러 가지 게임에서사용하는 방법입니다. (왜 게임프로그래머들은 자체포맷을 그리도 좋아하는징.. --;) 두 번째 방법은... 그 예를 많이 찾아볼 수는 없는데... 폭스베어나 창세기전3에서 조금 사용했던 것으로 알고 있습니다. (중간 비쥬얼 부분에서요..) 세 번째 방법은 Direct-Draw예제나 몇몇 일본게임에서 찾아 볼 수 있는 방법입니다.
제가 하려는 방법은, 두 번째 방법을 이용하겠습니다. bmp파일을 이용하여, 모든 해상도와 모든 칼라수에 맞출수 있는 범용적인 파일포맷을 만들기로 하겠습니다.
c. 실제적인 파일 포맷
bmp화일을 이용하는 것으로 하였는데, 기본적으로 bmp화일을 읽을 줄 알아야 뭐가 되겠죠? 지난번엔 LoadBmp란 윈도API를 사용해서 잘 넘어갔지만, 이번엔 직접 읽을 수 있는 루틴을 만들어야 겠습니다. bmp화일을 보면, 몇가지 헤더로 이루어 져있습니다. windows에서는 윈도기본 입출력 그래픽 포맷이기 때문에 파일의 구조체 자체를 지원해 주고 있습니다. 그 기본형을 토대로 보면 구조는 다음과 같습니다.
그럼 읽는 루틴을 만들어 봅시다.^^ 다음은 CDib객체 함수의 일부입니다. C++의 특징인 함수 다형성을 이용한겁니다.
fp = fopen( lpszPathName, "rb" ); if(fp==NULL)return; BITMAPFILEHEADERm_bmFileHeader; 파일헤더입니다. 읽기는 하지만, 읽은 파일이 bmp화일인지 아닌지를 확인하는 목적외에는 쓰이지 않는 구조체입니다. fread(&m_bmFileHeader, 1, sizeof(m_bmFileHeader) , fp);
if( m_bmFileHeader.bfType != 0x4d42 ) { bmp 파일이 맞지 않으면 내부 변수를 초기화 시켜줍니다. 즉, 아무것도 읽지 않습니다. Init( ); //..... ? } else { bmp이라면 읽어야죠.^^ BitmapLoad(fp); } fclose(fp); }
함수이름은 똑같은데, 인자가 다르네요.. C++문법에선 이래도 다른함수로 인정한답니다. 참, 편한 세상이죠. 파일 포인터를 받아서 그 내용을 읽는 루틴입니다. 함수를 두가지로 나눈 이유는 나중에 자체포맷(?)을 위해서입니다. void CDib::BitmapLoad(FILE* fp) { DWORD InfoHeader = sizeof(BITMAPINFOHEADER); m_pBmInfoHeader = (LPBITMAPINFOHEADER ) new BYTE [InfoHeader]; 비트맵 인포헤더의 메모리를 할당합니다. 실제 비트맵그림의 모든 정보를 저장하는 구조체로 뜯어보면, 이미지의 가로,세로 크기 이미지의 픽셀당 비트수 사용된 칼라수 등을 저장하고 있습니다.
fread( m_pBmInfoHeader, InfoHeader,1, fp );
int NumColor = GetBmColor(); 몇 개의 색상이 쓰였는지를 받아내는 함수입니다.
if (m_pBmInfoHeader->biClrUsed == 0) m_pBmInfoHeader->biClrUsed = NumColor;
m_pBmInfoHeader->biSizeImage = GetBmImageSize();
// m_pBmInfoHeader->biBitCount 가 16보다 작으면. .256 색상 컬러를 가지고 있다. 즉, 256일때는 팔렛테이블을 읽어야 한다는 말이되죵. if(m_pBmInfoHeader->biBitCount < 16) { DWORD cirTableSize = NumColor * sizeof(RGBQUAD); m_pRGBTable = (RGBQUAD*) new BYTE[cirTableSize]; fread(m_pRGBTable, cirTableSize,1, fp);
// 팔레트 셋팅.. BYTE* Info = (BYTE*)m_pBmInfo+sizeof(BITMAPINFOHEADER); memcpy(Info, m_pRGBTable, cirTableSize);
이제, bmpinfoheader와 팔렛테이블을 합친 새로운 구조체인 bitmapinfo를 만듭니다. 실제 draw 루틴에서는 이 구조체가 쓰이죠. --; CreateBmPalette(); } else { m_pRGBTable = NULL; DWORD InfoSize = sizeof(BITMAPINFO); m_pBmInfo = (LPBITMAPINFO ) new BYTE [InfoSize]; memset(m_pBmInfo, 0, sizeof(BITMAPINFO)); memcpy(m_pBmInfo, m_pBmInfoHeader, sizeof(BITMAPINFOHEADER)); 팔렛이 없을때는 그냥 bmpinfoheader에 있는 내용만을 복사합니다. }
DWORD ImageSize = m_pBmInfoHeader->biSizeImage; 그림의 크기를 받아내서 m_pDibBits = new BYTE[ImageSize]; 그만큼의 메모리를 할당합니다. fread(m_pDibBits, ImageSize ,1, fp);
}
읽은 방법을 대강 알았으면 찍는 방법도 알아봅시다. void CDib::Draw(HDC hDC, int StartX, int StartY) { StretchDIBits( hDC, StartX, StartY, GetWidth(), GetHeight(), 0, 0, GetWidth(), GetHeight(), m_pDibBits, m_pBmInfo, DIB_RGB_COLORS ,SRCCOPY); }
흐음.. 찍습니다. 필요한 정보는 무엇 무엇일까요? 가로,세로크기, 비트맵데이터, 그리고 bmpinfoheader와 팔렛테이블로 이루어진 bmpinfo구조체입니다. 그럼, 무엇을 저장해야 되는지 알겠죠? bmpfileheader를 제외한 모든 것을 읽은 그대로 저장하면 되는겁니다.
그~냥 저장해 버립니다.^^ 저장할 때, 파일 이름이 인자가 아니고 파일포인터인 이유는 무엇일까요? 앞에서 말했듯이 bmp파일 여러개를 하나의 파일로 저장하고자 하는 이유 때문입니다. 그래서 CSprite구조체에서 이 함수를 이용해서 저장하는 겁니다. 이 방법을 사용하기 때문에 위의 읽는 함수가 둘로 나뉜겁니다. 저장하는 영역과 읽는 영역이 같도록 하기 위해서죠. 실제 스프라이트 구조는 이 저장루틴을 그림의 개수만큼 루프돌려 주면 됩니다. 하지만, 몇가지 추가정보가 있겠죠. 그래서 추가정보를 저장할 수 있도록 CSprite객체에서 스프라이트화일 로드와 세이브를 담당합니다.
그럼 CDib객체의 구성을 잠깐 살펴보고, Sprite객체가 어떻게 바뀌었는지 알아봅시다.
class CDib { public: BYTE*m_pDibBits; // 비트맵의 이미지가 저장되어 있는 주소를 가지고 있다. HPALETTEm_pPalette; // 비트맵의 팔레트를 가지고 있다. 16Bit 이상이면 NULL RGBQUAD*m_pRGBTable; // RGB-Talbe 주소를 가지고 있다. 16Bit 이상이면 NULL LPBITMAPINFOm_pBmInfo; // Bitmap Info 주소를 가지고 있다. LPBITMAPINFOHEADERm_pBmInfoHeader; // Bitmap Info Header
public: voidInit(); // 클래스 초기화 변수.. voidBitmapLoad(LPCTSTR lpszPathName); // 비트맵을 로드하는 함수. voidBitmapLoad(FILE* fp); // 비트맵을 로드하는 함수. voidBitmapSave(FILE* fp); // 비트맵을 로드하는 함수. intGetWidth(); intGetHeight(); // 비트맵의 높이. UINTGetBmColor(); // 비트맵의 컬러수. WORDGetBitCount(); // 비트맵을 이루고 있는 비트수를 구한다. LONGGetBytePerLine(); // 비트맵의 한 라인을 구하는 함수. BOOLCreateBmPalette(); // 팔레트를 생성한다. BYTE*GetDibBits(); // 비트맵의 이미지 주소를 리턴한다. DWORD GetBmImageSize(); // Bitmap의 이미지 사이즈를 알아낸다. HPALETTEGetPalette(); // 팔레트 주소를 가져온다. RGBQUAD*GetRGBTable(); LPBITMAPINFOGetBmInfo();// Bitmap Info 값을 리턴한다.
voidDraw(HDC hDC, int StartX, int StartY, int ImageSizeX, int ImageSizeY);// 화면에 비트맵을 출력한다. voidDraw(HDC hDC, int StartX, int StartY);
BOOLImageCreate( CDib *pDib, RECT rect ); // dib객체에서 이미지를 사각영역만큼만 잡아낸다. 위의 함수는 장차 만들어질 다른 에디터인 타일에디터를 위해 만들어 진 겁니다.^^
꽤 필요없는 함수와 구조체가 보입니다.^^ 주석문만 보시면 이해가 절로 되겠죠? 그럼 바뀐 Sprite객체의 모습을 보겠습니다.
class Sprite { public: int m_XSize, m_YSize; LPDIRECTDRAWSURFACE m_lpDS;
// 리스토어를 위해 DWORD m_ColorKeyL; DWORD m_ColorKeyH; 여기서부터가 추가된 세 개의 변수입니다. int m_AbsX; int m_AbsY; 절대위치를 위해서 추가된 좌표변수입니다. 스프라이트내에서 그림의 좌표를 조금 바꾸고 싶을 때, 이 변수를 변화시키면, 움직일수있도록 하는 변수로, 이미지들의 중심점을 맞출 때 사용됩니다.
CDib* m_pDib;
그림의 원본인 Dib객체입니다. bmp화일을 읽는 것을 담당하죠. 실제로 저장, 로드되는 부분입니다.
// 링크드 리스트용. Sprite* m_Prev; Sprite* m_Next;
public: Sprite(); ~Sprite(); HRESULT PutSprite (LPDIRECTDRAWSURFACE lpSurface, int x, int y); HRESULT PutSpriteEx(LPDIRECTDRAWSURFACE lpSurface, int x, int y); HRESULT PutTile (LPDIRECTDRAWSURFACE lpSurface, int x, int y); HRESULT PutTileEx(LPDIRECTDRAWSURFACE lpSurface, int x, int y); void DelSurface();
};
m_AbsX, m_AbsY가 추가됨으로써 PutSprite함수가 조금 변화하였습니다.
HRESULT Sprite::PutSprite(LPDIRECTDRAWSURFACE lpSurface, int x, int y) { x += m_AbsX; y += m_AbsY; 여기가 추가된 부분입니다. 입력된 x,y에 추가로 m_AbsX와 m_AbsY를 더해줌으로써, 중심점을 맞출수 있게 해 줍니다. // // 클리핑 처리가 없는 스프라이트 함수 HRESULT ddrval; ddrval = lpSurface->BltFast(x , y , m_lpDS, NULL, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT); return ddrval; }
중심점을 맞추는 이유는 간단합니다. 몇장의 그림으로 이루어진 애니메이션이 이루어져야 할 경우. 그림마다 크기가 다르다면, 전부 0, 0 좌표에 출력했을 때, 자연스럽게 애니매이션이 되는 것이 아니라, 좌우 또는 상하로 그림이 흔들려 보일 경우가 있습니다. 실제로 게임 제작시 자주 접하는 부분입니다. 그림의 용량을 줄이기 위해 공백을 최대한 줄이게 되면, 그림들의 애니매이션의 중심점이 이상해 지는 경우죠. 그런경우를 위해서 중심점 변경루틴을 삽입하고, 이 중심점이 변경되면, 출력좌표또한 변경되도록 만드는 겁니다.
한마디로, 애니메이션이 부드럽게 보이도록 하는 이유죠. 또 한가지 이유가 있다면, RPG게임을 만들 때, 맵에 찍히는 각각의 오브젝트는 중심을 가운데 아래에 가지고 있도록 합니다. 그렇게 되면, 높이가 높은 오브젝트의 경우에도 중심점을 기본으로 충돌처리를 할 수 있게 되고, 오브젝트끼리의 가림처리가 손쉽게 이루어집니다. 자세한 내용은 후에 RPG제작시에 알려드리도록 하고.. 이번엔 여기까지... --;
하여간, 이렇게 바뀌어 버렸으면, CSprite객체또한 좀 바뀌었겠죠?
아래는 바뀐 함수 또는 추가된 함수들입니다.
Spr화일을 읽어들이는 함수입니다. void CSprite::LoadSpr(char* fname) { FILE* fp; fp = fopen(fname, "rb"); if(fp==NULL)return; DWORD CKL, CKH; int absx, absy; int size; fread(&size, sizeof(int), 1, fp); 전체 스프라이트의 개수를 읽습니다. for(int i=0; i { fread(&CKL, sizeof(DWORD), 1, fp); fread(&CKH, sizeof(DWORD), 1, fp); 칼라키를 읽습니다. fread(&absx, sizeof(int), 1, fp); fread(&absy, sizeof(int), 1, fp); 상대좌표를 읽습니다. LoadBmp(NULL, fp, CKL, CKH); bmpfileheader를 제외한 bmp화일의 정보와 비트맵이미지 데이터를 읽습니다. 그리고 다이렉트드로우 서피스를 생성합니다. m_SprTail->m_AbsX = absx; m_SprTail->m_AbsY = absy; 만들어진 Sprite객체에 읽어들인 상대좌표를 대입합니다. } fclose(fp); } 추가된 정보인 상대좌표와 칼라키는 반드시 저장해야 하는 정보입니다. 그래서 저장하게 되죠.^^ 그림의 크기는 bmp화일에 저장이 되기 때문에 중복 저장을 하지 않습니다.
스프라이트가 몇 개인지 계산하는 함수입니다. int CSprite::GetSize() { Sprite* Spr = m_SprHead; int size = 0; if(Spr) { do{
실제 게임루틴에서는 파일을 읽고나서 Sprite->m_Dib객체는 delete하는 것이 정석입니다. 그래야만 메모리가 절약되죠. 보통 이미지데이터가 적게는 4메가, 많게는 20메가 가까이 사용되는 게임이 많은데 이것을 계속 메모리에 저장해 놓으면, 메모리낭비가 되겠죠. 물론 이 Dib정보는 DirectDraw Surface가 Lost되었을 때, 이것을 Restore 시키기 위해 사용될 수 있습니다만, 대부분의 게임은 이럴 경우 파일들을 다시 읽는 방법을 택하고 있습니다. 에러시에 조금 느려지는 것이 전반적으로 메모리를 많이 사용하는 것보다는 좋다는 생각이지요.
하지만, 여기서는 에디터를 만드는 것이기 때문에 Dib객체를 지우면 절대 안됩니다. 저장할 것이 없어져 버리니까요^^
바뀐 리스토어 함수입니다. 처음부터 다시 읽어버리는 놀라운 모습을 볼 수 있죠? ^^ void CSprite::Restore() { m_pDDraw->Restore(); Sprite* Spr = m_SprHead; FILE* fp; fp = fopen(m_FileName, "rb"); if(fp==NULL)return; DWORD CKL, CKH; int absx, absy;
뭐 간단합니다.^^ text화일을 이용한 간단한 스프라이트 편집기 군요.^^ 이번에 만드는 스프라이트 편집기는 자르는 기능을 만들지 않았습니다. 범용적인 bmp화일을 사용하기 때문에 다른 좋은 그래픽툴을 사용해서 그림의 크기를 조절할 수 있기 때문이죠. 그림크기를 적절히 한다면, 중심점을 수정할 필요따윈 없어지고요.
하지만, 일단 윈도우로 넘어왔는데, 좀더 윈도우 답게 만들 수 는 없을까요? 그렇다면 만들어 봅시다.^^
d. 통합 에디터를 향한 한걸음.^^ 강좌의 궁극적인 목표는 서로 연계되는 데이터를 생성할 수 있는 에디터를 한화일에 모아보자~ 라는 겁니다. 뭐, 간단한겁니다. 진보된 에디터처럼 ole를 사용하거나 하지는 않죠. 다만, 에디터에 Dialog Box를 많이 쓰기 때문에 이런식이 되는겁니다. Dialog Box 기반 프로그램은 순수 Api로만 작성하기엔 좀 까다로운 점이 있기 때문이죠. 일단, 기본이 되는 윈도우를 띄워봅시다. 그리고 기본윈도우에서 메뉴를 선택하면 각각의 기능을 가진 다이얼로그가 떠서 에디트를 하는 구조죠. 그러기 위해서 간단한 객체를 만들어 봤습니다. CWinApp라는 객체입니다.^^
void CWinApp::WinGetFileName(char* FullDirectory, int Strlength) { char* pdest; int result; pdest = strrchr( FullDirectory, '\\' ); result = pdest - FullDirectory + 1; if(result<0)return; char* temp = new char[Strlength-result]; memcpy(temp, FullDirectory+result, Strlength-result); memset(FullDirectory, 0, Strlength); memcpy(FullDirectory, temp, Strlength-result); 파일 다이얼로그에서 읽힌 파일이름은 디렉토리이름까지 전부 있습니다. 그것에서 파일이름만을 뽑아내는 함수입니다. }
void CWinApp::WinGetDirectory(char* FullDirectory, int Strlength) { char* pdest; int result; pdest = strrchr( FullDirectory, '\\' ); result = pdest - FullDirectory + 1; if(result<0)return; memset(FullDirectory+result, 0, Strlength-result); 위와 같은 이유로 디렉토리명만 뽑아내는 함수입니다. }
bool CWinApp::WinIsFile(char* name) { FILE* Fp; Fp = fopen(name, "rb"); if(Fp==NULL)return false; fclose(Fp); return true; 해당 파일이 존재하는 지 여부를 판정하는 함수입니다. }
유틸리티함수들로 이루어진 CWinApp는 보통 다른 에디트 윈도우의 기본객체로 사용되어, 다른 에디터객체들은 이놈을 상속받아서 만들곤 합니다. 상속이란 개념이 어색하실지도 모르지만, 상속을 하게 되면 여러모로 편리한 점이 많습니다. 내부에 객체로 선언하여 사용해도 되지만, 여러 가지 객체를 중복 상속받을 수도 있으니 여러개의 객체내에 있는 함수와 변수를 사용가능합니다. 즉, A기능을 가진 A객체와 B기능을 가진 B객체가 있다고 할때, 두 객체를 상속받으면 A기능과 B기능을 할 수 있는 C객체가 만들어 집니다. 코드가 굉장히 절약되죠. 하지만, 귀찮으면 안쓰면 그만입니다.^^
하여간 DDraw예제에서 변화된 내요을 위주로 WinMain에서 변화된 부분만 보면, 다음의 윈도 생성부분입니다. CWinApp를 사용했습니다.
지난번에 그 길던 윈도 생성부분이 이렇게 바뀌어 버렷습니다. 감격스럽게 짧아지긴했는데.. 글쎄.. 쓰기 편한것만은 아니죠? 하여간, 이루틴까지 삽입하고 나면, 드디어 에디터용 윈도우가 뜹니다. 좀 이상하게 생겼을 겁니다. 일반 윈도우 같지 않고, 무슨 트레이바처럼 생긴 것이 화면 제일 위에 생길테니까요..^^
하여간, Resource를 추가합니다. VC에 보면 Resource를 추가하는 메뉴가 있을겁니다. IDR_MENU1이란 이름으로 메뉴를 추가합니다. 그리고, 메뉴에 Sprite란 이름을 가지고 IDC_SPRITE란 ID를 가진 메뉴를 넣습니다. 그러면 앞으로 만들 스프라이트 에디터는 메인트레이윈도(?)의 Sprite를 누르면 뜨게 되는 겁니다.
후~ 에디터를 만드는 부분은 단순히 코딩만이 아니라, VC에서 에디트도 좀 해야하기 때문에 이것저것 귀찮군요. --; 하여간 그림을 최대한 적게 쓰면서 써볼랍니다.
e. 스프라이트 에디터 만들기. 메인 윈도우를 띄웠으면, 이젠 스프라이트 에디터용 윈도우를 띄워봅시다. 스프라이트에디터는 다이얼로그박스 기반으로 만들겁니다.^^
완성된 형태는 이렇게 될겁니다. 여러분도 리소스 에디터를 사용해서 위와같이 만들어 보시길 바랍니다. 버튼의 ID를 알려드리자면... NEW 버튼은 IDC_NEW란 ID를 사용합니다. LOAD 버튼은 IDC_LOAD란 ID를 사용합니다. SAVE 버튼은 IDC_SAVE란 ID를 SAVE AS 버튼은 IDC_SAVEAS ID BITMAP 버튼은 IDC_LOADBITMAP PREV 버튼은 IDC_PREV NEXT 버튼은 IDC_NEXT 네방향의 화살표 버튼은 각각 IDC_UP, IDC_DOWN, IDC_LEFT, IDC_RIGHT 라는 ID를 가집니다. 이렇게 버튼을 다 만들었으면, 왼쪽에 그림을 찍힐 부분은 640*480정도 되도록 만드세요. 뭐 저야 수십번 만들어봐서, 눈대중으로 맞추면 대강 맞습니다. --;
버튼이 눌리면 어떻게 되느냐? 하면 아래 프로시저 함수가 불리워집니다. 그래서 해당 ID아래에 있는 명령을 실행시키죠. 1대1 대응이 되므로 아주 단순한 구조로 프로그래밍이 되어버리고 맙니다. 확실히 에디터 프로그래밍은 알고리즘이나 대단한 로직을 공부하는 것이 아니라, 데이터 포맷에 대한 고찰과 일종의 윈도용 유틸리티 제작의 스킬 연마입니다. 많이 만들어보면 볼수록 인터페이스도 편리하게 되고 보기에도 좋은 툴이 나오는거죠.
BOOL CALLBACK SprEditProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch(iMsg) { case WM_INITDIALOG: return TRUE; case WM_PAINT: pSprEdit->OnPaint(); break; case WM_COMMAND:
switch(LOWORD(wParam)) { case IDC_NEW: pSprEdit->OnNew(); break; case IDC_LOAD: pSprEdit->xxOnLoad(); break; case IDC_SAVE: pSprEdit->OnSave(); break; case IDC_SAVEAS: pSprEdit->OnSaveAs(); break; case IDC_LOADBITMAP: pSprEdit->xxOnLoadBitmap(); break; case IDC_PREV: pSprEdit->OnPrev(); break; case IDC_NEXT: pSprEdit->OnNext(); break; case IDC_UP: pSprEdit->xxOnMove(0, -1); break; case IDC_DOWN: pSprEdit->xxOnMove(0, 1); break; case IDC_LEFT: pSprEdit->xxOnMove(-1, 0); break; case IDC_RIGHT: pSprEdit->xxOnMove(1, 0); break; } break;
겉모양을 다 만들었으니, 이제 안쪽을 만들어 볼 차례입니다. 윈도우용이므로, 스프라이트를화면에 출력해야 합니다. 하지만, 지난번에 만든 DirectDraw객체는 창모드를 지원하지 않습니다. 그래서, 지난번의 강좌내용을 참고해서 창모드용 초기화 함수를 만들었습니다.
BOOL CDirectDraw::DirectDrawInit(HWND hWindow, int xsize, int ysize) { 역시 함수의 다형성을 이용해서 같은 이름으로 만들어 버렸습니다. m_hWnd = hWindow; m_MaxX = xsize; m_MaxY = ysize; m_bFullScreen = false; 몇가지 변수가 추가됐죠? 풀스크린모드인지를 판별하는 변수입니다.
// DirectDraw 객체 생성 ddrval=::DirectDrawCreate(NULL, &m_lpDD, NULL); if (ddrval != DD_OK) return DirectDrawError("드로우 객체 생성 실패");
// ddrval=m_lpDD->SetCooperativeLevel(m_hWnd, DDSCL_NORMAL); 노멀모드, 즉 윈도우 모드로 레벨을 정합니다.
// 하나의 2차표면을 갖는 1차표면을 생성 memset(&ddsd, 0, sizeof(ddsd)); ddsd.dwSize =sizeof(ddsd); ddsd.dwFlags=DDSD_CAPS ; ddsd.ddsCaps.dwCaps =DDSCAPS_PRIMARYSURFACE;
// 1차표면을 생성한다 ddrval=m_lpDD->CreateSurface(&ddsd, &m_lpDDSPrimary, NULL); if (ddrval != DD_OK) return DirectDrawError("1차 표면 생성 실패"); 1차표면은 그냥 만듭니다. 대신 플립이나 복합화면같은 인자는 집어넣지않습니다.
ddsd.dwWidth = m_MaxX; ddsd.dwHeight = m_MaxY; ddrval = m_lpDD->CreateSurface(&ddsd, &m_lpDDSBack, NULL); if(ddrval!= DD_OK)return DirectDrawError("2차 표면 생성 실패"); 2차화면은 풀화면모드와는 달리 오프스크린으로 생성합니다. 뭐 에디트만을 위해서는 꼭 필요하진 않지만, 정신건강상 만드는 것이 도움이 될겁니다. 프라이머리 서피스는 생성될때의 윈도핸들을 가진 윈도의 표면전체를 대상으로 생성됩니다. 그러니깐, 일부분에만 그리지를 못하죠. 검은색으로 화면을 깨끗하게 만들라고 하면... 다이얼로그 윈도 전체가 까맣게 되어버리고 맙니다. 그래서 일부만 사용하고 싶으면 2차화면에다가 그리는 버릇을 들여야죠 뭐.
ddrval = m_lpDD -> CreateClipper (0, &m_lpDDClipper, NULL); if(ddrval!= DD_OK)return DirectDrawError("클리핑 생성 실패"); ddrval = m_lpDDClipper -> SetHWnd(0, m_hWnd); if(ddrval!= DD_OK)return DirectDrawError("클리핑 생성 실패"); ddrval = m_lpDDSPrimary -> SetClipper(m_lpDDClipper); if(ddrval!= DD_OK)return DirectDrawError("클리핑 생성 실패"); 클리퍼 객체를 만듭니다. 이놈도 창모드를 위해서 추가된 객체입니다. 사용법은 지난번 강좌에서 잘 배우셨으리라 믿습니다.
화면을 Flip하는 것이 아니라 x,y 위치에 Blt하게 만들었습니다. 왜 x,y에다 찍으라고 했느냐,. 하면, 에디터에서는 화면에 찍히는 곳이 꼭 0,0이 아니기 때문입니다.
준비는 다 끝난 것 같군요. 리소스 에디터에서 다이얼로그박스를 만들엇으면, 이름을 바꿉니다. IDD_SPRITE로 바꾸세요.^^
그럼, 바뀐 WndProc 함수를 보면...
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch (iMsg) { case WM_CREATE : return 0 ; case WM_COMMAND: switch(LOWORD(wParam)) { case IDC_SPRITE: if(pSprEdit)return 0; pSprEdit = new CSpriteEdit; pSprEdit->Init(pMainWin->m_hInstance, hwnd, SprEditProc); IDC_SPRITE란 ID를 가진 메뉴를 클릭하면, 스프라이트 에디터가 작동합니다. 스프라이트에디터 객체는 CWinApp를 상속받았습니다. 그래서 이런식으로 생성되는 거죠. } break; /* case WM_KEYDOWN: if(wParam == VK_ESCAPE) SendMessage(hwnd, WM_CLOSE, 0, 0); return 0; */ case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; }
그럼 스프라이트 에디터의 구성을 알아봅시다~
class CSpriteEdit : public CWinApp 앞에서 말한대로 CWinApp를 상속받았습니다. { public: CSpriteEdit(); virtual ~CSpriteEdit();
protected: CDirectDraw*m_pDDraw; 창모드 편집을 위해 DirecrDraw를 사용합니다. CSprite*m_pSprite; 실제 데이터입니다. Sprite*m_pSpr; 현재 보여지고 있는 sprite입니다. //intm_EditNum; char m_Name[256]; 파일 이름이겠죠?
public: void Init(HINSTANCE hinst, HWND hwnd, DLGPROC wProc); 윈도를 만드는 초기화 함수입니다. void OnPaint(); 화면에 스프라이트를 출력합니다.
여기부터 아래는 버튼이름과 같은 함수들.. 버튼과 1대1대응합니다. 아까 프로시져 함수를 봤으니 이해하실 겁니다. void OnNew(); void xxOnLoad(); void OnSave(); void OnSaveAs(); void xxOnLoadBitmap(); void OnPrev(); void OnNext();
화살표 버튼을 누르면 호출됩니다. 상대좌표를 움직이는함수입니다. void xxOnMove(int xpos, int ypos);
};
만들고 나니 무슨 MFC 같아 지긴 했는데...^^ MFC보단 쓰기 힘들지도 모릅니다. 하하.
화면에 스프라이트를 그려주는 함수입니다. WM_PAINT 메시지가 넘어왔을때와 그림을 새로불럿을 때 같은때 호출합니다. void CSpriteEdit::OnPaint() { m_pDDraw->FillSurface(m_pDDraw->GetSurfaceBack(), 0); 먼저 2차화면을 깨끗이 지웁니다. 안그러면 잡티가 남게되죠. if(m_pSpr) { m_pSpr->PutSpriteEx(m_pDDraw->GetSurfaceBack(), 0, 0); 640,480화면보다 그림을 클경우를 대비해서 클리핑지원함수로 찍습니다. } m_pDDraw->FlipSurfaces(10, 10); 10,10좌표에 2차화면을 blt 시킵니다. 그렇게 하면 위의 캡쳐된 화면처럼 나옵니다. }
공용대화상자를 이용한 파일 열기입니다. void CSpriteEdit::xxOnLoad() { if(WinOpenFile(m_Name,"스프라이트 화일 *.spr\0;*.spr\0","Spr를 읽기")) { true가 리턴되면 파일열기가 성공한 것이고, false가 리턴되면, 파일을 열다가 취소한 경우입니다. if(m_pSprite) delete m_pSprite; 만약 이전에 에디트하고 있는 파일이 있으면 메모리에서 지웁니다. m_pSprite = new CSprite(m_pDDraw); m_pSprite->LoadSpr(m_Name); 새로 CSprite를 생성해서 파일을 읽어들입니다. m_pSpr = m_pSprite->m_SprHead; 스프라이트의 제일 앞 Header를 화면에 찍습니다. } OnPaint(); }
bmp화일을 읽습니다. bmp화일을 순차적으로 읽은후에 저장하는 것이 본 에디터의 본 목적이니까요.^^ 가장 핵심적인 부분입니다. void CSpriteEdit::xxOnLoadBitmap() { char Name[256]; wsprintf(Name,"untitled.bmp"); if(!m_pSprite) { m_pSprite = new CSprite(m_pDDraw); 아무것도 읽은 적이 없다면 새로 CSprite를 생성합니다. } if(WinOpenFile(Name,"BMP 화일 *.bmp\0;*.bmp\0","BMP를 읽기")) { m_pSprite->LoadBmp(NULL, Name, 0, 0); 파일을 읽어서 제일뒤에 추가합니다. m_pSpr = m_pSprite->m_SprTail; 현재 읽은 스프라이트를 출력합니다. } OnPaint(); }
스프라이트를 에디트하기 위해 앞,뒤로 돌려보는 함수입니다.
void CSpriteEdit::OnPrev() { if(m_pSprite) { if(m_pSpr->m_Prev) m_pSpr = m_pSpr->m_Prev; 그냥 포인터를 옮기는 겁니다. Next도 마찬가지죠. } OnPaint(); }
현재까지 만들어진 스프라이트 에디터는 매우 기능이 미약합니다. 기본적인 파일을 생성하는 기능밖에 없다고 해도 과언이 아니죠. 앞으로도 추가할 기능이 많이 남았겠죠? 단지, bmp화일을 읽어서 추가하기만 한다면 윈도우용 에디터가 아깝습니다.^^ 이왕이면, 몇가지 기능을 추가해 보죠. 흐음~ 그중 하나만 먼저 만든다면...Sprite를 지우는 기능이 있어야 겠죠. 쓸데없는 그림들이 잘못 들어갈 수도 있으니까요..
지우는 기능까지는 동봉된 소스에 구현되어 있습니다만, 여러분의 손으로 한번 만들어 보시기 바랍니다. 간단한 기능이니까요.
더불어 더필요한 기능이 있다면 여러분의 손으로 추가해 보시기 바랍니다. 현재까지는 기본적인 기능만을 지원하는 에디터입니다. 데이터는 분명 투명색을 지정할 수 있도록 되어있습니다. 하지만, 에디터에는 그냥 검은색이 투명색이 되도록 만들어 버렸죠? 실제로는 그림마다 다른 투명색을 사용할 수 있습니다. 이러한 투명색을 지정하는 루틴을 만들어 보세요. 여러 가지 재미있는 유용한 기능을 추가하는 것은 여러분의 몫입니다.
힌트를 드리자면...^^ 위에서 언급한 txt화일을 읽어서 그 순서대로 bmp화일을 로드하는 기능이라든가, 좀더 추가하면 txt화일에 투명색 값까지 지정할 수 있게 된다면 더욱 좋겠죠.
다음에는 RPG에서 사용되는 맵타일을 만드는 유틸과 맵에디터를 만들어 보겠습니다.
추신) 소스는 아직 완성이 안된 관계로 공개하지 않습니다. 추후 완성버젼이 만들어 진다면 공개하겠습니다.
첫댓글 길어도 읽으면 도움이 되실듯하네요~ ^^
블리터엔진이해하는데도 힘들었는데..스프라이트까지 해야되다니..ㅜㅜ