WIN-B01 | WinAPI-Isaac — Win32 Entry에서 Game Loop까지
이 글에서 확인할 내용
WinAPI-Isaac을 만들면서 처음으로
게임이 계속 반복되는 Loop 안에서 움직인다는 구조를 직접 구현했습니다.
처음에는 wWinMain, PeekMessage, Update, Render 같은 이름이 낯설었습니다.
그때는 창이 뜨고 캐릭터가 움직이면 된다고 생각했습니다.
하지만 직접 코드를 따라가면서 Windows 프로그램이 어디에서 시작되고,
OS Message를 처리하는 흐름과 게임의 Frame Update가
어떻게 하나의 Loop 안에서 함께 돌아가는지 조금씩 이해하게 됐습니다.
이 글에서는 그 시작점을 아래 흐름으로 정리합니다.
Text
wWinMain
↓
Window 생성
↓
CMainGame Initialize
↓
Message Loop
↓
Update
↓
Late_Update
↓
Render
↓
다음 Frame프로그램은 wWinMain에서 시작했습니다
WinAPI-Isaac의 시작점은 wWinMain입니다.
여기서 Window Class를 등록하고 InitInstance를 통해 실제 Window를 만든 뒤,
게임에서 사용할 Window Handle을 준비했습니다.
그 다음 CMainGame 객체를 만들고 Initialize()를 호출하면서
Win32 쪽의 프로그램 시작 흐름에서 실제 Game Runtime으로 넘어갔습니다.
C++
CMainGame MainGame;
MainGame.Initialize();처음에는 이 코드를 단순히
“프로그램을 실행하기 위해 필요한 시작 코드” 정도로 봤습니다.
하지만 지금 다시 보면 이 지점은
Win32 Application 영역과 Game Runtime 영역이 만나는 경계에 가까웠습니다.
Window 생성과 OS Message 처리는 Win32 쪽에서 준비하고,
그 위에서 Scene과 Object가 움직이는 게임 흐름은 CMainGame 이후부터 이어졌습니다.
Window Handle도 이후 Runtime의 기준이 됐습니다
Window가 만들어지면 g_hWnd 같은 Handle을 기준으로
화면 출력이나 좌표 처리에 필요한 WinAPI 기능들이 동작했습니다.
당시에는 HWND, HDC 같은 타입을 하나씩 외우는 데 더 집중했지만,
지금 다시 보면 중요한 점은 이름 자체보다 게임이 어느 Window를 기준으로 실행되고 그려지는가였습니다.
CMainGame::Initialize()에서는 Window에서 DC를 얻고,
Sound와 Bitmap Resource를 준비한 뒤 첫 Scene을 Logo로 전환했습니다.
흐름을 단순하게 보면 다음과 같습니다.
Text
Window 준비
↓
CMainGame::Initialize
↓
DC 준비
↓
Sound / Bitmap Resource 준비
↓
Logo Scene 진입즉 프로그램이 실행됐다고 바로 Gameplay가 시작되는 것이 아니라,
Game Runtime이 사용할 Resource와 첫 Scene을 준비한 뒤 Frame Loop로 들어갔습니다.
Message가 없을 때 게임이 움직였습니다
WinAPI 프로그램은 기본적으로 Message를 처리하면서 동작합니다.
키보드나 마우스 입력, Window 종료 같은 OS Event가 들어오면
PeekMessage로 Message를 확인하고,
TranslateMessage, DispatchMessage를 거쳐 WndProc 쪽으로 전달합니다.
그런데 게임은 Message가 들어왔을 때만 움직이면 안 됩니다.
아무 키도 누르지 않아도 Monster는 움직여야 하고,
Projectile은 계속 이동해야 하며,
현재 Scene과 Object도 다음 Frame으로 계속 갱신되어야 합니다.
그래서 이 프로젝트에서는 Message가 없을 때
게임 쪽 Update가 실행되는 구조였습니다.
C++
if (PeekMessage(...))
{
// Win32 Message 처리
}
else
{
MainGame.Update();
MainGame.Late_Update();
MainGame.Render();
}흐름만 보면 다음과 같습니다.
Text
Message가 있음
→ OS Message 처리
Message가 없음
→ Game Update
→ Late_Update
→ Render이 구조를 직접 보면서 처음으로
OS Event 처리와 Game Loop가 같은 While Loop 안에서 나뉘어 있다는 것을 이해했습니다.
왜 Message가 없어도 Update해야 하는지 직접 체감했습니다
처음에는 Message가 없는데 왜 계속 함수를 호출해야 하는지 감이 잘 오지 않았습니다.
하지만 게임 안의 Object를 움직여보니 이유가 바로 보였습니다.
Player가 아무 키도 누르지 않는 순간에도:
- Monster는 Player를 향해 이동할 수 있고
- 이미 발사된 Bullet은 계속 날아가야 하고
- Scene 안의 Object 상태는 계속 갱신되어야 하고
- 화면 역시 다음 Frame의 결과를 계속 보여줘야 합니다.
즉 게임은 “입력 Event가 들어왔을 때만 반응하는 프로그램”이 아니라
현재 상태를 계속 갱신하는 Runtime이었습니다.
WinAPI-Isaac에서 Game Loop를 직접 구현한 경험이 의미 있었던 이유도 여기에 있습니다.
한 Frame은 Update에서 Render까지 이어졌습니다
CMainGame은 매 Frame 아래 흐름을 반복했습니다.
Text
Update
↓
Late_Update
↓
Render그리고 실제 처리는 현재 Scene으로 다시 전달됐습니다.
Text
CMainGame::Update
→ CSceneMgr::Update
CMainGame::Late_Update
→ CSceneMgr::Late_Update
CMainGame::Render
→ Background Render
→ CSceneMgr::Render처음에는 세 함수가 왜 나뉘어 있는지 깊게 생각하지 않았습니다.
하지만 프로젝트를 만들면서 역할 차이가 조금씩 보였습니다.
Update에서는 Object의 위치나 상태가 바뀌고,
Late_Update에서는 Collision이나 Scene 전환처럼
Update 이후에 확인해야 하는 흐름이 이어졌습니다.
마지막으로 Render에서 그 Frame의 결과가 화면에 그려졌습니다.
예를 들어 Player가 한 번 움직이는 것도 실제로는:
Text
Key Input
↓
Player 위치 변경
↓
Rect 갱신
↓
Collision 확인
↓
Render 위치 변경처럼 여러 단계가 이어진 결과였습니다.
화면에서 보이는 이동 한 번이
한 Frame 안의 여러 처리 결과라는 것을 이때 처음 체감했습니다.
10ms 간격으로 Update를 제한했습니다
당시 코드에서는 GetTickCount()를 이용해
일정 시간이 지난 뒤에만 Update, Late_Update, Render를 호출했습니다.
대략적인 형태는 다음과 같았습니다.
C++
if (dwTime + 10 < GetTickCount())
{
MainGame.Update();
MainGame.Late_Update();
MainGame.Render();
dwTime = GetTickCount();
}지금 기준으로 보면 정교한 Fixed Timestep 구조는 아닙니다.
Delta Time을 각 Gameplay 계산에 전달하거나,
Update와 Render를 서로 다른 주기로 분리한 구조도 아니었습니다.
당시에도 복잡한 Frame 제어를 설계한 것은 아닙니다.
다만 이 코드를 직접 사용하면서
게임이 무조건 가능한 한 빠르게만 도는 것이 아니라,
시간의 흐름을 기준으로 Frame Update를 반복한다는 감각을 처음 얻었습니다.
중요했던 것은 10ms라는 숫자 자체가 아니라
게임이 일정한 순서를 계속 반복한다는 점이었습니다.
CMainGame은 전체 Runtime의 입구 역할을 했습니다
CMainGame은 Player나 Monster를 하나씩 직접 관리하는 Class라기보다,
Win32 Entry 이후 실제 Game Runtime을 시작하고 현재 Scene으로 흐름을 넘겨주는 역할에 가까웠습니다.
초기화 단계에서는 Window DC와 Resource, Sound, 첫 Scene을 준비하고,
Frame Loop가 시작된 뒤에는 현재 Scene의 Update / Late_Update / Render를 호출했습니다.
Text
Win32
↓
CMainGame
↓
CSceneMgr
↓
현재 Scene
↓
Object처음 만들 때는 이것을 단순한 함수 호출 계층으로 봤습니다.
하지만 지금 다시 보면
CMainGame이 Win32 쪽 실행 흐름과 Scene 기반 Game Runtime 사이를 연결해주고 있었습니다.
하나의 Loop 안에서 Scene만 바뀌었습니다
Game Loop는 계속 같은데
실제로 Update되는 내용은 현재 Scene에 따라 달라졌습니다.
프로그램 시작 이후에는 대략 다음 순서로 진행됩니다.
Text
Program Start
↓
MainGame Initialize
↓
Logo
↓
Menu
↓
StageGame Loop를 새로 만드는 것이 아니라
CSceneMgr가 현재 Scene을 바꾸고,
CMainGame은 계속 같은 Update / Render 흐름을 호출합니다.
처음에는 Logo에서 Menu로 바뀌는 것을
단순히 화면이 넘어가는 기능이라고 생각했습니다.
하지만 지금 보면 중요한 점은
Loop는 그대로 유지되고, 그 안에서 실행되는 Runtime Context가 바뀐다는 것이었습니다.
Stage에 진입하면 Player와 Monster 같은 Object가 등장하고,
그 Object들이 같은 Game Loop 안에서 다시 Update 대상이 됩니다.
이 경험 이후 다른 프로젝트에서도
“Game Loop가 어떻게 생겼는가?”와 함께
“현재 어떤 Context와 Object가 그 Loop에 참여하고 있는가?”를 같이 보게 됐습니다.
Runtime
실제 영상에서는 Logo와 Menu를 거쳐 Stage에 들어간 뒤
Player, Monster, Projectile이 계속 움직이는 모습을 확인할 수 있습니다.
화면에서는 단순히 게임이 실행되는 것처럼 보이지만,
그 뒤에서는 같은 Game Loop가 반복되면서
현재 Scene과 Object의 상태가 계속 바뀌고 있습니다.
Player가 움직이는 장면은 현재 Stage가 Update되고 있다는 뜻이고,
Monster가 움직이거나 이미 발사된 Projectile이 계속 진행되는 장면은
입력 Message가 없는 순간에도 Game Runtime이 계속 갱신되고 있다는 결과입니다.
지금 다시 보면
지금 다시 만든다면 Frame Time과 Update 주기를
조금 더 명확하게 나누고 싶습니다.
당시에는 GetTickCount() 기반으로 일정 시간이 지나면
Update와 Render를 함께 호출하는 방식이었습니다.
지금은 Delta Time이나 Fixed Timestep 같은 개념을 구분해서 볼 수 있지만,
그때는 우선 게임이 한 Frame씩 계속 진행되는 구조를 직접 만드는 것이 먼저였습니다.
또 CMainGame이 Resource, Sound, Scene 시작점까지 여러 역할을 함께 가지고 있어
규모가 커진다면 책임을 더 나눌 수 있는 부분도 보입니다.
다만 당시 구조를 현재 기준으로 과장해서 해석하지는 않습니다.
이 프로젝트에서 제가 직접 경험한 것은
정교한 Engine Loop를 설계했다는 것이 아니라:
Text
프로그램은 어디에서 시작하는가?
Message는 어디에서 처리하는가?
게임은 언제 계속 Update되는가?
한 Frame에서는 어떤 순서로 상태가 바뀌는가?
그 결과는 언제 화면에 그려지는가?를 처음 코드로 확인한 경험입니다.
다음 프로젝트에서 이어진 부분
이후 DX9과 DX11 프로젝트에서는 같은 Frame 개념 위에서
3D Object와 Animation, Rendering Pipeline, Tool Update 같은 더 큰 흐름을 보게 됐습니다.
Unreal에서는 개발자가 직접 wWinMain과 Message Loop를 작성하지 않지만,
Engine이 감싸고 있는 Tick과 Gameplay Framework 안에서
“현재 Frame에 무엇이 갱신되는가”를 다시 보게 됐습니다.
구현 방식은 완전히 달라졌지만
제가 Runtime을 볼 때 가장 먼저 확인하는 흐름은 여전히 비슷합니다.
Text
어디에서 시작하는가?
↓
무엇이 반복되는가?
↓
한 Frame에서 무엇이 갱신되는가?
↓
현재 Context는 무엇인가?
↓
그 결과가 언제 화면에 그려지는가?WinAPI-Isaac은 이 질문을 처음 코드로 경험한 프로젝트였습니다.
정리
WinAPI-Isaac에서 Game Loop를 구현하며 가장 크게 배운 것은
게임이 한 번 실행되고 끝나는 프로그램이 아니라는 점이었습니다.
wWinMain에서 Window와 Game Runtime을 준비하고,
PeekMessage로 OS Message를 처리하면서,
Message가 없는 시간에는 Update → Late_Update → Render를 반복했습니다.
Text
Win32 Entry
↓
Message Loop
↓
CMainGame
↓
Update
↓
Late_Update
↓
Render
↓
다음 Frame처음에는 창이 뜨고 캐릭터가 움직이는 결과만 봤지만,
직접 Loop를 만들고 그 안에서 Scene과 Object가 계속 갱신되는 것을 확인하면서
게임이 한 Frame씩 진행되는 기본 Runtime 흐름을 처음 몸으로 익혔습니다.
WIN-B01