728x90

원문 : http://www.rastertek.com/dx11tut02.html



Tutorial 2: Creating a Framework and Window


DirectX 11코드를 작성하기 전에 간단한 프레임워크를 만들기를 권합니다. 이 프레임워크는 기본 윈도우 기능을 다루며 조직적이고 읽기 좋은 방식으로 코드를 확장하는데 용이할 것 입니다. 이 강좌들의 의도는 DirectX 11만의 특성들을 다루기 위함이기 때문에 가능한한 가볍게 프레임워크를 유지할 것 입니다. (변화가 많나?)




The Framework


프레임워크는 네가지 항목으로 시작할 것입니다. 어플리케이션의 진입점을 제어하기 위해 WinMain함수를 가질 것이며 또 WinMain함수 안에서 호출되어 전체 어플리케이션을 집약하는 system 클래스를 가질 것입니다. system 클래스 내부에는 사용자 입력을 처리하기 위한 input 클래스와 DirectX 그래픽스 코드 처리를 위한 graphics 클래스를 가질 것입니다. 프레임워크 설정의 다이어그램은 다음과 같습니다.

이제 프레임워크가 어떻게 설정이 될지를 보기위해 main.cpp파일 내부에 WinMain함수를 보도록 하겠습니다.


WinMain

//////////////////////////////////////////////////////////////////////////////// // Filename: main.cpp //////////////////////////////////////////////////////////////////////////////// #include "systemclass.h" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow) { SystemClass* System; bool result; // 시스템 객체 생성 System = new SystemClass; if(!System) { return 0; } // 시스템 객체 초기화 후 Run()호출 result = System->Initialize(); if(result) { System->Run(); } // 시스템 객체 정리 후 할당 해제 System->Shutdown(); delete System; System = 0; return 0; }




보다시피 WinMain함수가 상당히 간단합니다. system 클래스를 만들고 초기화합니다. 만약 문제없이 초기화가 된다면 system 클래스의 Run()함수를 호출합니다. Run()함수는 내부 루프를 실행시켜 어플리케이션이 끝날때까지 동작합니다. Run()함수가 끝난 후에 Shutdown()함수를 호출하여 system 객체를 정리합니다. 그래서 system 클래스 내부에 간단하고 캡슐화된 전체 어플리케이션이 유지됩니다. 이제 system 클래스 헤더파일을 보도록 해 봅시다.


Systemclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_

여기 밑에 WIN32_LEAN_AND_MEAN를 정의하였습니다. 그로인해 사용되지 않는 API들을 제외함으로써 Win32 헤더파일들의 크기를 줄여 빌드 속도를 향상시킵니다.

///////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///////////////////////////////
#define WIN32_LEAN_AND_MEAN

Win32 API들의 사용과 창을 생성/제거하는 함수들을 호출하기 위해 Windows.h를 include 합니다.

//////////////
// INCLUDES //
//////////////
#include <windows.h>

이번 프레임워크의 두 클래드들의 헤더파일을 include하였습니다. system 클래스에서 사용할 수 있습니다.

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "graphicsclass.h"

system 클래스의 정의는 상당히 간단합니다. 위에 정의된 WinMain에서 호출되었던 Initialize(), Shutdowon(), Run()가 보입니다. 이들 함수들에서 호출될 몇몇 private속성의 함수들도 있습니다. 또한 어플리케이션이 실행되는 동안 윈도우 시스템 메시지를 처리할 MessageHandler가 있습니다. 그리고 마지막으로 그래픽스와 입력을 처리할 두 객체 포인터 m_Input와 m_Graphics를 private속성으로 가집니다.

////////////////////////////////////////////////////////////////////////////////
// Class name: SystemClass
////////////////////////////////////////////////////////////////////////////////
class SystemClass
{
public:
	SystemClass();
	SystemClass(const SystemClass&);
	~SystemClass();

	bool Initialize();
	void Shutdown();
	void Run();

	LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);

private:
	bool Frame();
	void InitializeWindows(int&, int&);
	void ShutdownWindows();

private:
	LPCWSTR m_applicationName;
	HINSTANCE m_hinstance;
	HWND m_hwnd;

	InputClass* m_Input;
	GraphicsClass* m_Graphics;
};


