2D 게임 프로그래밍 (1) | 간단한 윈도우 만들기
다음 글을 참고하여 작성하는 글입니다. 개인적으로 공부하는 내용이므로 틀린 부분이 있을 수 있습니다. 있다면 알려주세요.
http://3dapi.com/bs11_2d_basic/
[들어가기 전에]
Windows 운영체제를 배경으로 하는 2D 게임을 만드는 방법엔 윈도우의 GDI(Graphics Device Interface)를 이용하는 방법과 Direct3D, OpenGL 등의 그래픽 라이브러리를 이용하는 두 가지 방법이 있다. GDI는 MS Windows에서 제공해주는 API 중 하나로 정해진 로직과 다양한 기능으로 사용자로 하여금 쉽게 그래픽 객체를 모니터에 출력하도록 도와준다. 하지만 많은 하드웨어의 도움없이 소프트웨어 만으로 커버하기 때문에 높은 퀄리티의 그래픽을 빠르게 처리하기엔 무리가 있다.
DirectX와 OpenGL과 같은 그래픽 라이브러리는 사용자로 하여금 SDK(Software Development Kit)에 대한 높은 이해도를 요구한다. 하지만 이러한 그래픽 라이브러리는 GDI와 달리 그래픽 카드의 도움을 많이 받을 수 있어, 그래픽의 퀄리티와 성능에 있어 GDI를 압도한다. 오늘날의 대부분의 게임은 주로 그래픽 라이브러리를 사용하여 만들어진다.
기본적으로 우리는 윈도우즈 OS에서 동작하는 프로그램을 만들 것이기 때문에 윈도우 프로그램에 대한 이해를 할 필요가 있다. 따라서 해당 포스팅에선 간단한 윈도우를 생성하는 것부터 시작한다. 참고로 DirectX는 버전 9를 사용한다. 버전 11을 원한다면 다음 링크를 참고하자.
https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-devices-create-ref
혹은 다음 DirectX SDK 를 다운받아 튜토리얼 코드를 분석하며 공부해도 된다.
https://www.microsoft.com/en-us/download/details.aspx?id=6812
[윈도우 프로그램]
윈도우 프로그램은 다양한 작업 처리를 일종의 메세지(Message)를 통해서 처리하고 관리한다. 예를 들어 사용자의 키보드 입력 또는 마우스 변화와 같은 시스템 변화가 운영체제로 전달되면 운영체제는 이러한 것을 메세지로 만들어서 일종의 메세지 큐에 저장한다. 프로그램은 GetMessage(), PeekMessage()와 같은 함수를 통해 메세지 큐에서 하나의 메세지를 가져와서 처리한다. 보통 프로그램 내에 이러한 메세지를 반복적으로 가져오는 루프와 처리하는 함수를 구현한다. 메세지 루프는 보통 WM_QUIT라는 메세지를 받으면 종료한다. 우선은 윈도우 프로그램은 메세지 기반의 프로그램이라는 것과 동작 방식에 대해서만 기억해두자. 다음은 이러한 동작 방식을 나타낸 그림이다. (더 자세한 내용 클릭)
이러한 메세지 루프를 구현해놓으면, 메세지를 처리하는 함수가 필요하다. 이 함수는 운영체제에 의해 호출되는 함수로 WndProc()라는 이름을 가지고 있으며, 사용자가 구현해야 한다. WndProc()은 메세지 처리가 핵심적인 역할이며, 프로그램마다 그 상세 구현이 다르다. WndProc()이라는 함수와 별개로 WinMain()이라는 함수가 존재한다. 콘솔 프로그램에서의 엔트리 포인트(진입점)이 main() 이었다면, 윈도우 프로그램에서의 진입점은 WinMain()이다.
다음은 윈도우 기반의 프로그램 코드 흐름이다. 코드가 복잡해 보일 수 있지만 전체적인 과정만 이해하면 된다.
main() or WinMain() → 프로그램 핸들(hInstance) 얻기 → 윈도우 클래스 등록 → 윈도우 생성
→ Run → 윈도우 소멸 → 윈도우 클래스 해제
위의 과정을 코드로 구현해 보자. 다음은 윈도우를 띄우는 기본적인 예제이다. (혹은 소스코드 파일로 첨부)
// SimpleWindow.h
/*
*************************************
2D Game programming #1
*************************************
윈도우 기반의 프로그램 코드 흐름
1. main()
2. 프로그램 핸들 얻기
3. 윈도우 클래스 등록
4. 윈도우 생성
5. Run (Do something)
6. 윈도우 소멸
7. 윈도우 클래스 해제
Create -> Run -> CleanUp
*/
#pragma once
#pragma comment(linker, "/subsystem:windows") /* 윈도우 창을 띄울 수 있게 해줍니다. */
#include <windows.h>
#include <memory> // std::unique_ptr
/* 윈도우 정보 */
struct WindowInfo
{
DWORD WinStyle = WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_VSCROLL;
INT WinX = 20;
INT WinY = 10;
INT WinWidth = 800;
INT WinHeight = 600;
};
class SimpleWindow final
{
public:
SimpleWindow(LPCWSTR name, HINSTANCE hInst = GetModuleHandle(NULL));
INT Create(); /* 윈도우 클래스를 생성 및 등록 & DX 초기화 */
INT Run(); /* 게임 루프 실행 */
private:
static LRESULT MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); /* Wrapper(WndProc에서 호출) */
static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); /* 메세지 처리 루프 */
static void CleanUp(); /* 데이터 소멸 */
static std::unique_ptr<WindowInfo> mWinInfo;
LPCWSTR mName;
HINSTANCE mhInst; /* 프로그램 핸들 */
HWND mhWnd; /* 윈도우 핸들 */
};
// SimpleWindow.cpp
#include "SimpleWindow.h"
/* static 멤버 변수 초기화 */
std::unique_ptr<WindowInfo> SimpleWindow::mWinInfo = std::make_unique<WindowInfo>();
/* 생성자에서 프로그램 핸들을 얻습니다. */
SimpleWindow::SimpleWindow(LPCWSTR name, HINSTANCE hInst)
: mName(name)
, mhInst(hInst)
, mhWnd()
{
}
LRESULT SimpleWindow::MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_KEYDOWN:
{
switch (wParam)
{
case VK_ESCAPE:
{
SendMessage(hWnd, WM_DESTROY, 0, 0);
break;
}
}
return 0;
}
case WM_DESTROY:
{
CleanUp();
PostQuitMessage(0); /* 메세지큐에 WM_QUIT 메세지 전달 */
return 0;
}
}
return DefWindowProc(hWnd, msg, wParam, lParam); /* 프로그램이 처리하지 않은 잡다한 메세지를 처리 */
}
LRESULT WINAPI SimpleWindow::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
return MsgProc(hWnd, msg, wParam, lParam);
}
INT SimpleWindow::Create()
{
/* 윈도우 클래스를 등록합니다. */
WNDCLASS wc =
{
CS_CLASSDC
, WndProc /* 메세지 처리 함수인 WndProc을 등록해주어야 함 */
, 0L
, 0L
, mhInst /* WinMain으로부터 받은 프로그램 핸들(인스턴스) */
, NULL
, LoadCursor(NULL, IDC_ARROW)
, (HBRUSH)GetStockObject(LTGRAY_BRUSH)
, NULL
, mName /* 윈도우 클래스 이름 */
};
RegisterClass(&wc);
/* 윈도우 영역을 정해줍니다. 지금은 크게 신경X */
RECT rc;
SetRect(&rc, 0, 0, 500, 300);
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW | WS_VISIBLE, FALSE);
/* 윈도우를 생성합니다. */
mhWnd = CreateWindow
(
mName /* WNDCLASS 타입 객체의 윈도우 클래스 이름과 동일 */
, mName /* 윈도우 타이틀 바에 띄울 이름 */
, mWinInfo->WinStyle
, 20 /* x */
, 10 /* y */
, mWinInfo->WinWidth /* width */
, mWinInfo->WinHeight /* height */
, GetDesktopWindow() /* 부모 또는 소유주 윈도우의 핸들 지정 */
, NULL
, mhInst
, NULL
);
ShowWindow(mhWnd, SW_SHOW);
UpdateWindow(mhWnd);
ShowCursor(TRUE);
/*
***********************
여기에 DX를 초기화합니다.
***********************
*/
return 0;
}
INT SimpleWindow::Run()
{
// 메세지 루프로 진입합니다.
MSG msg;
memset(&msg, 0, sizeof(msg));
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
/*
**************
게임 루프 작성
**************
*/
}
}
UnregisterClass(mName, mhInst);
return 0;
}
void SimpleWindow::CleanUp()
{
/*
***********************
게임 데이터 + DX 해제
***********************
*/
}
// main.cpp
#include "SimpleWindow.h"
using namespace std;
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, INT)
{
SimpleWindow win1(L"firstWindow", hInstance);
if (FAILED(win1.Create()))
{
return -1;
}
return win1.Run();
}
1. WinMain() & 프로그램 핸들 얻기
윈도우 프로그램의 진입점이다. OS로부터 전달받은 첫 번째 인자(프로그램 핸들)만 사용되며, 클래스 생성자를 통해 프로그램 핸들을 전달해준다. 그리고 Create()와 Run() 이라는 멤버 함수를 통해 프로그램을 초기화하고 실행한다. FAILED는 실패 시 TRUE를 반환하는 매크로이다. 만약 main 함수를 사용한다면 프로그램 핸들을 얻을 수 없다. 이런 경우 GetModuleHandle()이라는 함수를 통해 핸들을 얻을 수 있다. 따라서 생성자의 두 번째 인자를 비우면 기본값으로 GetModuleHandle()를 호출하도록 해 놓았다.
2. 윈도우 클래스 등록
윈도우 클래스를 생성하기 위해선 반드시 윈도우 클래스가 먼저 등록이 되어 있어야 한다. 따라서 먼저 윈도우 클래스를 등록해준다. 윈도우를 등록하기 위해 먼저 WNDCLASS라는 구조체 타입의 객체를 생성하고 데이터를 채운다. 중요한 데이터로는 운영체제가 메세지를 처리하기 위해 호출하는 WndProc과 프로그램 핸들, 윈도우 클래스 이름이 있다. 데이터를 채우고 RegisterClass() 함수를 통해 등록한다.
3. 윈도우 클래스 생성
CreateWindow() 함수를 통해 윈도우를 생성한다. 윈도우 생성에 실패하면 NULL을 반환하며, 성공하면 윈도우 핸들을 반환한다. 이 반환값을 HWND라는 윈도우 핸들 타입의 변수에 저장해야 한다. 중요한 값이니 기억해두자.
4. Run (Do Something)
실질적인 게임 로직을 처리하는 부분이다. 운영체제의 메세지 큐에서 메세지를 받아오다가 WM_QUIT라는 메세지를 받게 되면 윈도우를 소멸시킨다. 이후 UnregisterClass()를 통해 윈도우 클래스를 해제한다. 해당 예제에선 PeekMessage() 라는 함수를 통해 메세지를 받아오는데, 이외에 GetMessage()라는 함수도 존재한다.
GetMessage()와 PeekMessage()는 모두 메세지큐에 메세지가 존재한다면 메세지를 받아온다. 하지만 메세지큐가 비어있다면 GetMessage()는 메세지가 생성될 때까지 기다리고, PeekMessage()는 기다리지 않고 바로 리턴을 한다. 때문에 GetMessage()과 같이 맹목적으로 기다리는 것은 CPU를 낭비하는 일이다. 따라서 PeekMessage()를 사용하여 메세지가 없을 땐 다른 작업을 할 수 있도록 해준다.
여기까지 간단한 윈도우를 띄우는 프로그램을 작성하였고, 여기에 조금씩 DirectX 관련 코드를 추가할 것이다.