게임 엔진 개발에서 핵심적인 부분인 '윈도우 시스템'에 대해 알아보려고 합니다. 특히 멀티 플랫폼과 다중 창을 지원하는 윈도우 시스템의 설계와 구현 방법을 간단히 이해해보려 합니다.
윈도우 시스템은 게임이나 그래픽 애플리케이션이 화면에 표시되는 창을 관리하는 시스템입니다. 단순히 창을 띄우는 것 이상으로, 다음과 같은 중요한 역할을 담당합니다:
많은 게임 엔진이 GLFW 라이브러리를 사용하지만, 다중 창과 멀티 플랫폼 지원에는 몇 가지 한계가 있습니다. 이런 한계를 극복하기 위해 SDL2(Simple DirectMedia Layer 2)를 사용한 접근법을 살펴보겠습니다.
GLFW의 한계:
SDL2의 장점:
우리의 윈도우 시스템은 두 개의 핵심 클래스로 구성됩니다:
각 창의 생명주기와 상태를 관리합니다:
class Window {
public:
Window(const WindowCreateInfo& createInfo);
~Window();
// 창 상태 관리
void setTitle(const std::string& title);
void setSize(int width, int height);
void setFullscreen(bool fullscreen);
// Vulkan 통합
VkSurfaceKHR createSurface(VkInstance instance);
// 이벤트 처리
void setEventCallback(EventCallback callback);
bool processEvent(const SDL_Event& event);
// ... 기타 멤버 함수 및 변수 ...
};
Window 클래스는 다음과 같은 책임을 갖습니다:
모든 창을 관리하고 시스템 수준의 작업을 처리합니다:
class WindowSystem {
public:
WindowSystem();
~WindowSystem();
// 시스템 초기화/정리
void initialize();
void shutdown();
// 창 관리
Window::WindowId createWindow(const WindowCreateInfo& createInfo);
void destroyWindow(Window::WindowId windowId);
Window* getWindow(Window::WindowId windowId);
// 이벤트 처리
void pollEvents();
bool shouldClose() const;
// 플랫폼 정보
static std::string getPlatformName();
static int getDisplayCount();
// ... 기타 멤버 함수 및 변수 ...
};
WindowSystem 클래스는 다음을 담당합니다:
창을 생성할 때는 다양한 옵션을 설정할 수 있습니다:
// 창 설정 구조체
struct WindowCreateInfo {
std::string title = "Vulkan Window";
int width = 800;
int height = 600;
bool resizable = false;
bool fullscreen = false;
bool borderless = false;
bool vsync = true;
int displayIndex = 0; // 다중 모니터 지원
};
// 창 생성 예시
WindowCreateInfo info;
info.title = "게임 메인 창";
info.width = 1024;
info.height = 768;
info.resizable = true;
Window::WindowId windowId = windowSystem.createWindow(info);
이 과정에서 SDL2는 내부적으로 다음 작업을 수행합니다:
1. 설정된 플래그(resizable, borderless 등)에 맞는 SDL 창 생성
2. 창 ID 할당 (다중 창 식별용)
3. 초기 창 속성 설정
이벤트 처리는 세 단계로 이루어집니다:
중앙 이벤트 폴링: WindowSystem이 모든 SDL 이벤트를 수집
void WindowSystem::pollEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
// 이벤트 처리
}
}
이벤트 분배: 각 이벤트를 관련된 창으로 전달
// 창 이벤트를 해당 창에 전달
if (event.type == SDL_WINDOWEVENT) {
for (auto& pair : m_windows) {
if (pair.second->processEvent(event)) {
break; // 이벤트가 처리됨
}
}
}
창별 이벤트 처리: 각 창은 자신에게 해당하는 이벤트 처리
bool Window::processEvent(const SDL_Event& event) {
// 이 창에 관련된 이벤트인지 확인
if (event.type == SDL_WINDOWEVENT &&
event.window.windowID == SDL_GetWindowID(m_window)) {
// 이벤트 처리 및 콜백 호출
if (m_eventCallback) {
m_eventCallback(event);
}
return true;
}
return false;
}
각 창은 Vulkan 렌더링을 위한 표면(Surface)을 생성할 수 있습니다:
VkSurfaceKHR Window::createSurface(VkInstance instance) {
if (m_surface != VK_NULL_HANDLE) {
return m_surface; // 이미 생성됨
}
// SDL을 사용한 Vulkan 표면 생성
if (SDL_Vulkan_CreateSurface(m_window, instance, &m_surface) != SDL_TRUE) {
throw std::runtime_error("Failed to create Vulkan surface");
}
return m_surface;
}
이 표면은 후속 Vulkan 초기화 단계에서 사용됩니다:
1. Vulkan 물리장치 선택
2. 스왑체인 생성
3. 렌더 패스와 프레임버퍼 설정
코드는 컴파일 타임에 플랫폼을 감지하고 적절한 설정을 적용합니다:
std::string WindowSystem::getPlatformName() {
#if defined(_WIN32) || defined(_WIN64)
return "Windows";
#elif defined(__APPLE__)
return "macOS";
#elif defined(__linux__)
return "Linux";
#elif defined(__ANDROID__)
return "Android";
#elif defined(__EMSCRIPTEN__)
return "Web";
#else
return "Unknown";
#endif
}
각 플랫폼별 특성도 자동으로 처리됩니다:
전체 시스템을 사용하는 기본 코드를 살펴보겠습니다:
int main() {
// 윈도우 시스템 초기화
WindowSystem windowSystem;
windowSystem.initialize();
// 주 창 생성
WindowCreateInfo mainWindowInfo;
mainWindowInfo.title = "메인 창";
mainWindowInfo.width = 1024;
mainWindowInfo.height = 768;
Window::WindowId mainWindowId = windowSystem.createWindow(mainWindowInfo);
// 보조 창 생성
WindowCreateInfo secondaryWindowInfo;
secondaryWindowInfo.title = "보조 창";
secondaryWindowInfo.width = 800;
secondaryWindowInfo.height = 600;
Window::WindowId secondaryWindowId = windowSystem.createWindow(secondaryWindowInfo);
// Vulkan 초기화 (실제 코드에서 구현)
VkInstance vulkanInstance = createVulkanInstance();
// 각 창에 대한 Vulkan 표면 생성
Window* mainWindow = windowSystem.getWindow(mainWindowId);
VkSurfaceKHR mainSurface = mainWindow->createSurface(vulkanInstance);
// 메인 루프
while (!windowSystem.shouldClose()) {
// 이벤트 처리
windowSystem.pollEvents();
// 렌더링 코드
// ...
}
// 정리
windowSystem.shutdown();
return 0;
}
이 설계는 다음과 같은 기능으로 쉽게 확장할 수 있습니다:
입력 시스템 통합
하이 DPI 지원
전체화면 관리
다중 그래픽 API 지원
멀티 플랫폼, 다중 창을 지원하는 윈도우 시스템은 현대 게임 엔진의 핵심 요소입니다. SDL2를 활용한 설계는 다음과 같은 장점을 제공합니다:
이러한 윈도우 시스템을 구축하면 게임 엔진의 다른 부분(렌더링, 물리, 오디오 등)을 더 쉽게 통합할 수 있으며, 개발자와 사용자 모두에게 더 나은 경험을 제공할 수 있습니다.
다음 게임 엔진 시리즈에서는 Vulkan Instance의 개념과 구현 방법에 대해 알아보겠습니다. 제안이 있으시면 언제든지 댓글로 남겨주세요!