/////////////////////////
// FUNCTION PROTOTYPES //
/////////////////////////
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);


/////////////
// GLOBALS //
/////////////
static SystemClass* ApplicationHandle = 0;

#endif


WndProc 함수와 ApplicationHandle 포인터가 이 클래스에 포함됨으로 윈도우 시스템 메시지를 system 클래스 내부의 MessageHandler 함수로 리다이렉트 할 수 있습니다.

이제 system 클래스의 소스파일을 보도록 해 봅시다.


Systemclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"

클래스 생성자 함수에서 객체 포인터들을 null로 초기화 합니다. 만약 이 객체들의 초기화를 실패할 경우 Shutdown() 함수는 그 객체들을 정리하려 할 것입니다. 그런데 객체가 null이 아닐 경우 유효하게 생성된 객체로 추정하여 메모리가 할당되지 않았음에도 객체를 정리하고 해제하려 할 것입니다. 때문에 포인터를 null로 초기화하는 것은 중요합니다. 모든 변수와 포인터들을 초기화하는 것은 좋은 연습입니다. 그렇지 않으면 몇몇 release 빌드에서 실패할 수도 있습니다.

SystemClass::SystemClass()
{
	m_Input = 0;
	m_Graphics = 0;
}

빈 복사 생성자와 클래스 소멸자를 만들었습니다. 이번 클래스에는 필요하지 않지만 만약 정의되지않으면 몇몇 컴파일러들이 생성합니다. 그 경우 차라리 복사 생성자와 소멸자가 비어있는 것이 나아 만들어 두었습니다.


이 소멸자에서 아무것도 정리하지 않는것을 알아차리셨을 것입니다. 대신에 밑에서 살펴볼 Shutdown() 함수에서 모든 객체 정리를 수행합니다. 그 이유는 소멸자가 호출될 지를 신뢰하지 않기 때문입니다. 확실히 ExitThread()같은 몇몇 윈도우 함수들은 클래스 소멸자를 호출하지 않아 메모리 누수를 낳는 것으로 알려져 있습니다. 물론 더 안전한 버전의 윈도우 함수들을 호출 할 수 있지만 저는 윈도우에서 프로그래밍 할때 조심하고 있습니다.

SystemClass::SystemClass(const SystemClass& other)
{
}


SystemClass::~SystemClass()
{
}

이어서 Initialize() 함수는 어플리케이션에 대한 모든 설정을 수행합니다. 먼저 우리의 어플리케이션이 사용할 윈도우(창)을 생성하는 InitializeWindows() 함수를 호출합니다. 또 사용자 입력 처리와 화면상에 그래픽을 렌더링하는데 사용할 input객체와 graphics객체를 만들고 초기화합니다.

