Direct3D-11 렌더링 엔진 만들기: 2-2 창 클래스 만들기
Direct3D-11 렌더링 엔진 만들기: 2-2 창 클래스 만들기
지난 포스팅을 통해 창을 생성하고 창에서 발생하는 기본 메시지를 처리하는 방법에 대해 살펴봤습니다.
이번에는 WinMain 함수에 모두 작성했던 로직을 별도의 클래스로 분리하는 과정을 진행합니다.
Engine 프로젝트에 Types.h 추가하기
Window 클래스는 콘텐츠를 제작하는 Application 프로젝트에 배치될 필요가 없고, 엔진을 실행할 때 필요한 기본 기능이기 때문에 Engine 프로젝트에 추가합니다.
Header/C++ 파일을 생성하기 전에 폴더를 생성해 정리해가면서 진행하겠습니다.
Engine 프로젝트에서 우클릭 > Add > New Folder를 선택해 새 폴더를 생성하고, Core로 이름을 지정합니다.
생성된 Core 폴더에 Window.h / Window.cpp / Types.h 파일을 각각 추가합니다.
Window 클래스 작성을 하기 전에 Types.h 파일에 앞으로 자주 사용할 타입을 재정의하고 dll/exe 프로젝트 구성에 필요한 매크로를 정의합니다.
Types.h 파일을 열고 아래와 같이 각 타입을 재정의 합니다.
또한 Engine(dll) 프로젝트에서 Application(exe) 프로젝트에 제공할 클래스/구조체는 __declspec(dllexport)를 지정하고,
Application은 Engine으로부터 클래스/구조체 정보를 가져와야 하기 때문에 __declspec(dllimport)를 지정해줘야 합니다.
한 클래스를 위해 클래스 헤더를 2가지 종류로 만드는 것은 번거롭기 때문에 BUILD_DLL이 선언됐는지 여부에 따라 자연스럽게 바뀔 수 있도록 매크로를 선언합니다.
Types.h
#pragma once // dll에서 템플릿을 내보낼 때 발생하는 경고를 끄는 옵션. // dll 내부의 클래스에서 std::vector 등을 사용하면 발생하기 때문에 인지하는 경우에는 끌 수 있음. #pragma warning(disable: 4251) // __declspec(dllexport) / __declspec(dllimport)를 자연스럽게 변경할 수 있도록 매크로 선언. #ifdef BUILD_DLL #define ENGINE_API __declspec(dllexport) #else #define ENGINE_API __declspec(dllimport) #endif // 자주 사용하는 타입에 대한 타입 재정의. using uint8 = unsigned __int8; using int8 = __int8; using uint32 = unsigned __int32; using int32 = __int32; using uint64 = unsigned __int64; using int64 = __int64;
이어서 Window.h 파일을 열고 필요한 변수 및 함수의 선언을 작성합니다.
Window.h
#pragma once #include "Types.h" #include <Windows.h> #include <string> class ENGINE_API Window { public: Window( HINSTANCE instance, uint32 width, uint32 height, const std::wstring& title, WNDPROC messageProcedure ); ~Window(); // Getter. inline const uint32 Width() const { return width; } inline const uint32 Height() const { return height; } const std::wstring& Title() const { return title; } inline const HWND Handle() const { return handle; } private: // 창 가로 크기. uint32 width = 0; // 창 세로 크기. uint32 height = 0; // 창 제목. std::wstring title; // 창 클래스 생성시 사용할 클래스 이름. std::wstring className = TEXT("EngineClass"); // 창 핸들. HWND handle = nullptr; // 프로그램 인스턴스. HINSTANCE instance = nullptr; };
창을 생성하는데 필요한 변수를 기반으로 클래스를 작성합니다.
앞으로 문자열의 경우에는 std::string/std::wstring을 사용하겠습니다.
그리고, wide string의 경우 값을 설정할 때 L”Test”와 같이 설정할 수도 있지만, TEXT(“Test”)와 같이 매크로를 사용하겠습니다.
class ENGINE_API Window와 같이 클래스 이름 앞에 ENGINE_API 매크로가 붙었습니다.
이 매크로는 앞에서 Types.h에 선언했던 매크로인데, BUILD_DLL 이 선언됐는지 여부에 따라서 __declspec(dllexport)또는 __declspec(dllimport)로 바뀝니다.
코드 작성을 완료한 뒤에 Engine 프로젝트에는 프로젝트 설정에서 BUILD_DLL 전처리 선언 구문을 추가할 예정입니다.
이렇게 하면 Engine 프로젝트 내에서 ENGINE_API는 __declspec(dllexport)로 변환되고, Application 프로젝트는 __declspec(dllimport)로 변환됩니다.
헤더 작성이 완료되었으면, Windows.cpp 파일을 이어서 작성합니다.
WinMain에 작성했던 내용을 기반으로 클래스에 맞게 약간 조정한 버전입니다.
잘 살펴보시면 로직은 같다는 것을 알 수 있습니다.
Window.cpp
#include "Window.h" Window::Window( HINSTANCE instance, uint32 width, uint32 height, const std::wstring& title, WNDPROC messageProcedure) : instance(instance), width(width), height(height), title(title) { // 윈도우 클래스 구조체 설정. WNDCLASS wc = { }; wc.lpfnWndProc = messageProcedure; // 창에서 발생한 메시지를 처리할 때 호출될 함수. wc.hInstance = instance; // 프로그램 핸들 값 전달. wc.lpszClassName = className.c_str(); // 클래스 이름 설정. // 클래스 등록. if (RegisterClass(&wc) == false) { // 클래스 등록에 실패하면 이 코드에서 중단점이 실행되도록 중지 처리. __debugbreak(); } // 창 크기 조정. // 창을 생성할 때 타이틀 바 등이 차지하는 영역을 제외한 // 실제 그려지는 영역의 크기를 제대로 구하기 위한 코드. RECT rect = { 0, 0, static_cast<long>(width), static_cast<long>(height) }; AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false); // 실제 Direct3D를 활용해 그리는 공간을 1280/800으로 확보한 실제 창 크기 다시 구하기. int windowWidth = rect.right - rect.left; int windowHeight = rect.bottom - rect.top; // 창 위치 구하기. // 모니터의 가운데 위치 구하기. int xPosition = (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2; int yPosition = (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2; // 창 생성. handle = CreateWindowEx( 0, // 윈도우 스타일(옵션). className.c_str(), // 윈도우 클래스 이름. title.c_str(), // 제목에 보여줄 타이틀. WS_OVERLAPPEDWINDOW, // 창에 적용할 스타일. // 위치. xPosition, yPosition, // 크기. windowWidth, windowHeight, nullptr, // 부모 창. nullptr, // 메뉴. instance, // 프로그램 핸들 값. nullptr // 추가로 전달할 어플리케이션 데이터. ); // 창 생성에 실패한 경우, 프로그램 종료. (오류 코드 -1). if (handle == nullptr) { // 창 생성에 실패하면 이 코드에서 중단점이 실행되도록 중지 처리. __debugbreak(); } // 창 보여주기. ShowWindow(handle, SW_SHOW); // 업데이트 반영. UpdateWindow(handle); } Window::~Window() { // 클래스 등록 해제. UnregisterClass(className.c_str(), instance); }
기존에 사용하던 exit(-1) 대신, 디버그 모드에서 실행할 경우 정지가 되도록 __debugbreak() 함수를 사용했습니다.
또한 문자열은 const wchar_t* 대신 std::wstring으로 대체했고, 윈도우 메시지 처리를 위한 함수 포인터를 전달하는 부분도 생성자의 인자로 전달받을 수 있도록 구성했습니다.
창 생성에 필요한 코드 작성은 완료되었습니다.
이제 Engine 프로젝트에서 필요한 전처리 구문 선언을 추가합니다.
솔루션 탐색 창의 Engine 프로젝트 이름에서 우클릭 > 속성을 선택합니다.
아래 그림을 참고해서 왼쪽 메뉴에서 C++ > Preprocessor 항목을 선택하고, 오른쪽 메뉴에서 Preprocessor Definition 항목 맨 앞에 BUILD_DLL; 을 추가합니다.
이렇게 하면 Engine 프로젝트에는 BUILD_DLL이 선언되었기 때문에 앞서 Window 클래스 선언 시 이름 앞에 붙여준 ENGINE_API 매크로가 __declspec(dllexport)로 해석됩니다.
Engine 프로젝트 빌드
이제 Engine 프로젝트만 빌드한 뒤에 생성된 결과물을 살펴보겠습니다.
솔루션 탐색 창에서 Engine 프로젝트를 선택하고, Visual Studio 상단 메뉴에서 Build > Build Engine 메뉴를 선택해 Engine 프로젝트를 빌드합니다.
Engine 프로젝트의 빌드 결과물은 dll과 lib 파일이 나와야 합니다.
출력 경로로 이동해 파일이 제대로 생성됐는지 확인해봅니다.
솔루션 경로\Bin\x64\Debug\Engine 폴더로 이동해보면 아래와 같이 Engine.dll과 Engine.lib 파일이 생성된 것을 볼 수 있습니다.
Application 프로젝트의 Main.cpp 코드 업데이트
지금까지 Application 프로젝트에 추가된 Main.cpp에서 창 생성 로직을 처리했습니다.
이제는 창 생성을 Window 클래스를 통해서 할 수 있도록 했기 때문에 Window를 사용해서 생성하도록 코드를 업데이트합니다.
// 창 클래스를 사용할 수 있도록 Window.h 헤더 인클루드. #include "../Engine/Core/Window.h" // 창에서 메시지가 발생하면 호출될 함수. ... // 메인 함수(Entry Point). int WINAPI WinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) { // 창 생성. Window window(hInstance, 1280, 800, TEXT("Toy Render Engine"), WindowProc); // 메시지 루프. MSG msg = { 0 }; while (msg.message != WM_QUIT) { // 창에서 메시지가 발생하는 지 확인. if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { // 메시지 변환. TranslateMessage(&msg); // 메시지 전달. DispatchMessage(&msg); } else { } } }
기존에 창 생성 및 업데이트를 위한 코드는 모두 제거하고, Window 클래스를 통해서 창이 생성될 수 있도록 코드를 변경했습니다.
아직 프로그램을 실행하는 핵심 루프 코드를 다른 곳으로 옮기지는 못했지만, Main 함수의 코드를 많이 줄일 수 있었습니다.
이처럼 Main 함수는 가볍게 유지하는 것이 대체로 좋습니다.
Application 프로젝트 빌드
코드를 업데이트 했으니 빌드를 진행해보겠습니다.
솔루션 탐색 창에서 Application 프로젝트 우클릭 > Build를 선택해 빌드를 진행합니다.
출력(Output) 창에서 오류가 발생하지 않았는지 확인해봅니다.
Application 프로젝트는 Engine 프로젝트를 종속성으로 등록했기 때문에 Engine 프로젝트와 함께 빌드되었습니다.
(Engine 프로젝트의 빌드가 최신인 경우 건너뛰는 경우도 있습니다.)
결과가 2 succeeded로 나오고, 오류도 발견되지 않았습니다.
이제 Ctrl + F5를 사용해 실행을 해보겠습니다.
빌드에는 문제가 없었는데, Engine.dll을 찾을 수 없다는 오류가 발생합니다.
지금 Application은 Engine 프로젝트에 포함된 Window 클래스를 사용하고 있습니다.
Window.h 헤더는 공개되어 사용이 가능합지만, cpp 파일이 빌드된 정보는 Engine.dll에 있습니다.
따라서 dll을 사용하는 경우에는 실행할 때 dll 파일을 찾은 뒤 링크(연결)를 진행합니다.
이 오류는 Engine.dll 파일을 찾지 못해서 발생하는 오류입니다.
DLL 링크 오류 임시 해결하기
앞서 만난 dll 링크 오류를 해결하려면 Application.exe가 배치된 곳에 Engine.dll 파일을 복사해 옮겨두어야 합니다.
코드에서 dll 연결 작업을 직접해주지 않으면 exe와 같은 경로에서 dll 파일을 찾아 링크를 진행하기 때문입니다.
일단 실행이 잘 되는지 확인하기 위해서 Engine.dll 파일을 손수 Application의 실행 파일이 있는 곳으로 복사해줍니다.
솔루션경로\Bin\x64\Debug\Engine 폴더에 가보면 Engine.dll 파일이 있는 것을 볼 수 있습니다.
이 파일을 솔루션경로\Bin\x64\Debug\Application 폴더로 복사해 줍니다.
직접 Engine 출력 경로에서 Ctrl + C로 복사한 뒤에 Application 출력 경로에 Ctrl + V 붙여넣기로 직접 복사했습니다.
이제 Visual Studio에서 Ctrl + F5 또는 F5키를 눌러 실행해봅니다.
잘 실행되는 것을 확인할 수 있습니다.
물론 Application 출력 경로에 있는 Application.exe 파일을 실행해도 잘 실행됩니다.
dll 링크 오류를 해결하는 과정이 찜찜하긴 하지만 일단 여기에서 마무리 하도록 하겠습니다.
dll을 연결을 자동화하는 과정 등은 다음에 진행해보겠습니다.
질문은 댓글로 달아주세요.
긴 글을 읽어주셔서 감사합니다.