UE5 소스 분석 - 엔진 진입점 (WinMain → GuardedMain)

Engine/Source/Runtime/Private/Windows/LaunchWindows.cpp

1. 진입점: WinMain

  • 언리얼도 익숙한 WinMain 함수를 통해 프로그램이 시작된다.
  • 생각해보면 당연한 건데, 뭔가 특별한 방식일 것이라는 막연한 기대가 있었음. 결국 언리얼도 Windows 애플리케이션이다.
  • WinMain에서 바로 LaunchWindowsStartup 함수로 진입.

2. LaunchWindowsStartup

  • 윈도우 실행 환경 설정 + 커맨드라인 해석 담당.
  • 여러 전처리기(#ifdef)와 예외 처리가 배치되어 있음 → 실행 모드(에디터/게임/커맨드렛 등)를 미리 분기하는 구조.
  • 여기서 핵심은 결국 GuardedMain 호출.

3. GuardedMain

Engine/Source/Runtime/Private/Launch.cpp

int32 GuardedMain( const TCHAR* CmdLine )
{
	// 게임 스레드 시작 및 태깅
	// bWaitForDebugger = 디버거 있는지 확인
	if (bWaitForDebugger)
	{
		while (!FPlatformMisc::IsDebuggerPresent())
		{
			FPlatformProcess::Sleep(0.1f); // 디버거 붙을 때까지 이 스레드 대기
		}
 
		if (bWaitForDebuggerAndBreak)
		{
			UE_DEBUG_BREAK();
		}
	}
 
	// 메인 초기화 이전에 실행되는 델리게이트 브로드캐스트
	// C# 델리게이트 같은 역할. 순서가 중요함
	FCoreDelegates::GetPreMainInitDelegate().Broadcast();
 
	// 스코프 객체를 이용한 종료 보장
	struct EngineLoopCleanupGuard 
	{ 
		~EngineLoopCleanupGuard()
		{
			if (!GUELibraryOverrideSettings.bIsEmbedded)
			{
				EngineExit();
			}
		}
	} CleanupGuard; // 선언과 동시에 객체 생성
	// 메인 함수가 어떤 경로로 끝나든 소멸자에서 EngineExit() 보장 
 
	// FEngineLoop 객체에게 초기화 전달
	int32 ErrorLevel = EnginePreInit( CmdLine );
	
	// 로딩 진행률 객체
	{
		// 전체 로딩량을 100이라고 했을 때
		FScopedSlowTask SlowTask(100, NSLOCTEXT("EngineInit", "EngineInit_Loading", "Loading..."));
		// EnginePreInit이 전체의 80%에 해당하는 작업
		SlowTask.EnterProgressFrame(80);
		// 나머지 20%는 EngineInit / EditorInit 몫
		SlowTask.EnterProgressFrame(20);
		```
		//엔진 프리 이닛에서 20%의 진행율을 남겨둔채로 끝나니까 이어서 진행하려고.
		
#if WITH_EDITOR
		if (GIsEditor)
		{
			ErrorLevel = EditorInit(GEngineLoop);
		}
		else
#endif
		{
			ErrorLevel = EngineInit();
		}
	}
	
	// 엔진 시작 시간 측정
 
	// 임베디드 모드라면 내부 틱을 사용하지 않음
	if (!GUELibraryOverrideSettings.bIsEmbedded)
	{
		while( !IsEngineExitRequested() )
		{
			EngineTick(); // 메인 엔진 루프
		}
	}
 
	if( GIsEditor )
	{
		EditorExit();
	}
	return ErrorLevel;
}

흐름 요약

WinMain → LaunchWindowsStartup (환경 설정, 커맨드라인 해석) → GuardedMain

GuardedMain 내부 순서:

  1. 디버거 대기 처리 (bWaitForDebugger)
  2. PreMainInitDelegate 브로드캐스트 (메인 초기화 전 훅)
  3. EngineLoopCleanupGuard 생성 → 소멸자에서 EngineExit() 보장 (RAII)
  4. EnginePreInit(CmdLine) — 전체 초기화의 80%
  5. EditorInit 또는 EngineInit — 나머지 20% (WITH_EDITOR 분기)
  6. while (!IsEngineExitRequested()) { EngineTick(); } — 메인 루프
  7. 에디터라면 EditorExit(), 스코프 종료 시 CleanupGuard가 EngineExit()

추가 느낀 점

  • 언리얼은 전처리기 #ifdef와 스코프 객체(RAII)를 활용해 측정/태깅 지점을 코드 곳곳에 심어두었다.
  • EngineLoopCleanupGuard, FScopedSlowTask 모두 같은 패턴 — 생성자/소멸자를 진입/이탈 훅으로 쓰는 방식.
  • 이전 프로젝트에서 ms 측정하려고 스코프 안에 타이머 객체 넣어두었던 것과 같은 원리. 상용 엔진도 결국 같은 C++ 관용구 위에서 돌아간다.