bool SystemClass::Initialize() { int screenWidth, screenHeight; bool result; // 함수안으로 변수를 전달하기전에 화면 너비와 높이를 0으로 초기화한다. screenWidth = 0; screenHeight = 0; // windows api를 초기화한다. InitializeWindows(screenWidth, screenHeight); // input 객체 생성. 이 객체는 사용자로부터 키보드를 읽는 것을 처리하는데 사용될 것이다. m_Input = new InputClass; if(!m_Input) { return false; } // input 객체 초기화. m_Input->Initialize(); // graphics 객체 생성. 어플리케이션을 위한 모든 그래픽을 렌더링하는 것을 처리한다. m_Graphics = new GraphicsClass; if(!m_Graphics) { return false; } // graphics 객체 초기화. result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd); if(!result) { return false; } return true; }

Shutdown() 함수는 정리를 수행합니다. graphics, input 객체에 관련된 모든 것을 멈추고 해제합니다. 윈도우도 shutdown하고 관련 핸들들을 정리합니다.

void SystemClass::Shutdown() { // graphics 객체 해제. if(m_Graphics) { m_Graphics->Shutdown(); delete m_Graphics; m_Graphics = 0; } // input 해제. if(m_Input) { delete m_Input; m_Input = 0; } // 윈도우 shutdown. ShutdownWindows(); return; }

Run() 함수는 우리가 끝낼때까지 우리의 어플리케이션이 돌고 처리, 연산하는 곳 입니다. 어플리케이션 연산처리는 각 루프에서 호출되는 Frame() 함수에서 수행됩니다. 이는 Run() 함수와 반드시 작성되는 어플리케이션의 다른 부분을 깊이 이해하는데 중요한 개념입니다. 의사코드는 아래와 같이 봅니다.


while done이 0일때 윈도우 시스템 메시지 확인 시스템 메시지 처리 어플리케이션 루프 수행 frame처리동안 사용자가 종료하기를 원하는지 확인


void SystemClass::Run() { MSG msg; bool done, result; // 메시지 구조체 초기화. ZeroMemory(&msg, sizeof(MSG)); // 윈도우나 사용자로부터 종료 메시지가 있을때까지 돈다. done = false; while(!done) { // 윈도우 메시지를 처리한다. if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } // 윈도우가 어플리케이션 종료를 보내면 루프를 종료한다. if(msg.message == WM_QUIT) { done = true; } else { // 종료 메시지 외 에는 frame 처리를 한다. result = Frame(); if(!result) { done = true; } } } return; }

다음 Frame() 함수는 우리의 어플리케이션이 수행되는 것에 관한 모든 처리를 하는 곳 입니다. 상당히 간단한데 사용자가 Esc키를 눌렀고 프로그램을 종료하기를 원하는지를 보는 input 객체를 확인합니다. 만약 사용자가 종료를 원치 않을 경우 해당 프레임을 위한 그래픽 렌더링을 처리할 graphics 객체의 Frame() 함수를 호출합니다. 어플리케이션이 커짐에 따라 여기에 많은 코드를 작성할 것입니다.

bool SystemClass::Frame() { bool result; // 사용자가 Esc키를 눌렀고 프로그램을 종료하기를 원하는지 확인. if(m_Input->IsKeyDown(VK_ESCAPE)) { return false; } // graphics 객체의 Frame() 함수 호출. result = m_Graphics->Frame(); if(!result) { return false; } return true; }

MessageHandler() 함수는 윈도우 시스템 메시지가 바로 보내지는 곳 입니다. 이 방법으로 우리는 우리가 원하는 특정 정보를 들을 수 있습니다. 현재 우리는 키가 눌렸는지 떼어졌는지를 읽으며 그 정보를 input 객체에게 전달 할 것입니다. 다른 모든 정보는 윈도우 default 메시지 처리기로 돌려보낼 것입니다.

LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) { switch(umsg) { // 키보드상의 키가 눌렸는지를 확인한다. case WM_KEYDOWN: { // 만약 키가 눌렸다면 input객체에게 key를 전달한다. 그래서 상태를 기록한다. m_Input->KeyDown((unsigned int)wparam); return 0; } // 키보드상의 키가 떼어졌는지를 확인한다. case WM_KEYUP: { // 만약 키가 떼어졌다면 input객체에게 전달한다. 그래서 상태를 지운다. m_Input->KeyUp((unsigned int)wparam); return 0; } // 그 외 다른 모든 메시지는 우리의 어플리케이션은 사용하지 않기 때문에 default 메시지 처리기로 전달한다. default: { return DefWindowProc(hwnd, umsg, wparam, lparam); } } }


InitializeWindows() 함수는 우리가 렌더링하는데 사용할 윈도우(창)을 생성하는 코드가 위치할 곳입니다. screenWidth와 screenHeight를 호출한 함수로 반환하는데 어플리케이션 전반에 걸쳐 사용할 수 있습니다. 우리는 어떠한 테두리도 없는 평평한 검정 화면을 초기화하기 위한 기본 설정을 사용하여 윈도우를 만듭니다. 전역 변수 FULL_SCREEN에 따라 전체화면 혹은 작은 화면을 만들 것입니다. 만약 1로 설정되어있으면 사용자 모니터 화면 전체를 덮는 창을 만듭니다. 만약 0으로 설정되어 있으면 화면 중앙에 800x600크기의 창을 만듭니다. 여러분이 FULL_SCREEN을 수정하기 원하는 경우 graphicsclass.h파일 최상단에 위치하니 확인하시면 됩니다. 왜 systemclass.h파일이 아닌 graphicsclass.h파일에 위치하였는지는 훗날 알게 될 것입니다.

void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight) { WNDCLASSEX wc; DEVMODE dmScreenSettings; int posX, posY; // 외부 포인터로 현재 객체의 주소를 담습니다. ApplicationHandle = this; // 현재 어플리케이션의 인스턴스를 얻습니다. m_hinstance = GetModuleHandle(NULL); // 어플리케이션 이름 설정. m_applicationName = L"Engine"; // 윈도우 클래스를 기본으로 설정. wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.lpfnWndProc = WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = m_hinstance; wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); wc.hIconSm = wc.hIcon; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = m_applicationName; wc.cbSize = sizeof(WNDCLASSEX); // 윈도우 클래스 등록. RegisterClassEx(&wc); // 사용자 환경에 맞춘 화면크기 결정. screenWidth = GetSystemMetrics(SM_CXSCREEN); screenHeight = GetSystemMetrics(SM_CYSCREEN); // 전체 화면 여부에 따른 화면 관련 설정. if(FULL_SCREEN) { // 만약 전체화면모드이면 화면을 최대사이즈로 하고 32비트 컬러로 한다. memset(&dmScreenSettings, 0, sizeof(dmScreenSettings)); dmScreenSettings.dmSize = sizeof(dmScreenSettings); dmScreenSettings.dmPelsWidth = (unsigned long)screenWidth; dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight; dmScreenSettings.dmBitsPerPel = 32; dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT; // 디스플레이 설정을 전체화면으로 변경. ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN); // 창 위치를 왼쪽 최상단에 위치. posX = posY = 0; } else { // 창모드일 겨우 800x600으로 설정. screenWidth = 800; screenHeight = 600; // 창이 화면 중앙에 위치하도록 위치 설정. posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth) / 2; posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2; } // 윈도우(창)을 설정에 맞게 생성하고 해당 윈도우에 대한 핸들을 얻는다. m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName, WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP, posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL); // 윈도우(창)을 가장 위쪽(위일수록 보이며 아래일수록 다른 화면들에 가려진다.)에 둔다.

// 그리고 활성화시킨다. (현재 작업중인 윈도우로) ShowWindow(m_hwnd, SW_SHOW); SetForegroundWindow(m_hwnd); SetFocus(m_hwnd); // 마우스 커서를 숨긴다. ShowCursor(false); return; }

ShutdownWindows() 함수는 이게 다 입니다. 화면 설정을 원래대로 돌리고  윈도우와 관련 핸들을 해제합니다.

void SystemClass::ShutdownWindows() { // 마우스 커서를 보인다. ShowCursor(true); // 전체화면모드이면 화면 설정을 변경한다. if(FULL_SCREEN) { ChangeDisplaySettings(NULL, 0); } // 윈도우(창) 제거. DestroyWindow(m_hwnd); m_hwnd = NULL; // 어플리케이션 인스턴스 제거. UnregisterClass(m_applicationName, m_hinstance); m_hinstance = NULL; // 현재 객체로의 포인터 해제. ApplicationHandle = NULL; return; }

WndProc() 함수는 윈도우가 메시지를 보내는 곳입니다. 여러분들은 우리가 바로 위에 InitializeWindows() 함수에서 wc.lpfnWndProc = WndProc라고 함수이름을 윈도우에게 말해준 것을 알아차리셨을 겁니다. 저는 WndProc() 함수가 SystemClass안에 정의된 MessageHandler() 함수로 모든 메시지를 보내도록 system 클래스로 직접 묶기위해 여기에 포함하였습니다. 이렇게 함으로 메시지기능을 건져서 우리의 클래스로 바로 보낼수 있으며 코드를 깔끔하게 유지할 수 있습니다.

LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam) { switch(umessage) { // 윈도우를 제거중인지 확인한다. case WM_DESTROY: { PostQuitMessage(0); return 0; } // 윈도우를 닫는중인지 확인한다. case WM_CLOSE: { PostQuitMessage(0); return 0; } // 그 외 모든 메시지는 system 클래스의 메시지처리기로 넘긴다. default: { return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam); } } }




Inputclass.h


강좌를 간단하게 유지하도록 DirectInput(엄청나다) 강좌를 할때까지 윈도우 입력을 사용했습니다. input 클래스는 키보드로부터 사용자 입력을 처리합니다. input 클래스는 SystemClass::MessageHandler() 함수로부터 입력을 전달받습니다. input 객체는 키보드배열에 각 키의 상태를 저장할 것입니다. 특정 키가 눌렸는지 질의를 하면 질의하는 함수들에게 그 여부를 알려줄 것입니다. 헤더파일은 다음과 같습니다.


////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_


////////////////////////////////////////////////////////////////////////////////
// Class name: InputClass
////////////////////////////////////////////////////////////////////////////////
class InputClass
{
public:
	InputClass();
	InputClass(const InputClass&);
	~InputClass();

	void Initialize();

	void KeyDown(unsigned int);
	void KeyUp(unsigned int);

	bool IsKeyDown(unsigned int);

private:
	bool m_keys[256];
};

#endif



Inputclass.cpp

//////////////////////////////////////////////////////////////////////////////// // Filename: inputclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "inputclass.h" InputClass::InputClass() { } InputClass::InputClass(const InputClass& other) { } InputClass::~InputClass() { } void InputClass::Initialize() { int i; // 모든 키들을 눌리지 않은 상태로 초기화한다. for(i=0; i<256; i++) { m_keys[i] = false; } return; } void InputClass::KeyDown(unsigned int input) { // 키가 눌릴 경우 키배열에 상태를 저장한다. m_keys[input] = true; return; } void InputClass::KeyUp(unsigned int input) { // 키가 떼어질 경우 키배열을 비활성으로 되돌린다. m_keys[input] = false; return; } bool InputClass::IsKeyDown(unsigned int key) { // 키의 상태가 눌렸는지 눌리지 않았는지를 반환한다.( 1:눌림, 0:안 눌림) return m_keys[key]; }



Graphicsclass.h


graphics 클래스는 system 클래스가 만든 또 다른 객체입니다. 모든 어플리케이션의 그래픽 기능은 이 클래스에 캡슐화 되어 있습니다. 저는 전체화면모드, 창모드 같은 변경하고 싶은 모든 그래픽 관련 전역 설정(변수)들에 대해 이 헤더를 사용할 것입니다. 현재 이 클래스는 비어있지만 나중 강좌에서는 모든 그래픽 객체들로 넘쳐날 것입니다.


////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <windows.h>


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;

위 4개의 전역변수들은 꼭 필요합니다.

////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
	GraphicsClass();
	GraphicsClass(const GraphicsClass&);
	~GraphicsClass();

	bool Initialize(int, int, HWND);
	void Shutdown();
	bool Frame();

private:
	bool Render();

private:

};

#endif



Graphicsclass.cpp


이번 강좌에서는 단순히 프레임워크를 만드는 중인기 때문에 이 클래스 전체를 빈 상태로 유지합니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"


GraphicsClass::GraphicsClass()
{
}


GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}


GraphicsClass::~GraphicsClass()
{
}


bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{

	return true;
}


void GraphicsClass::Shutdown()
{

	return;
}


bool GraphicsClass::Frame()
{

	return true;
}


bool GraphicsClass::Render()
{

	return true;
}



요약


자 이제 우리는 프레임워크와 실행 시 화면이 튀오나오는 윈도우를 만들었습니다. 이 프레임워크는 나중 강좌들의 기본이 될 것으로 이를 이해하는 것은 매우 중요합니다. 다음 강좌로 가기전에 컴파일과 실행이 확실히 되는지 확인하기 위해 '연습하기'를 시도해보세요. 아직 이번 프레임워크를 이해하지 못했다면 괜찮으니 다음 강좌들로 가도 좋습니다. 후에 프레임워크가 여러가지 채워지면 이해할 수 있을겁니다.


연습하기

1. graphicsclass.h의 FULL_SCREEN을 true으로 설정하여 다시 컴파일 후 실행해 보십시오. 윈도우가 출력된 후 Esc버튼을 눌러 종료시키세요.

728x90

+ Recent posts