| ||
| ||
안녕하세요? 제가 요즘 슈팅게임을 만들고 있습니다. 게임진행에 필요한 클래스들은 거의 다 만들어서 이제 게임진행부분 (적기를 추가하고 이동시키고 등등..)만 하면 되어서 이정도면 슈팅게임에 필요한 클래스들은 대충 다 만든거 같다. 하여 고생해서 만든거지만 공개하고 또 하나하나 차례차례 따라 해보는 식으로 초보 분들도 게임을 만들 수 있게 하려고 합니다. 서론이 너무 길었나요? 자 그러면 이제 해봅시다! 흠.. 매우 죄송스럽지만 저도 밑에 네모네모 처럼 반말을 쓰게 습니다. 왜냐하면 존대말은 너무 타자 치기가 힘들어서요. 참고로.. 저는 중3 입니다^^ 중3짜리가 반말한다고 바로 백 하실까봐 두렵네요 -_ㅠ
1.윈도우 생성과 다이렉트 드로우 초기화
보통 강의를 보면 여기서 끝낸다. 이것만 하고 끝내는 것이다. 대략 난감하다. 그다음이 더 어려운데 말이다. 하지만 나는 끝까지 할 것이니 걱정들 말게나. 일단 이 강의를 보기 전에 C++, Win32 API를 대충 다 보고 와라. C++이나 Win32 API 에 대해서 까지 다 쓰러면 내가 먼지 지칠것 같아서 그런다. www.winapi.co.kr에 들어가서 C강의랑 공개된 Win32 API만 봐도 된다. API에 관한게 조금밖에 공개 안되있지만 그정도면 충분하다 언능가서 보고 오너라. 자 다했는가? 이제 한다. 아! 그전에 DirectX SDK 8.0a 도 깔아라 pds.hanafos.com에 들어가서 DirectX SDK라고 검색하면 나올것이다. 그리고 Visual C++ 도 6.0이상으로 설치해라 근데 참고로 6.0을 기준으로 할 것이다. 그래 다시 말하자면 모든 기준은 Visual C++ 6.0 & DirectX SDK 8.0a 이다. 이제 진짜 시작해보자.
VC++을 실행시키고 Win32 Application 이름은 Example로 프로젝트를 생성해 보자. 생성할때는 "An Empty Project"를 선택하도록 해라. 그다음에는 Flie -> New로 C++ Source File을 하나 만들자. 이름은 main.cpp 로. 참고로 지금 나도 같이 하고 있다. 알트+탭 을 자꾸 누르니 슬슬 귀찮다-_-ㅎ 자 허연 화면이 떴는가? 그래 이제 코딩을 해야지. 일단 배운대로 해보자. 교과서 적인 방식이 제일 좋다. 인클루드 -> WinMain -> WinProc 이다.
#include "windows.h"
#define CLASSNAME "Game"
HRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) { switch(uMsg) { case WM_CLOSE: PostQuitMessage(0); break; } return DefWindowProc(hWnd,uMsg,wParam,lParam); }
int APIENTRY WinMain(HINSTANCE hInst,HINSTANCE,LPSTR,int) { WNDCLASS WC={CS_VREDRAW|CS_HREDRAW,WndProc,0,0,hInst,LoadIcon(0,IDI_APPLICATION), LoadCursor(0,IDC_ARROW),0,0,CLASSNAME}; RegisterClass(&WC); CreateWindow(CLASSNAME,CLASSNAME,WS_POPUP|WS_VISIBLE,0,0,640,480,0,0,hInst,0);
MSG Msg; while(TRUE) { if(PeekMessage(&Msg,0,0,0,PM_REMOVE)) { if(Msg.message==WM_QUIT) break; TranslateMessage(&Msg); DispatchMessage(&Msg); } } return 0; }
자.. 대충 위에 처럼 했는가? 뭐? 안했다고! 당연하지. 전부다 똑같이 코딩할수는 없는 것이다-_- Win32 API를 공부했다고 믿기 때문에 위에 것들에 대해서는 설명안한다. 아 한가지는 설명하지.
WNDCLASS WC={CS_VREDRAW|CS_HREDRAW,WndProc,0,0,hInst,LoadIcon(0,IDI_APPLICATION), LoadCursor(0,IDC_ARROW),0,0,CLASSNAME};
저게 왜 저따구로 2줄 밖에 안차지 할까? 라고 고민할거 같아 한마디 쓴다. 매우 간단하게 생각해서 RECT rt = {0,0,640,480}; 과 같은 것이다. 이해 됬는가? 그냥 구조체가 생성되자 말자 바로 값을 넣는 것이다. 교과서랑 좀 다르지?ㅎ 나도 원래는 WC.cbsize = 0; 대충 이런식으로 했다. 그런데 Direct SDK 보니깐 아니더라. 역시 막강한 파워를 지닌 마이크로 소프트사의 프로그래머들은 저렇게 2줄로 끝내버리더군.
자! 지루함을 덜어내기위해 위에꺼 실행이라도 한번 해보자. F5을 부드럽게 눌러준다. 흠.. 그래 윈도우가 하나 만들어졌는데 이상하게 시리 그냥 투명한지 뒤에꺼 다 보인다. 당연하다 왜냐하면 배경색을 0 으로 줬기 때문이다. 그냥 무시하자 나중에 그림으로 채워 넣을 거니깐. 이제 무엇을 해야 하나? 그래 다이렉트 드로우를 초기화 시켜야 한다. 내가 올려논거에 보면 ddutil.cpp ddutil.h dxutil.cpp dxutil.h 라고 있다. 그것들을 프로젝트에 추가해보자. 왼쪽 창에 FlieView 탭에서 소스파일 폴더에서는 오른쪽 클릭후 "Add Files to Folder"로 *.cpp 소스 파일을 넣고 헤더파일 폴더에서는 *.h 헤더파일을 넣자.
추가 했는가? 이제는 클래스를 만들어보자. Class View 탭에서 Example Classes를 오른쪽 클릭후 New Class를 누른다. 그리고 Name 에다가는 CGame 이라고 넣자. 그리고 확인! 참 편리하다 VC++은 지알아서 다 해준다-_-ㅋ 막 부려먹자. 흠.. 여기서 게임이론에 대해서 하나 설명을 해야 겠다. 그래 나도 지루한거 싫어. 근데 어쩔 수 없이 해야 된다. 게임은 사용자로부터 입력을 받아 처리를 한다. 그런데 그 사용자가 언제 입력을 할지 우리로써는 도무지 알수가 없다. 그렇다고 위에서 부터 차근차근 내려가는 C언어의 모든 구역에다가 키 입력처리를 할 수도 없다. (그래서 Win32 API의 키보드 메세지를 이용하기는 하나 그건 별로다) 근데 생각해보자 컴퓨터는 매우 빠르다. 프레임이라는 개념을 아는가? 게임 하다보면 32FPS 이렇게 뜬다. "32프레임 퍼 세컨드"라고 나는 해석한다. 뭐 분명 다른 뜻이 있겠지만은. 하여튼 1초당 32번 화면을 보여주는 것이다. 이해 되는가? 1초다 32번이다. 와-_- 상상이 가는가. 1초당 32번 찍어낸단다. 나는 1초당 30번 발차기를 할 수 있어! 혹은 1초당 30번 키를 누를수 있어! 라고 하는 사람 봤는가? 없다-_- 그래 사람은 저런 컴퓨터의 프레임 속도보다 느리다. 스타를 생각해보자 마린이 움직이는 장면을 보이기 위해서 컴퓨터는 마린이 반의반의반발짝만 가도 마린이미지를 반의반의반발짝만큼 이동시킨 곳에다가 출력한다. 이렇듯 프레임은 매우 빠른속도로 다시 돌아오는 것이다. 아.. 나는 설명을 잘 못한다. 하여튼 대충 프레임이라는 것에 개념이 잡혔는가? 그정도면 충분하다. 나는 프레임이 먼지도 모르고 겜 만들기 시작했으니깐-_- 그래 이제 우리는 이 프레임을 이용해야 된다. 매 프레임 마다 키입력을 처리 하면 되는거 아닌가? 그렇지? 그래서
MSG Msg; while(TRUE) { if(PeekMessage(&Msg,0,0,0,PM_REMOVE)) { if(Msg.message==WM_QUIT) break; TranslateMessage(&Msg); DispatchMessage(&Msg); } <<요 부분은 매번 온다. }
에 보면 내가 표시해논 부분 있지 않은가? 그 쪽은 매번 다시 돌아온다. 이해 되는가? 매번돌아오는게 프레임이다. 그래 저기다가 프레임을 관리할 함수를 넣으면 되는것이다. 예전에 게임들은 모두 Init -> Main -> Exit 로 되있었다고 들었다. Init으로 초기화 하고 Main으로 매프레임마다 처리를하고 Exit로 종료처리를 하는것이다. 우리도 그렇게 해보자. 교과서적이고 옛날 적인게 좋은 것이다. 근데 우리에게는 아직 Init 과 Main 같은게 없다. 만들자.
ClassView에서 CGame 클래스를 오른쪽 클릭후 "Add Member Function"을 눌러서 펑션타입에는 int를 그리고 그 밑에는 Init을 입력한뒤 OK를 누르자. 같은방법으로 Main, Exit 함수도 만들자. (반환값은 역시 int 가 좋다. 반환값을 쓸일이 없더라도 급작스럽게 펑션을 종료해야 할 경우에는 return 이 쓰여지므로 무난한 int로 잡자.) 이 함수들의 정의로 가려면 (아.. 참고로 Class에 대해서 이해가 안가면 C++을 다시 공부하여라) CGame클래스옆에 +를 눌러서 함수가 보이게 한다음 가고 싶은 함수를 더블클릭 하면된다. 참 편리하지 않은가? 나는 이래서 VC가 좋다. 자! 이제 우리에겐 저 3함수가 생겼다. main.cpp를 켜보자. 그리고 다음과 같이 만들어 버리자.
#include "windows.h"
#define CLASSNAME "Game"
CGame Game;
HRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) { switch(uMsg) { case WM_CLOSE: PostQuitMessage(0); break; } return DefWindowProc(hWnd,uMsg,wParam,lParam); }
int APIENTRY WinMain(HINSTANCE hInst,HINSTANCE,LPSTR,int) { WNDCLASS WC={CS_VREDRAW|CS_HREDRAW,WndProc,0,0,hInst,LoadIcon(0,IDI_APPLICATION), LoadCursor(0,IDC_ARROW),0,0,CLASSNAME}; RegisterClass(&WC); CreateWindow(CLASSNAME,CLASSNAME,WS_POPUP|WS_VISIBLE,0,0,800,600,0,0,hInst,0);
Game.Init(); MSG Msg; while(TRUE) { if(PeekMessage(&Msg,0,0,0,PM_REMOVE)) { if(Msg.message==WM_QUIT) break; TranslateMessage(&Msg); DispatchMessage(&Msg); } Game.Main(); } Game.Exit(); return 0; }
그래 이제 CGame::Init() 에서 초기화 하고 Main()에서 매프레임마다 해줄것들을 하면 되고 Exit()에서 종료처리를 하면 된다. 그럼 오늘은 Init을 한번 건드려보자. 다시 CGame::Init()을 찾아가자. 난감하지? 그래 다이렉트 드로우를 초기화 해야 된다. CGame 클래스를 더블클릭하면 헤더 파일이 뜰것이다. class CGame 위에다가 추가하고 싶은거 추가하면 된다. 인클루드를 해보자.
#include "windows.h" #include "ddraw.h" #include "ddutil.h" #include "dxutil.h"
class CGame { public: int Exit(); int Main(); int Init(); CGame(); virtual ~CGame();
};
헤더파일에 저렇게 추가하자. 그런데 이대로 빌드하면 오류가 난다. 그래서 알트+F7을 눌러서 Link 탭으로 이동한뒤 Objects / Library Modules 부분의 맨 뒤에다가 ddraw.lib 와 winmm.lib 를 추가하자. 근데 그래도 오류가 날것이다. main.cpp 맨위에다가 #define INITGUID 라고 쓰자. 그리고 main.cpp위에 #include "game.h" 도 쓰자. 이제 빌드해볼까? 아.. 제길. 함수에 리턴값이 없단다. 아까 만든 3 함수 안에다가 return 0; 이라고 추가하자. 이제 모든 오류는 다 잡았다. 이제부터는 왜 그렇게 해야 되는거냐? 라고 의심하지 말고 그냥 따라해라. CGame 클래스를 오른쪽 클릭후 Add Member Variable를 누른후 타입에다가는 CDisplay* 을 그리고 아래에는 g_pDisplay 를 입력하고 OK눌러라. 이것은 하나의 리모콘이라고 생각하면 된다. 다이렉트 드로우를 맘대로 갖고 놀 수 있는 조종기라고 생각하자. 그리고 Init()안에다가 g_pDisplay = new CDisplay; 라고 써넣자. 이러면 이제 이 리모콘은 충전을 한것이다. g_pDisplay-> 라고 써보자 그러면 안에 들어있는 함수가 쭉 뜰것이다. 이름만 봐도 알겠다. CreateFullScreenDisplay() 가 필요하다 우리에겐. 지금 전체화면으로 만들어야 하니깐. 근데 인자들을 보니 HWND, DWORD, DWORD, DWORD다. 이름을 보니 대충 감이 오는가? HWND에는 전체화면으로 만들 윈도우 핸들값을 넣어줘야 하고 그뒤로는 해상도와 색깊이를 넣어 줘야 된다. 문제는 이 핸들값을 어디서 들고 오리? Init() 함수를 다음처럼 만들자.
int CGame::Init(HWND m_hWnd) { g_pDisplay = new CDisplay; g_pDisplay->CreateFullScreenDisplay(m_hWnd,800,600,16); return 0; }
그리고 CGame을 더블클릭한 뒤 int Init(); 를 int Init(HWND m_hWnd); 로 바꾸자. 함수의 인자를 바꾼것이다. 그런뒤에 Main.cpp에다가
Game.Init(CreateWindow(CLASSNAME,CLASSNAME,WS_POPUP|WS_VISIBLE,0,0,800,600,0,0,hInst,0));
이렇게 고쳐버리자. 2줄이던게 한줄이 되니깐 참 좋다ㅎ 저렇게 한 이유는 알겠지? HWND, 즉 핸들값을 받기 위해서다. 자 그러면 실행시켜 보자. 오!! 전체화면이 됬는가? 나갈때는 알트+F4를 누르자. 아.. 더 쓰고 싶은데 학원 갈시간이 다됬다. 지금 까지 한거는 내가 올려놓을테니 한번 자기가 한거랑 비교해보아라. 다음번에는 (학원갔다와서는) 메뉴창을 띄우고 방향키를 입력받으면 메뉴를 가르키는 커서가 위아래로 움직이게 만들것이다. 나는 참 말이 많다. 오늘 별로 안했는데. 이렇게나 길다. 그냥 그러려니 하고 생각해라. 왠지 이러고 끝내면 사람들이 욕할 거 같다. "다 아는 내용이야!" 라면서-_- 그래도 초보들은 이것부터 해야 된다. 또 나중에 가서 게임다 만들어도 그런소리 하나 보자! 난주 한.. 4시쯤에 다시 쓰겠다. See You Next Time!! | ||
|
| ||
| ||
안녕하세요. 학원갔다오니깐 피곤하네요. 그래도 이왕 시작한거 끝을 봐야 될거 같아서 또씁니다ㅎ 1번글에서 배운걸 한번 정리하고 시작하겠습니다. 1번글에서 배운것은 Class를 만들고 멤버 함수 및 멤버 변수추가 관리법. CDisplay를 이용하여 전체화면으로 초기화 시키기. 이런거였죠? 솔직히 다 아는 내용이고 하니 재미없고 그랬을 겁니다. 이번엔 좀 재밌는걸 해봐야 겠네요ㅎ 바로 메뉴 만들기 입니다. 참고로 그림은 제가 올려논거 쓰세요. 다른거 하실라면 또 좌표 새로 계산해야 되고 귀찮잖아요-_-ㅎ 그럼 시작합니다!
2.게임의 기본 구조 설정 & 메뉴 구현
이번에는 게임의 기본구조 설정 및 메뉴 구현을 해볼것이다. 나는 이거 내 혼자 암것도 모르고 한다고 고생했다. 뭐 그네들은 내 따라하면서 하니깐 내보다 조금은 났지 싶다. 자! 생각해보자. 게임이라는 것은 어떤 구조를 갖고 있는가? 간단하게 스타크래프트를 생각해보자. 내가 자꾸 스타를 예로 드니깐 나 나이 들어 보인다-_- 하지만 울집은 똥컴이라 요즘 게임은 못한다. 그래서 젤 잼있게 하는 게임은 스타, 바람 같은거 뿐이다. 아.. 또 이야기가 이상한데로 새어 버렸다. 다시 해보자. 스타를 예로 들면
메인 메뉴 ->>> 1.싱글플레이 ->>> 1.오리지날 2.확장팩 2.멀티플레이 ->>> 1.IPX 2.배틀넷 3.나가기
이런식이다. 즉 매우 계단식으로 되어 있는 것이다. 다른 게임들도 다 그렇다. 그럼 우리도 그렇게 해보자. 교과서 적이고 옛날적이고 따라하는게 제일 좋다. (갈수록 말이 하나씩 더 붙여 지고 있다-_-ㅎ) 그럼 어떻게 하지? 라고 걱정말고 그냥 무조건 따라 해봐라. 일단 현재 게임의 상태를 저장을 변수를 하나 만들자. CGame 클래스에 int 형으로 GameState 라는 전역변수를 만들자. 그뒤에 우리는 이제 GameState에 따라 표현을 달리해야 된다. 즉 더 복잡해졌다. 이걸 어떻게 할까? 단순히 Main에서 스위치 문으로 해도 된다. 하지만 계단이 한개만 있는것이 아니다. GameState를 참조하는 스위치문 아래에 또 스위치문이 존재할터 즉, 각 상태마다 함수를 만들어서 좀더 간단하고 깔끔하게 하자는 것이다. CGame내에 int Menu() 라는 함수를 하나 만들자. 그리고 이제 어떻게 해야 하나? Main() 함수에다가 다음과 같이 해보자.
int CGame::Main() { switch(GameState) { case 0: Menu(); break; case 1: break; case 2: break; case 3: break; case 4: break; } return 0; }
즉 GameState가 0이면 게임은 메뉴 상태인 것이다. 나머지는 나중에 쓸일이 있기때문에 미리 해놓자. 좋다. 그럼 이제 게임의 기본적인 구조설정은 된것이다. 단순히 그 구조아래에 있는 표현하는 함수만 작성해서 알맞는 위치에 넣어주면 된다. 그럼 이제 메뉴를 구현해 볼까? 메뉴... 나는 생각하는 것을 좋아한다. 또 한번 생각해보자. 메뉴는 어떻게 생겼는가?
┏━━━━━━━━━━━━━━━┓ ┃ 배 경 ┃ ┃ ┃ ┃ ┃ ┃ --> 게임시작 ┃ ┃ ┃ ┃ 게임종료 ┃ ┃ ┃ ┃ 그 림 ┃ ┗━━━━━━━━━━━━━━━┛
대충 저런식이다. 배경그림이 있고 메뉴가 여러개 있고 커서가 있다. 가장 먼저 뭐 부터 해야할까? 배경그림 부터 해보자. 배경그림을 그냥 불러와서 바로 모니터에 그릴 수도 있다. 그런데 그렇게하면 사용자가 모니터에 그림을 그리는 모습을 다 보지 않는가. 그래선 안된다. 그래서 Direct Draw라는게 있는 것이다. 이놈은 메모리에 먼저 그림을 그려놓고 필요할때 꺼내 쓴다. 이말이 이해 됬는가? 그러면 우리는 뭘 해야 하는가. 메모리에 배경그림을 그려놓고 Menu() 함수가 호출되면 꺼내써야 된다. 해보자. 또 일단 따라 해봐라. 먼저 CGame클래스 안에 CSurface* 형의 전역변수 g_pMenuBack을 만들자. 그러면 game.h 안에 CSurface* g_pMenuBack 이렇게 됬을 거다. 이게 바로 그림을 넣어둘 상자라고 생각하면 된다. 그러면 그림을 어떻게 그려넣지? 걱정마라 우리에겐 리모콘이 있지 않은가. Init()함수를 다음과 같이 바꿔보자. 아! 그전에 내가 올려논 파일을 받아서 안에 img 폴더를 프로젝트 폴더 내에 넣어라. 왠지는 아래를 보면 알것이다.
int CGame::Init(HWND m_hWnd) { char strBMP[128];
srand(GetTickCount()); g_pDisplay = new CDisplay; g_pDisplay->CreateFullScreenDisplay(m_hWnd,800,600,16);
wsprintf(strBMP,"img\\menu_back%d.bmp",rand()%3+1); g_pDisplay->CreateSurfaceFromBitmap(&g_pMenuBack,strBMP,0,0);
GameState = 0;
return 0; }
뭐가 바뀌었는지 알겠는가? 우리는 그냥 배경그림을 하나만 만들지 않고 3개중 랜덤으로 하나만 생성되게 만들었다. g_pDisplay 의 멤버함수중 하나인 CreateSurfaceFromBitmap() 은 인수로 **CSurface, TCHAR, DWORD, DWORD 를 받는다. 첫번째인수는 우리가 아까 만든 그림이 들어갈 상자(이제부터 서피스라고 부르겠다) 의 주소값을 넣어 주고 두번째는 그림의 경로 세네번째 것들은 그냥 0을 넣어주면 알아서 전체그림을 다 그려준다. 그러면 소스를 분석해보자. 첫번째 줄에 그림의 경로를 넣어줄 변수를 하나 만들었다. 그리고 srand(GetTickCount()) 는 난수 발생 함수인 rand()를 초기화 시켜주는 것이다. 그냥 srand(GetTickCount())와 rand()를 통째로 외우자. rand()에 대해서는 모두 알 것이다. 모른다면 www.winapi.co.kr 다시 들어가 봐라. 그리고 밑에 wsprintf()는 첫번째 인수로 들어가 있는 변수에다가 printf() 처럼 되있는 뒤에것들을 복사 해준다. printf()를 한번쯤은 써봤을 거므로 다 이해 될 것이다. 그 다음줄은 알아서 해석해봐라 위에서 다 설명했다. 자 그러면 어떻게 작동되는 것일까? strBMP[128]을 만들고 -> "img\\menu_back1~3.bmp"중 하나를 집어 넣고 -> g_pMenuBack에 strBMP경로에 있는 그림을 넣는다. 참고로 경로 잡을때는 "img\\" 처럼 \를 두번 써줘야 한다. 왠지는 딱 필이 오지? 안오면 C공부가 허술한거다.
그림을 서피스에 저장했다. 이것을 이제 바로 모니터에 뿌려야 한다. 하지만 다이렉트 드로우는 그렇게 하지 않는다. 바로 뿌리면 저장했다가 꺼내서 뿌리는거랑 그냥 뿌리는거랑 다를게 없다. 백 서피스 라는 보이지 않는 영역 (모니터 뒤편이라고 생각하면 된다)에다가 미리 그림을 다 그려놓고(이작업을 블리트라고 한다) Present() 함수가 호출되면 프라이머리 서피스라는 보이는 영역 (모니터 화면이라고 생각하면 된다)과 백서피스를 뒤집는 것이다. 이것을 플리핑이라고 한다. 뒤집으면 어떻게 되겠는가? 백서피스에 있던 그림이 눈에 보이겠지? 그럼 우리도 해보자. Menu()를 다음처럼 바꿔 보자.
int CGame::Menu() { g_pDisplay->Blt(0,0,g_pMenuBack); g_pDisplay->Present(); return 0; }
매우 간단하다. 위에는 무언가? 바로 블리트 하는것이다. CDisplay::Blt() 는 첫번째 두번째 인수로 블리트 할 위치, 그리고 세번째는 블리트할 그림이 저장되 있는 서피스, 네번째는 블리트할 서피스내의 영역. 우리는 불러들인 그림 전체를 블리트 할것이기 때문에 아무값도 주지 않았다. 지알아서 0이 들어간다. 이렇게 백서피에스 그림을 그렸으면(블리트) 아까 말했든 프라이머리 서피스와 백서피스를 뒤집어야 된다. 즉 플립을 해야 하는 것이다. 그 일을 해주는게 CDisplay::Present() 이다 인수는 없다. 그냥 호출되면 뒤집기만 하니깐. 이렇게까지 해놓고 한번 실행시켜 보아라. 실행시마다 랜덤으로 배경그림 1,2,3 중에 하나가 나올 것이다. 만약 그냥 검은 화면만 나온다면 img폴더내에 그림을 넣지 않았던가 아니면 경로를 잘못잡았던가 혹은 타이핑을 잘못했을 것이다. 왜냐면 나는 잘되거든^^
┏━━━━━━━━━━━━━━━┓ ┃ 배 경 ┃ ┃ ┃ ┃ ┃ ┃ --> 게임시작 ┃ ┃ ┃ ┃ 게임종료 ┃ ┃ ┃ ┃ 그 림 ┃ ┗━━━━━━━━━━━━━━━┛
후우~! 한번 숨을 들이키고 몸을 풀어주자. 너무 앉아서 컴퓨터만 하는건 좋지 않다. 분명 위에 글을 2~3번은 읽었을 것이다. 그러는동안 그네들의 허리는 0.00000000000572115751도 휘었을 것이다. 한번 펴주고 다시 해보자. 지금까지 한게 뭐지? 메뉴의 배경그림을 출력하는것! 위에 보자. (여자친구 땜에 잠시 연습했던 이모티콘이 이럴때 쓰인다 ^^v) 배경그림이 있고 게임시작, 게임종료 라는 메뉴가 뜬다. 이제 이놈을 해야 된다. 역시나 일단 따라해봐라. 먼저 CGame 클래스 내에 CSurface* 형 전역변수 g_pMenuText 를 만들자.
int CGame::Init(HWND m_hWnd) { char strBMP[128];
srand(GetTickCount()); g_pDisplay = new CDisplay; g_pDisplay->CreateFullScreenDisplay(m_hWnd,800,600,16);
wsprintf(strBMP,"img\\menu_back%d.bmp",rand()%3+1); g_pDisplay->CreateSurfaceFromBitmap(&g_pMenuBack,strBMP,0,0);
g_pDisplay->CreateSurface(&g_pMenuText,800,600); g_pMenuText->SetColorKey(RGB(255,155,0)); g_pMenuText->DrawBitmap("img\\menu_text.bmp",0,0);
GameState = 0;
return 0; }
int CGame::Menu() { g_pDisplay->Blt(0,0,g_pMenuBack); g_pDisplay->Blt(0,0,g_pMenuText); g_pDisplay->Present(); return 0; }
CGame::Init() 과 CGame::Menu()를 위에처럼 수정해라. 자 새로 추가된 부분을 보자. CDisplay::CreateSurface() 함수는 위에서 배운 CGame::CreateSurfaceFromBitmap()과 비슷한 함수이다. 그런데 이놈은 서피스를 초기화후 비트맵을 그리지는 않는다. 첫번째 인수는 해당서피스의 주소값 그리고 뒤에는 크기이다. 그리고 밑에를 보자. CSurface::SetColorKey() 이다. 이놈은 칼라키를 설정해주는데 일단 칼라키가 무엇인지 알아야 할 것이다. RPG게임을 보면 배경이있고 케릭터가 있다. 근데 참 신기하게도 케릭터 주위에는 배경이 그대로 보인다. 이게 왜 신기하냐고? 모든것을 사각영역으로 해결하는 컴퓨터가 어떻게 케릭터의 겉선을 하나하나 처리해서 거기 까지만 표현을 하는가 하는게 신기한 것이다. 그런데 사실은 그게 아니라 칼라키를 설정하는것이다. 이 칼라키로 설정된 부분은 투명으로 처리되기 때문이다.
┏━━━━━━━━━━━━━━┓ ┃ 배경그림 ┃ ┃ ┏━━━━━━━━━┓ ┃ ┃ ┃ 칼라키 ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┏━━━┓ ┃ ┃ ┃ ┃ ┃케릭터┃ ┃ ┃ ┃ ┃ ┗━━━┛ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗━━━━━━━━━┛ ┃ ┗━━━━━━━━━━━━━━┛
저런식으로 있다면 실제 케릭터 그림은 2번째로 큰 영역까지다 하지만 칼라키로 채워져 잇는 부분은 투명처리 되어서 뒤에 배경그림이 보이는것이다. 그럼 역시 우리도 한번 해보자. img 폴더내의 menu_text.bmp 를 보면 메뉴말고 다른것들은 RGB(255,155,0)의 색으로 채워져 있다. RGB는 색상값을 표기하는 방법중 하나인데. 자세한것은 자료를 찾아 보면 나올것이다. 나는 쉽게 말하겠다. RGB는 Red, Green, Blue 색을 각각 0~255만큼 섞어서 표현하는 것이다. 즉 RGB(255,0,0)은 빨간색이 될것이다. 자 그럼 우리는 투명처리될 색이 RGB(255,155,0) 이다. 그럼 이 색을 투명처리 해보자. CSurface::SetColorKey()는 그런 일을 해준다. 인수로는 투명처리될 색을 RGB() 매크로로 넣어 주면 된다. 그러면 그색은 투명처리 되는 것이다. 그리고 Menu()에서는 배경그림을 블리트 하고 그위에다가 다시 메뉴그림을 블리트 하였다. 머리속으로 상상해 보아라. 참고로 배경색 RGB(255,155,0)투명처리 되었다. 그러면 배경그림위에 파란 글자만 뜨겠지? 저기 까지 했으면 F5를 살포시 눌러보자. 어떤가? 됬는가? 심심하면 칼라키 설정부분을 빼도 된다. 그러면 투명처리 되지 않아서 낭패를 보겠지만. 아! 바보다-_- CSurface::DrawBitmap()을 설명안했다. 근데 사실 설명할 것도 없다. 딱보면 알수 있다. 모든 인수는 CreateSurfaceFromBitmap의 2,3,4번째 인수랑 같다. 그냥 단순히 서피스에 그림을 그려주는 것이다. 우리는 칼라키를 설정해야 했기 때문에 CreateSurfaceFromBitmap을 사용할 수 없었다.
원래 이번강의에서 커서구현 까지 하려고 했으나 안되겠다. 포기다-_ㅠ 조금쉬었다가 다시 3번글에서 커서를 구현해 보겠다. 이번에 배운것은 서피스생성, 칼라키설정, 블리트, 플리핑 등이다. 이것은 DirectDraw에서 매우 중요한것이자 DirectDraw의 전부라 할 수 있는 것들이다. 이해안되는거 있으면 끙끙앓고만 있지 말고 바리바리 밑에 리플을 달아라. 난 간다. 좀 쉬었다가 나중에 커서구현글을 써보겠다. 지금까지 한것을 올려놓을거니 자기 소스랑 비교해보고 그림도 자기 프로젝트에 옮기길.. 아 배고파-_ㅠ | ||
|
| ||
| ||
안녕하세요? 오늘 벌써 3번째 쓰네요. 역시나 이때까지 배운것을 정리해봅시다. 제가 그냥 무조건 따라하세요. 라고 말은 했지만 사실 무조건 따라만 해서는 안됩니다. 제가 여기서 정리하면서 말한것들을 정말 다 알고 왔는지 한번 재검토 해보세요. 이때까지 배운것은요. CDisplay와 CSurface에 관한 것이었는데요. 풀스크린모드(독점모드)로 초기화 시키기, Class다루기, 서피스생성, 서피스에 그림그리기, 블리트, 플리핑등이었습니다. 오늘은 Direct Input과 메뉴커서구현을 해보려고 합니다. 역시 서론보다는 본론이 중하지 않겠습니까? 시작합세다! (이거 보기 전에 먼저 소스파일을 다운받아서 img 내의 그림을 옮긴뒤에 하세요)
3.Direct Input과 메뉴커서 구현
이때까지 배운것은 다 정리가 되었나? 그럼 바로 시작하도록 하지. 나는 자주 "그럼 뭘 해야 하지?" 라고 질문한다. 어떤이들은 이런걸 보고 쟤 이상해-_- 라고 비난을 하겠지만 아주 중요하다. 나는 프로그래밍을 하면서 뭘해야하지? 라는 질문을 나스스로에게 수십번 수백번씩 했다. 그것은 다른것에대한 모험을 유발함과 동시에 새로운 정보습득을 할 기회를 제공한다. 다시 한번 질문하지. 우리가 지금까지 한것은 메뉴 배경그림과 메뉴텍스트를 출력한 것이다. 이제 뭘 해야 하지? 답이 나왔는가. 앞서 말한대로 커서를 구현해야 한다. 커서라는것은 키보드를 입력받아서 처리해야 한다. 그렇다. 키보드를 입력받아야 한다. 그러기 위해서 Direct Input을 사용해보자. 일단 초기화를 해야겠지. CGame 클래스 안에 전역으로 LPDIRECTINPUT8형 g_lpDI과 LPDIRECTINPUTDEVICE8형 g_lpDIDevice를 넣어보자. 그런뒤에 CGame::Init()함수 안에 다음과 같이 써넣어보자.
DirectInput8Create(GetModuleHandle(0),DIRECTINPUT_VERSION,IID_IDirectInput8,(void**)&g_lpDI,0); g_lpDI->CreateDevice(GUID_SysKeyboard,&g_lpDIDevice,0); g_lpDIDevice->SetDataFormat(&c_dfDIKeyboard); g_lpDIDevice->SetCooperativeLevel(m_hWnd,DISCL_FOREGROUND|DISCL_NONEXCLUSIVE); if(g_lpDIDevice) g_lpDIDevice->Acquire();
이것은 DirectInput을 초기화 시키는 과정이다. 흠.. 설명을 해볼까? 이런-_- 사실 나도 잘 모른다. 그냥 다이렉트 SDK를 보고 스스로 습득하길. 사실 저거는 그냥 통째로 써붙이면 되는것이다. Direct SDK 한글 헬프 문서를 올려놓을테니 보고 알아서 공부해라. 사실 별로 공부할 것도 없다. SDK아닌가. 그냥 가져다가 쓰면 되지 뭘-_-ㅋ 그래 이제 키 입력을 받아야 겠지? 저것만 가지고는 안된다. 키입력을 받는 함수를 만들어보자. CGame 클래스에 bool형 반환값을 가지는 KeyDown() 이라는 함수를 만들자. 만들었는가? 그리고 int 형 전역변수 keyboard[256] 을 만들어보자. 그러고 아래처럼 써보자.
bool CGame::KeyDown(unsigned long m_key,bool m_mode) { BYTE m_arDiks[256]; ZeroMemory(m_arDiks,sizeof(m_arDiks)); g_lpDIDevice->GetDeviceState(sizeof(m_arDiks),m_arDiks);
for(int i=0;i<256;i++) { if(m_arDiks[i]&0x80) { if(keyboard[i]!=2) { keyboard[i]=1; } } else { keyboard[i]=0; } }
if(m_mode) { if(keyboard[m_key]==1) { keyboard[m_key]=2; return 1; } return 0; }
else { if(keyboard[m_key]) { return 1; } return 0; } }
이것도 설명하기 귀찮다. 그냥 가져다가 쓰자. 그런데 한가지 알아둘게 있다. Direct Input의 키보드 입력 방식에 관한 것이다. Immediate 모드와 Buffered가 그것인데. 전자는 연속적인 키를 입력받을시에 필요하고 후자는 연속적이지 않는 키입력을 받을시에 필요하다. 자세한것은 따로 공부해야 할것이다. 나도 이거만 가지도 이틀을 잡고 있었다. DirectInput 간단하다고 모두들 말하지만 사실은 그게 아니다. 다른 다이렉트X의 객체들에 비하면 매우 간단하지만 나름대로 생각해볼 것들이 있다. 그에 관한것들은 따로 공부해보아라. 나도 잘 모른다. 아! 한마디 더 붙이면 bool CGame::KeyDown(unsigned long m_key,bool m_mode) 에서 m_mode에 0이 주어지면 Immediate처럼 작동하고 (키가 계속 눌러져 있어도 1을 반환한다) 1이 주어지면 Buffered처럼 작동한다. (키가 눌러질때 한번만 1을 반환한다) 이를 잘생각하고 사용하자. 그리고 첫번째 인수는 키의 번호이다. 이를 알기 위해서는 내가 올려논 KeyDown을 실행시켜보면 알 수 있을 것이다. 그러면 이제 키보드 입력함수는 가져온것이다. 한번사용해보자. CGame::Menu() 함수제일 밑에 다음과 같이 추가해보자.
if(KeyDown(1,1)) PostQuitMessage(0);
그러고 빌드해보면.. 에러가 난다-_- 쩝. CGame을 더블클릭해서 헤더파일을 열어서 #include "dinput.h"을 추가하고 알트+F7을 눌러서 링크 탭에서 dinput8.lib을 추가해보자. 그러면 잘될 것이다. 실행시키고 ESC키를 한번 눌러보자 바로 빠져 나가는가? 그렇다 1은 ESC의 키번호이다. 그리고 뒤에 1은 아까 위에서 설명했다. ESC가 입력되니깐 리턴값이 1이 되고 그럼으로써 참이 되므로 (0이 아닌값이면 무조건 참이다) PostQuitMessage() 함수를 발생시키는 것이다. 대충 이런식으로 쓴다는걸 알았으면 다음으로 넘어가보자. 전역변수로 int MenuBox, CSurface* g_pMenuBox 를 추가하고 Init() 함수에다가 다음과 같이 써넣자.
g_pDisplay->CreateSurface(&g_pMenuBox,256,192); g_pMenuBox->SetColorKey(RGB(255,155,0)); g_pMenuBox->DrawBitmap("img\\menu_box.bmp",0,0);
이제 척보면 무슨의민지 알겠지? 다음번부터는 비트맵 로딩은 직접 안써줄거다. 알아서 해라. 흠.. 이제 뭘해야하지? 키입력을 받는 함수도 만들었고 MenuBox이미지도 불러들였다. 생각해보자 메뉴박스, 즉 커서는 초기에 1번 메뉴에 위치해 있다가 사용자로부터 키가 눌러지면 한칸 내려가거나 한 칸 올라간다. 지금 이거를 이때까지 배운것으로 구현할수 있겠는가? 나는 가능하겠다. 지금까지 배운것만으로도 충분하다. 한번차근차근 생각을 해보아라. 뭐, 그래봤자 열에 여덟은 그냥 바로 스크롤을 내릴것이다. 그래 내가 구현해보겠다.
int CGame::Menu() { if(KeyDown(200,1)) MenuBox -= 1; if(KeyDown(208,1)) MenuBox += 1; if(MenuBox<0||MenuBox>3) MenuBox = (MenuBox<0)?0:3;
g_pDisplay->Blt(0,0,g_pMenuBack); g_pDisplay->Blt(0,0,g_pMenuText); g_pDisplay->Blt(16,MenuBox*88+232,g_pMenuBox); g_pDisplay->Present(); return 0; }
Menu()함수를 다음과 같이 고친뒤에 빌드해보자. 어때? 메뉴가 움직이나? 그런데 문제가 있다고. 그럴만도 하지 g_pMenuBox그림 전체를 불러들여서 찍어냈으니 그럴만도 할것이다. 그런데 왜 그림을 3개나 만들었냐고? 그래 먼가 다 이유가 있다. 그것도 소스입력으로 해결볼수 있는 문제다. 그래서 원래 내가 원하는 소스는 저게 아니다. 다른게 있다. 근데 안했다. 왜냐고? 왜 그림이 3개나 필요하고 또 그거를 전부다 g_pMenuBox서피스 불러들여야 했는가? 라는 의문을 가지게 하기 위해서다. 자, 그럼 그 의문을 풀어보자.
애니매이션 이라는 말을 많이 들어봤을 것이다. 여러개의 그림을 차례대로 보여주어 움직이는듯 보여주는 것이다. 이런건 만화뿐만 아니라 게임에서도 많이 사용된다. 그러면 우리도 사용을 한번 해보자. 여러개의 그림을 차례대로 보여준다고 했다. 그렇다 여러개의 그림이다. 우리에겐 준비되어있다. menu_back.bmp 가 바로 그것이다. 각 그림은 256*64 으로 그려져 있다. 어떻게 사용하면 될까? 처음에는 0,0,256,64 영역의 그림을 두번째는 0,64,256,128의 영역 그리고 세번째는 0,128,256,192 그리고 네번째는 다시 첫번째 보여줬던 그림을 보여주면서 반복하면 될것이다. 지금까지 배운걸 조금만 응용하면 충분히 가능하다. 그러나 역시 나의 소스를 기다리고 있을 그네들을 생각하여 써본다. 일단 전역변수로 int형 count1 을 만들어라. 그리고 아래처러 바꾸자.
int CGame::Menu() { if(KeyDown(200,1)) MenuBox -= 1; if(KeyDown(208,1)) MenuBox += 1; if(MenuBox<0||MenuBox>3) MenuBox = (MenuBox<0)?0:3;
RECT rt = {0,count1/15%3*64,256,count1/15%3*64+64}; g_pDisplay->Blt(0,0,g_pMenuBack); g_pDisplay->Blt(0,0,g_pMenuText); g_pDisplay->Blt(16,MenuBox*88+232,g_pMenuBox,&rt); g_pDisplay->Present(); return count1++; }
이렇게 만들고 실행시켜 보아라. 어떤가 잘되는가? 그럼 이제 저것을 한번 집중 분석해보자. 이제부터 Line01, Line02 이런식으로 말하거니깐 알아서 들어라. Line01과 Line02는 위로 키(200)와 아래키(208)가 눌러졌을경우 MenuBox(메뉴박스가 어디에 위치하는가에 대한 값을 갖고 있는 변수)에 값을 +- 하는 것이다. 그리고 Line03은 메뉴박스가 0보다 적거나 3보다 크다면 실제 메뉴영역에서 벗어나기때문에 메뉴박스값을 0보다 작거나 3보다 크게 만들어졌을경우 0또는 3을 넣어주는것이다. 삼항연산자가 뭔지 모르는 사람은 C언어 공부를 좀더 해라. 그리고 Line05에 보면 count1 이라는 값을 참조하여 RECT (사각영역 구조체) 형 변수 rt 를 만든다. 이 rt는 (count/15)가 3으로 나누었을때 나오는 값 0,1,2를 참조하여 영역을 만든다. 나머지는 몫보다 크기가 무조건 적다는 것쯤은 알것이다.(참고로 %연산은 나머지값을 리턴한다) 이 %3 은 아마 rand함수 쓸때 썼을것이다. 그것도 같은 원리이다. 생각해보자 count1 은 마지막에 함수종료시 ++ 된다. 즉 매프레임 +1 되는 것이다. 즉 count1/15 라는 값이 정수가 되는 시점은 15, 30, 45, 60 ...... 이다. 즉 15프레임 간격으로 정수가 되는 것이다. 그렇다면 count1이 15일때는 나누기 15를 해서 나오는 값이 1이다. 30때는 2이고 45때는 3이다. 즉 이말은 15프레임 간격으로 count1/15 는 +1 된다는 소리다. 여기까지 이해가 가는가? 그리고 그 값을 나누기 3 했을때 나머지 값은 15프레임 간격으로 매번 달라진다. 즉 count1 이 15일경우에는 count1/15%3 이 1이고 30일경우에는 2, 45일경우에는 0 60일경우에는 1 75때는 2.. 이렇게 계속 반복하는 것이다. 즉 우리가 하고자 하는 애니매이션이 가능하다는 것이다. 0,1,2 라는 값이 반복되니깐. 아무래도 한번더 설명을 해야 할 것 같다. 나도 첨에는 애니매이션을 구현하기위해 for문을 3번 중첩시켜서 사용했었다.-_- 하도 귀찮아서 조금 생각해보니깐 다음과 같은 식이 성립되더라.
[ 프레임수 / 애니매이션 간격 % 애니메이션에 쓰일 그림 수 ] ->> count1/15%3 = "count1을 15로 나눈 값에 3으로 나누어서 얻어지는 나머지(=그림번호)"
어떤가 글로 풀어서 쓰니깐 좀 이해가 되는가? 즉 15프레임 간격으로 0,1,2가 반복적으로 리턴 되니깐 그 값으로 사각영역을 지정하면 되는것이다. 이 사각영역을 지정할때에도 하나의 식이 필요하다. 이것또한 조금 생각해보니깐 나온결론이다. (이런거는 돈받고 갈켜줘야 되는거 아닌가? -_-ㅎ 장난이다)
애니메이션에 쓰일 그림이 가로로 놓아져 있을 경우 -> [ 애니메이션그림 번호 * 그림가로크기, 0, 애니메이션그림 번호 * 그림가로크기 + 그림가로크기, 전체세로크기 ] 애니메이션에 쓰일 그림이 세로로 놓아져 있을 경우 -> [ 0, 애니메이션그림 번호 * 그림세로크기, 전체가로크기, 애니메이션그림 번호 * 그림세로크기 + 그림세로크기 ] ->> RECT rt = {0,count1/15%3*64,256,count1/15%3*64+64};
이거는 조금 생각해보면 이해 갈것이다. 조금 설명해보자면 count1/15%3으로부터 리턴되는 값이 0일때는 (즉 0번그림) [0,0,256,64] 가 될것이다. 위에서 말한 것과 같은 영역이다. 그리고 1번일때는 [0,64,256,128]가 될것이고 마지막으로 2일때는 [0,128,256,192]가 될터이다. 즉 영역이 같은간격으로 한칸씩 내려가는 것이다. 그럼 3일때는요? 라고 묻는 바보는 없겠지.. %3 연산을 했으니깐 3으로 나눴을때 나오는 나머지가 나올 것이다. 그런데 나머지가 몫보다 클일이 있겠는가? 없지 않은가. 어떤 정수든간에 2로 나누면 나머지는 0또는 1이 된다. 3으로 하면 0,1,2가 되는 것이다. 문제없지? 자 이런식으로 그림의 영역을 계산해 놓는다. 그다음은 다 이해되지? Line08 이 이해 되지 않는가? 그 맨뒤에 &rt 는 g_pMenuBack 서피스에서 &rt 영역만큼만 따오라는 소리인것이다. 그리고 리턴할때는 count1을 +1 해주는것은 당연할테고. 그래야지 지금 프레임이 몇프레임 지났는지 알거니깐.
오늘은 여기까지다. 오늘 배운것은 Direct Input관한거 조금(사실 배운것도 없다), 애니매이션 구현 이다. 중요한것은 애니매이션 구현이다. 애니매이션이 어떻게 이루어지는지 아는것이다. 사실 이런것을 애니매이션이라고 하는지는 잘 모르겠다. 그냥 내맘대로 갖다 부친것이다. 하여튼 이런 움직이는 그림은 이렇게 하는 것이다. 아! 빌드하고 실행시키고 나면 마우스 커서가 상당히 신경쓰일 것이다-_- 그럴때는 Init함수 젤위에 ShowCursor(0) 이라고 넣어두자. 그러면 조용히 사라져 줄것이다. 그럼 내일 봅세! | ||
|
| ||||
| ||||
안녕하세요. 어제 늦게잤더니 피곤해 죽겠네요. 오늘도 한번정리를 해볼까요? 지금까지 배운것은 CDisplay 와 CSurface를 이용하여 다이렉트를 초기화 시키고 서피스를 생성하고 서피스에 그림을 그린뒤 블리트와 플리핑을 하는 것이었습니다. 또 애니메이션 구현에 대해서도 한번 알아봤구요. 기억 나십니까? 아! 1번글에서 말씀 드렸드시 이 강의는 제가 만들고 있는 게임을 바탕으로 하고 있습니다. 허나 제가 만든게임을 똑같이 만들면서 하기는 힘듭니다. 응용하면 쉽게 구현 가능한것들은 뺐습니다. 제가 만들고 있는 게임을 올려놓을테니 한번해보세요. 무엇을 구현하지 않고 넘어갔는지 아실 수 있을 겁니다. 한번 혼자서 연구해서 구현해보는것도 좋겠지요. 오늘도 역시나 먼저 소스파일을 다운받으셔서 img 폴더내의 그림파일들을 옮기세요. 다하셨나요? 그럼 시작합니다!
4.종료처리와 맵스크롤 구현
한번 눈을 감고 지금 까지 한것에 대해서 생각해보자. 했는가? 그럼 이제 무얼 해야 하지? 메뉴도 만들었겠다. 이제 게임플레이 부분을 만들어야 겠지? 하지만 그전에 할일이 있다. 바로 종료처리 이다. 이것은 매우 중요하다. 나는 이것을 소홀히 했다가 알 수 없는 렉 때문에 그거 잡는다고 어제 밤샜다. Main함수는 GameState의 값에 따라 처리가 달라진다. GameState가 0일때는 메뉴를 처리했다. 필이오지? 우리는 1, 2, 3, 4 일때의 처리를 해야 한다. 1 일 때는 무얼 하고 2일때는 무얼해야 하는지 감이 안온다고? 1~4, 4개다. 우리의 메뉴도 4개다. 각 메뉴마다 하나씩 하면 안되겠는가? 그중에 맨 마지막 4번을 한번 해보자. 그래 4번은 종료되는 부분이다. 우리가 처음 게임을 만들때 종료처리는 Exit 에서 한다고 했다. 그럼 한번해보자 따라해봐라. 간단하다.
int CGame::Exit() { SAFE_DELETE(g_pMenuBox); SAFE_DELETE(g_pMenuText); SAFE_DELETE(g_pMenuBack); SAFE_DELETE(g_pDisplay);
SAFE_RELEASE(g_lpDIDevice); SAFE_RELEASE(g_lpDI); return 0; }
CGame::Eixt() 함수를 위에 처럼 바꿔라. 딱보면 뭔지 알것이다. 안전하게 제거한단다. 포인트형은 SAFE_DELETE()를 사용해야 하고 아닐경우에는 SAFE_RELEASE()를 사용해야 한다. 그리고 주의할점은 작은것부터 지워야 한다는 것이다. 맨위에 SAFE_DELETE(g_pDisplay)를 써넣는다면 게임 종료후 바로 에러가 날 것이다. 이런식으로 사용한거는 다시 되돌려 놓는게 종료 처리의 기본이다. 절때로 잊지말고 챙겨주도록 하자. 게임을 종료시키려면 어떻게 해야하지? 그래 GameState를 4로 만들고 Main()이 호출되면 게임을 종료 시킬수 있다. 한번 해보자. 역시나 일단 따라 써라.
if(KeyDown(28,1)) { count1 = 0; GameState = MenuBox+1; MenuBox = 0; }
switch(GameState) { case 0: Menu(); break; case 1: GameState = 0; break; case 2: GameState = 0; break; case 3: GameState = 0; break; case 4: PostQuitMessage(0); break; }
위에 if문은 CGame::Menu() 에 넣고 아래 switch 문은 CGame::Main()에 넣어야 한다. 그래놓고 한번 실행시켜 봐라. 1,2,3번 메뉴는 눌러도 아무 일을 하지 않는다. 하지만 4번메뉴는 누르면 종료를 한다. 그럼 왜그런지 한번 봐야겠지? if(KeyDown(28,1)) 은 무슨말인지 알것이다. 근데 28번이 무슨키인지 모른다고? 28은 엔터키의 키 번호이다. 즉 엔터키가 눌러진다면 다음일을 해라. 라고 해석된다. count1 = 0 은 count1을 초기화 시키는 작업이다. 안그러면 나중에 숫자가 너무 커져 버려서 int의 최대 범위인 32767을 넘어설수도 있다. 그럴일은 없겠지만. 대충 그럴때를 대비하여 메뉴에서 빠져나갈때 초기화 시켜 버리는 것이다. 그리고 GameState = MenuBox+1 을 보자. GameState에서는 0번이 메뉴 1번이 게임시작 4번이 게임종료이다. 하지만 MenuBox같은경우는 게임시작 부분에 커서가 있을때는 0번 게임종료에 있을때는 3번이다. 즉 1만큼 적은것이다. 그래서 +1 한것이다. 그리고 다시 MenuBox도 초기화. 스위치 문을 볼까? 1,2,3번 즉 게임시작, 기체정보, 만든사람 메뉴는 일단 GameState = 0 으로 메뉴가 불러지자 마자 다시 메인메뉴로 돌아가게 했다. 4번은 게임종료 부분이다. PostQuitMessage(0) 으로 게임을 종료 시켰다.
자! 또한번 생각해보자. 이제 무얼해야 하나? 게임시작 부분을 해야지! 참고로 기체정보, 만든사람은 구현하지 않겠다. 나는 저런것보다 수학적인 것이다 다이렉트 드로우 사용법에 관한것을 중점으로 할것이다. 기체정보, 만든사람 같은거야 지금까지 배운것만으로도 충분히 구현가능하다. 그래, 잠깐 이야기가 샜는데. 게임시작을 해야겠지. 그러면 int형 반환값을 가지는 함수 Play() 를 하나 만들자. 그리고 안에 return 0 도 써주자. 안그럼 VC++ 이 화낸다.
case 1: Play(); break;
그리고 스위치문의 case 1을 위에처럼 만들자. 슈팅게임에 보면 꼭 있는것이 바로 배경맵스크롤 이다. 그까이꺼 만들어넣으면 되지. int형 반환값을 가지는 MapScroll 이라는 함수도 만들자. 그리고 CSurface* 형 전역변수 g_pMap, g_pGameInfoR, g_pGameInfoL 3개를 만들어 각각을 CretaeSurfaceFromBitmap으로 "img\\map.bmp", "img\\game_info_right.bmp", "img\\game_info,left.bmp" 그림파일을 그려넣자. 말했다 이젠 서피스생성부분은 내가 하지 않겠다고 알아서 CGame::Init() 함수에다가 해봐라. 그리고 CGame::Exit() 부분에 종료처리도 꼭! 하고. 마지막으로 int형 전역변수 count2도 만들자. 다했는가? 그러면 다음과 같이 써넣어 보자.
int CGame::Play() { if(KeyDown(1,1)) { GameState = 0; count2 = 0; } g_pDisplay->Clear(); MapScroll(); g_pDisplay->Present(); return count2++; }
int CGame::MapScroll() { g_pDisplay->Blt(0,0,g_pGameInfoL); g_pDisplay->Blt(650,0,g_pGameInfoR);
RECT rt = {0,4400-(count2/4),500,5000-(count2/4)}; g_pDisplay->Blt(150,0,g_pMap,&rt); return 0; }
이번에도 수학적인 개념이 약간 들어간다. 좌표계산이라는게 이렇게 귀찮은 놈이다-_ㅠ 자 집중 분석을 해보자. 일단 Play() 함수. 이놈은 GameState가 1일때 불려지는놈이다. Line01 에보면 if(KeyDown(1,1)) 이 있다. 1번 이 뭐라고? 그래 ESC키다. ESC키가 눌려지면 일을 수행하는데. 어떤일인지 보아라. GameState = 0, count2 = 0 이다. 게임스테이트를 0 으로 만들어서 메뉴로 돌아가고 count2를 초기화 시키는 작업이다. Line06 을 보면 새로운 함수가 나온다. CDisplay::Clear() 라는 함수다. 그냥 한번 호출되면 화면을 깨끗~하게 지우는 일을한다고 보면 될것이다. 사실 나도 저 함수에 대해서 따로 배운것은 없다. 그냥 호출 해보니 화면을 지워버리더라-_-ㅋ 대충 찍어 맞춰서 쓰면 되지 뭐. 그리고 MapScroll() 이 호출되고 플리핑하고 count2가 +1 이 된다. 이놈도 프레임수를 체크하는놈이라고 보면 된다. Menu() 의 count1과 같은 기능이다. 그리고 MapScroll() 을 보자. Line01, 02는 알겠고. 04를 보자. 영역을 계산한다. 맵스크롤 할때 필요한 영역계산 식은 다음과 같다.
[맵의 왼쪽 X, 맵의 최하단 Y좌표 - 출력될 크기 - 프레임당 스크롤 될 양, 맵의 오른쪽 X, 맵의 최하단 Y좌표 - 프레임당 스크롤 될 양]
RECT rt = {0,4400-(count2/4),500,5000-(count2/4)} -> 맵의 왼쪽 X, 맵의최하단 Y(5000) - 출력될크기(600, 왜냐면 우리는 800*600해상도로 제작중이니깐) - 프레임당 스크롤 될양(0.25*프레임 수, 프레임/4 니깐 매프레임당 0.25씩 까이는것이다), 맵의 오른쪽 X, 맵의 최하단 Y(5000) - 프레임당 스크롤 될양(0.25*프레임 수)
더럽게 길다-_- 참고로 저 식은 맵이 세로로 스크롤될때에 해당되는 것이다. 한번에 이해가 안갈지도 모른다. 나는 프로그래밍 하면서 수학식을 만들어낼때가 제일 재밌고 기분이 좋더라. 그네들도 소스코드의 줄을 줄일 요량으로 수학식을 짜보아라 재밌다. 발사각도에 따른 총알의 x,y 값의 변화값을 계산해주는 식을 만들고 아주 통쾌했다! 이러는 동안 모두 이해 했는가? 만약 프레임이 100프레임이라고 해보자. 그러면 rt = {0,4150,500,4750} 이 될것이다. 어떤가 영역이 조금 올라갔는가? 그러면 1000프레임 이라고 해보자. rt = {0,1900,500,2500} 이 될것이다. 영역이 상당히 많이 올라간 것을 알 수 있다. 자 그러면 다시 돌아가서 MapScroll() 을 보자. g_pMap 그림을 rt 영역 만큼 블리트 한다. 우리는 {0,150,0,600} 영역에 게임인포L 이라는 놈이 존재하므로 150더 가서 블리트했다. 그리고 리턴 0.
어떤가? 오늘은 꽤 재밌는것을 한거 같은가? 오늘 배운것은 맵스크롤 이었다. 이제부터 CFlight라는 클래스를 만들어서 기체를 만들고 움직이고 충돌처리를 하고 점수를 높이고 등등. CFlight, CGame 2개의 클래스를 조작해야 되니 살포시 복잡해 질 것이다. 오늘 익힌 맵스크롤을 가로로 써먹어 보는것도 꽤 재밌을 것 같으니 한번 해봐라. 그럼 나는 아침 먹으러 간다. 나중에 보자! 참고로 내가 만든 게임의 공격키는 'Q' 이다. | ||||
|
| ||||
| ||||
안녕하세요. 그동안 조회수도 별로고 반응도 좋지 않은거 같고 해서 그만 두려다가 또 새로 씁니다. 유도구현하려고 좌표그려놓고 기울기랑 삼각비 뭐 등등 머리 아픈짓만 하다가 데브피아 오니깐 또 이게 생각나서요. 지금까지 한거 한번 빌드 & 실행 해볼가요? 게임시작하면 맵만 덩그러니 스크롤 되는 형식이군요. 그럼 시작해보겠습니다!
5.비행기 띄우기
이번엔 무얼하는고 하니 비행기를 띄우는 것이다. 그러면 뭐가 필요할려나.. 석유 100리터.. 활주로.. -_-? 장난이고. 비행기 클래스가 필요할 것이다. 클래스를 한번 설계해보자. 뭐가 필요할까? 비행기의 x,y 좌표. 일단은 그것만 있어도 되겠네. (나는 이런 작업을 클래스 설계라고 내혼자 부른다) 아니다. 이런저런 함수도 필요하다. 대충 정리해보면..
CFlight : 멤버변수 - x, y, speed, g_pDisplay : 멤버함수 - CreateFlight(), Teleport(), Move(), DrawFlight()
저정도면 충분한가? 참고로 speed는 한프레임당 이동할 거리를 나타내는 값이다. 한번 클래스를 만들어보자. 모든 멤버변수 및 멤버함수의 반환값은 int 로 주자. 아! g_pDisplay 는 CDisplay 형으로 선언하자. 다만들었는가? 그러면 이제 멤버 함수들을 구체적으로 구현해보자.
int CFlight::CreateFlight(int m_Model, CDisplay *m_pDisplay) { x = 0; y = 0; g_pDisplay = m_pDisplay;
switch(m_Model) { case 0: speed = 5; }
return 0; }
int CFlight::Teleport(int m_x, int m_y) { x = m_x; y = m_y; return 0; }
int CFlight::Move(int m_head) { switch(m_head) { case 0: y -= speed; break; case 1: y += speed; break; case 2: x -= speed; break; case 3: x += speed; break; } return 0; }
int CFlight::DrawFlight(int m_count, CSurface *m_pFlight) { RECT rt = {m_count%3*64,0,m_count%3*64+64,96}; g_pDisplay->Blt(x,y,m_pFlight,&rt); return 0; }
int CGame::Init(HWND m_hWnd) {
//중략.. g_pDisplay->CreateSurface(&g_pFlight,512,2048); g_pFlight->SetColorKey(RGB(255,155,0)); g_pFlight->DrawBitmap("img\\flight.bmp",0,0);
GameState = 0;
return 0; }
int CGame::Play() { static CFlight Player;
if(KeyDown(1,1)) { GameState = 0; count2 = 0; }
if(count2==0) { ZeroMemory(&Player,sizeof(Player)); Player.CreateFlight(0,g_pDisplay); Player.Teleport(300,500); }
if(KeyDown(200)) Player.Move(0); if(KeyDown(208)) Player.Move(1); if(KeyDown(203)) Player.Move(2); if(KeyDown(205)) Player.Move(3);
g_pDisplay->Clear(); MapScroll(); Player.DrawFlight(count2/10,g_pFlight); g_pDisplay->Present(); return count2++; }
저기 위에 제시된것처럼 함수를 고쳤는가? 한번 살펴보자. 흠.. 근데 지금까지 다 배운것들이라 이해안가는 부분은 없을 것 같다. 흠.. 정말 설명할게 없다. 참고로 200, 208, 203, 205는 위로, 아래로, 좌, 우 방향키의 키번호이다. Player.DrawFlight(count2/10,g_pFlight)에서 count2/10 은 10프레임 간격으로 그림을 교체한다는 것이다. 전에 애니메이션에 대해서 설명할때 했었지? "프레임수 / 프레임 간격 % 그림수" 로 그림번호를 얻을 수 있다고. %는 DrawFlight() 함수안에 있음을 주의하자. 그외에는 따로 설명할게 없다. 한번 소스를 훑어 보기 바란다. 그러고보니 오늘은 정말 한게 없다. 사실 이제부터는 배운것들을 응용하는 것이기 때문에 자기능력에 달린것이다. 다음번에는 충돌구현을 한번해보자. | ||||
|
| ||
| ||
안녕하세요? 오랜만에 다시 들어와보니 조회수가 380이 넘은 글도 있고 또 누가 4.5점을 줘서 너무 기쁘네요. 이번에는 2D에서의 충돌처리에 대해서 알아볼까 해요. 모르고 Windows2000 으로 바꾸다가 예제소스를 다 날려 버렸어요-_ㅠ 라고 생각하고 있을때 wo.to 무료 도메인 받아논거에 올려논게 있어서 다행스럽게 다시 쓸 수 있게 됬어요. 참고로 제 도메인 주소는 www.power8624.wo.to 에요. 근데 사실 들어가봐도 별거 없어요. 그냥 자잘한 파일 창고 랄까요?ㅎ
6.2D에서의 충돌처리
음.. 모두들 소스파일은 다 다운받았는가? 오랜만이라 좀 어색하군. 지금까지 한것이 무언가? 메뉴를 구현하고 배경을 스크롤 하고 비행기를 띄웠다. 그런데 한가지 문제점이 있었다. 알아냈는가? 비행기가 화면밖으로 나가는것이다. 자! 이제 화면밖으로 못나가게 한번만들어보자.
int CFlight::Move(int m_head) { switch(m_head) { case 0: y -= ((y-speed<000)?0:speed); break; case 1: y += ((y+speed>504)?0:speed); break; case 2: x -= ((x-speed<150)?0:speed); break; case 3: x += ((x+speed>586)?0:speed); break; } return 0; }
CFlight::Move() 함수를 위에처럼 만들어보고 빌드를 해보아라. 어떤가? 비행기가 밖으로 나가지 않는가? 그러면 성공이다. 참고로 말해두는데 지금 이소스는 예제 소스 이므로 예외적인 경우는 제외 했다. 1945를 보면 적기가 화면으로 밖으로나가고 들어오고 하지 않는가? 그런경우는 배제해두었다는 것이다. 이부분은 알아서 생각해보아라^^ 힌트를 주자면 플레이어기만 밖으로 나가지 말아야 한다. 즉 CFlight::CreateFlight() 함수의 인자인 m_model를 삼항연산자에서 체크해주어 이값이 0이면 좌표값을 제한하게 만들면 되는것이다. 결국 다가르쳐 줬군.
이제 위의 CFlight::Move() 함수를 한번보자. case0: 아래를 보면 y 에다가 어떤 값을 빼는데 그값은 y-speed 가 0보다 적다면 0이고 아니라면 speed 이다. 즉 위로 speed 만큼 움직였을때 y 의 좌표값이 0보다 적다면 움직이지 않게 다시 말하면 0을 빼주는 것이다. 이해가 됬는가? 그러면 이제 case1: 아래를 보자. y+speed가 504보다 적으면 0을 빼고 그렇지 않다면 speed를 뺀다. 왜 504인가? 분명 우리는 800*600 해상도를 사용하므로 600을 줘도 될 것 같다. 하지만 그건 잘못된 생각이다. 여기서 y 는 기체의 크기를 RECT값으로 본다면 RECT.top 이다. 즉 우리는 case1 에서 RECT.bottom 의 값이 600을 넘는지를 알아보아야 하는것이다. 다시 말해 기체의 아랫부분이 600을 넘는가를 체크해야 되는데 기체의 윗부분의 좌표값인 y 가 600을 넘는가를 보면 되는게 아니란말이다. 정리하자면 y+96 이 600을 넘는가 봐야 하므로 식은 y+96>600 이다. 여기서 96을 오른쪽으로 넘기면 y>600-96 이므로 y>504 (중학수학까지 배웠다면 알것이다.)가 되는것이다. 나머지 case2, case3 도 같다.
우리가 방금 한 기체가 밖으로 나가는 것을 막는 부분에서 2D에서의 충돌처리에 대한 개념을 익힐 수 있었다. 그렇지 않다고? 그래 충돌처리를 구현해본것은 아니다. 하지만 충돌처리도 저것과 다를바가 없다. 단순히 left, top, right, bottom 의 값이 임의의 영역을 넘어서는가 만을 체크 하면 되는것이다. 그렇다면 left, top, right, bottom 이 필요하다. 즉 RECT값이 필요하다. CFlight 클래스에 전역으로 RECT형 변수 domain과 domain_b 를 만들고 다음과 같이 만들어보자.
int CFlight::CreateFlight(int m_Model, CDisplay *m_pDisplay) { //중략 switch(m_Model) { case 0: speed = 5; domain_b.left = 5; domain_b.top = 12; domain_b.right = 59; domain_b.bottom = 76; }
return 0; }
int CFlight::Move(int m_head) { //중략 domain.left = x+domain_b.left; domain.top = y+domain_b.top; domain.right = x+domain_b.right; domain.bottom = y+domain_b.bottom;
return 0; }
<그림1>
두 함수를 위에처럼 바꾸었는가? 그럼 한번 살펴보자. CFlight::CreateFlight() 함수부터 보면 domain_b 값을 넣어주는게 보일 것이다. 저 값들은 플레이어 기체의 영역이다. 이렇게 말하니 이해가 안가지? 플레이어 기체가 (0,0)의 좌표에 있을경우의 영역이다. 이것을 그림으로 나타내자면 <그림1>과 같다. 그림으로 보니깐 이해가 확 오는가? 자 그러면 이제 우리는 기체의 영역을 만들었다. 이제 임의의 영역과 겹쳐지는가만을 확인하면 되는것이다. CFlight 클래스 안에 int형 반환값을 가지는 Crash(RECT m_domain) 라는 함수를 만들어보자.
int CFlight::Crash(RECT m_domain) { RECT rt1,rt2; rt1 = domain; rt2 = m_domain; if(rt1.top<=rt2.bottom&&rt1.bottom>=rt2.top&&rt1.right>=rt2.left&&rt1.left<=rt2.right) { return 1; } return 0; }
int CGame::Play() { static CFlight Player; static CFlight Enemy1;
if(count2==0) { ZeroMemory(&Player,sizeof(Player)); ZeroMemory(&Enemy1,sizeof(Enemy1)); Player.CreateFlight(0,g_pDisplay); Player.Teleport(320,500); Player.Move(4); Enemy1.CreateFlight(0,g_pDisplay); Enemy1.Teleport(320,0); Enemy1.Move(4); }
if(KeyDown(200)) Player.Move(0); if(KeyDown(208)) Player.Move(1); if(KeyDown(203)) Player.Move(2); if(KeyDown(205)) Player.Move(3);
if(KeyDown(1,1)||Player.Crash(Enemy1.domain)) { GameState = 0; count2 = 0; return 0; }
g_pDisplay->Clear(); MapScroll(); Player.DrawFlight(count2/10,g_pFlight); Enemy1.DrawFlight(count2/10,g_pFlight); g_pDisplay->Present(); return count2++; }
위의 두함수처럼 수정해보자. 그리고 실행시켜 보아라. 기체가 하나더 있고 그쪽으로 다가가 부딫히면 게임은 종료된다. 즉 충돌처리가 된것이다. 한번 자세하게 알아보자. CFlight::Crash() 함수를 보면 if 문이 하나 보일것이다. 그 위에것은 뭐 당연히 무슨소리인지 알겠지. if 문을 보자. 두개의 RECT 영역의 left, top, right, bottom 을 검사하고 있다. <그림2>를 보자. 그림과 같이 된경우가 충돌된 경우라 할 수 있겠는가? 그러면 저 모든 경우 if 문을 충족시키는지 알아보자. 사각영역에서 왼쪽선은 left, 오른쪽 선은 right, 위쪽 선은 top, 아래쪽 선은 bottom이다.
<그림2>
자 그러면 빨간색을 알아보자. rt1 을 빨간색이라고 하고 rt2를 파란색이라고 하자. rt1.top<=rt2.bottom 부터 보자. 다시말하면 빨간색의 위쪽 선이 파란색의 아래쪽 선보다 높은위치에 있는가(좌표값이 적은가)하는것이다. 충족시키는가? 그리고 다음으로 rt1.bottom>=rt2.top 을 보자. 이것은 빨간색의 아래쪽 선이 파란색의 위쪽 선보다 좌표값이 많은가 즉, 아래쪽에 있는가 하는것이다. (DirectDraw의 좌표계는 모니터의 가장 왼쪽 윗부분이 0,0 이고 오른쪽으로 갈수록 x값은 늘어나고 내려갈수록 y값이 늘어난다.) 이것 역시 충족한다. rt1.right>=rt2.left 또한 빨간색의 오른쪽선이 파란색의 왼쪽 선보다 오른쪽에 있으므로 충족한다. 마지막으로 rt1.left<=rt2.right 의 경우또한 빨간색의 왼쪽선이 파란색의 오른쪽 선보다 왼쪽에 있으므로 충족한다. 다른 사각영역들도 모두 비교해보아라. 모두 파란색과 비교했을 시에 if문은 충족된다.
신기하지 않은가? 어떤이들은 충돌처리 하는 방법에 대해서 말할때 위에서 아래로 부딫힐경우, 왼쪽에서 오른쪽으로 부딫힐경우 등등 따로 설명을 한다. 하지만 저런식으로 하면 신기하게도 어느방향에서 충돌을 하던간에 모두 충족된다. 자 이로써 충돌했는가를 체크했다. 다시 if문을 보면 모두 충족할때 즉 충돌햇을때 1을 반환하고 그렇지 않으면 0을 반환한다.
이제 CGame::Play()를 보자. 그다지 어려운것은 없고. 텔레포트 시키자 마자 CFlight::Move() 함수를 호출해서 domain값 즉 영역값이 입력되게 했다. 그리고 충돌이 되면은 ESC키를 눌렀을 경우와 똑같은 일을 하도록 했다. 오늘은 여기까지만 하겠다. 오늘 알아본것은 충돌처리 이다. 만약 충돌처리가 이해가 안되면 그림2 를 두세번 살펴보아라. 다음번에는 총알을 구현해볼까 하는데. 어차피 지금까지 배운것의 응용이니깐 혼자서 총알을 구현해보는것도 좋을듯 하다. | ||
|
'- 음악과 나 - > 『 짬 통 』' 카테고리의 다른 글
DirectX 9.0 SDK Update - summer 2004 (0) | 2006.05.08 |
---|---|
겜 프로그래밍 강좌 (0) | 2006.05.06 |
네모네모 로직 (0) | 2006.05.06 |
저수준 제어를 이용한 WAVE 재생 (0) | 2006.05.06 |
음성채팅 (0) | 2006.05.06 |