메인함수
2024년 3월 17일
주로 사용되는 매크로 #
TRACE_BOOKMARK #
디버깅 실행 인자로 -trace=bookmark 를 추가하면, UnrealInsights에서 BookMark 텍스트가 출력된다.
LaunchWindowsStartup에 가장 먼저 호출되는 TRACE_BOOKMARK(TEXT("WinMain.Enter")); 매크로에 의해 북마크가 표시된 것을 확인할 수 있다.
WinMain #
엔트리 포인트를 확인하는 방법은 StepOver(F10)을 눌러서 디버깅모드로 한줄만 실행하도록 하면 된다.
실행해보면 아래의 WinMain에서 멈추게 될 것이다. WinMain에서 멈췄다는 것은 현재 언리얼 엔진이Windows 환경에서 실행됐다는 의미이다.
리눅스나 macOS 에서는 main() 함수를 사용하게 된다.
LaunchWindowsStartup #
SetupWindowsEnvironment() #
- _set_invalid_parameter_handler(InvalidParameterHandler);
잘못된 파라미터가 전달됐을때 호출할 핸들러를 설정하는 C런타임 라이브러리(CRT)에 내장된 함수이다. 적당히 로그를 출력하는 핸들러를 등록하고 있다. - _CrtSetReportMode( _CRT_ASSERT, _CRTDBG_MODE_DEBUG );
CRT의 리포트 모드를 설정하는함수이다. ASSERT 가 발생할때 디버그 출력으로 오류를 출력한다는 의미이다. - _CrtSetDebugFillThreshold( 0 );
디버그 모드에서는 잘못 사용하는 메모리를 쉽게 감지하기 위해 함수에 전달된 메모리의 사이즈만큼 0xDD, 0xFE 같은 쓰레기 값이 채워지고, 개발자가 예상하지 못하게 사이즈를 넘으면 다른 메모리에 영향을 줘서 에러를 발생시킨다.
0을 인자로 전달하면 비활성화되는데 디버깅 모드의 속도를 빠르게 하기 위해 0으로 설정한다.
GetCommandLineW(); ProcessCommandLine(); #
argv에서 커맨드 라인을 가져오고, 커맨드라인을 파싱한다.
- ProcessCommandLine 이전 :
"D:\project\UnrealEngine-release\Engine\Binaries\Win64\UnrealEditor.exe" LyraStarterGame -tracehost=127.0.0.1 -trace=bookmark,cpu,gpu,frame - 이후 :
LyraStarterGame -tracehost=127.0.0.1 -trace=bookmark,cpu,gpu,frame
FParse::Param(CmdLine,TEXT(“noexceptionhandler”)); #
파라미터(CmdLine) 에 각각의 인자가 포함되어 있는지 확인한다.
FPlatformMisc::IsDebuggerPresent(); #
현재 애플리케이션이 디버거에 의해 모니터링 되고있는지를 확인한다.
이 값들에 따라 GuardedMain을 호출할지, GuardedMainWrapper를 호출할지 결정한다.
SEH (__try ~ __except 패턴) #
1#if UE_BUILD_DEBUG
2 if (GUELibraryOverrideSettings.bIsEmbedded || !GAlwaysReportCrash)
3#else
4 if (GUELibraryOverrideSettings.bIsEmbedded || bNoExceptionHandler || (bIsDebuggerPresent && !GAlwaysReportCrash))
5#endif
6 {
7 ErrorLevel = GuardedMain( CmdLine );
8 }
9 else
10 {
11#if !PLATFORM_SEH_EXCEPTIONS_DISABLED
12 __try
13#endif
14 {
15 GIsGuarded = 1;
16 ErrorLevel = GuardedMainWrapper( CmdLine );
17 GIsGuarded = 0;
18 }
19#if !PLATFORM_SEH_EXCEPTIONS_DISABLED
20 // 예외 발생시 ReportCrash() 를 호출하며 크래시로그를 기록하고 미니덤프를 생성한다.
21 __except( FPlatformMisc::GetCrashHandlingType() == ECrashHandlingType::Default
22 ? (GEnableInnerException
23 ? EXCEPTION_EXECUTE_HANDLER
24 : ReportCrash(GetExceptionInformation()))
25 : EXCEPTION_CONTINUE_SEARCH
26 {
27 // Crashed.
28 }
29#endif
30 }
SEH는 try catch로 잡기힘든 windows 자체에서 발생하는 예외(Access Violation, Divide by Zero, Stack Overflow 등)를 잡기 위한 패턴이다. c++ 응용 프로그램의 예외(개발자가 throw한 예외)는 catch하지 못한다.
RaiseException, _set_se_translator, /EHa 를 사용하면 개발자가 throw한 것도 잡을 수 있지만, 분리해서 사용하는것이 좋다.
임베디드이거나 디버거를 사용하고 있는 경우엔 크래시를 프로그램에 넘기기 위해 SEH 예외처리 없이 GuardedMain( CmdLine ); 을 호출하고,
그게 아니라면 SEH 로 한번 더 감싸서 예외가 발생했을때 크래시 리포팅, 에러처리, 정적셧다운, 메모리 상태 출력 후 종료하게 된다.
GuardedMainWrapper 를 확인해보면 내부에서도 SEH를 사용하는데, XAudio2 등의 라이브러리는 자체적인 예외처리 로직이 있을 수 있는데 내부 SEH에서 크래시가 발생한다면, 외부에서 잡을 수 있는것으로 보인다.
GuardedMain #
FTrackedActivity::GetEngineActivity().Update(TEXT(“Starting”)…); #
프로세스에서 엔진 상태를 업데이트 한다. 진행중인 작업을 시각화 할때 사용된다.
FTaskTagScope Scope(ETaskTag::EGameThread); #
Thread나 Job 실행 컨텍스트에 태그를 지정하는데 사용되고 나중에 호출스택에서 상태를 쿼리할 수 있게 한다.
while (!FPlatformMisc::IsDebuggerPresent()) #
1 if (FParse::Param(CmdLine, TEXT("waitforattach")) || FParse::Param(CmdLine, TEXT("WaitForDebugger")))
2 {
3 while (!FPlatformMisc::IsDebuggerPresent())
4 {
5 FPlatformProcess::Sleep(0.1f);
6 }
7 UE_DEBUG_BREAK();
8 }
커맨드라인 인자에 waitforattach가 있거나 WaitForDebugger가 있으면 들어온다.
디버거가 붙으면 IsDebuggerPresent() 함수가 true를 반환한다. 디버거가 붙을때까지 기다리고, UE_DEBUG_BREAK() 로 인터럽트를 걸어서 디버거가 이 코드에서부터 디버깅할 수 있도록 한다.
FCoreDelegates::GetPreMainInitDelegate().Broadcast(); #
Delegate는 함수나 메서드를 대신해서 호출할 수 있는 함수객체이다.
GuardedMain 에서 게임 코드가 실행되기 전에 미리 호출해야하는 함수들이 등록되어 있고, AddStatic으로 추가할수도 있다. 이후 Broadcast 메서드가 호출되면서 등록된 모든 함수가 순차적으로 실행된다.
Broadcast 메서드가 호출되는 타이밍(GuardedMain)부터 발생하는 UnhandledException은 언리얼엔진에서 관리하겠다는 의미이다.
1// 전역변수이기 때문에 OS가 EXE/DLL 로드할때(CRT initialization) 생성된다.
2static FCrashReportingThread GCrashReportingThread;
3
4FCrashReportingThread::FCrashReportingThread()
5{
6 // UnhandledException 핸들러를 델리게이트에 등록하는 핸들러
7 FCoreDelegates::GetPreMainInitDelegate().AddRaw(this, &FCrashReportingThread::RegisterUnhandledExceptionHandler);
8}
9
10void RegisterUnhandledExceptionHandler()
11{
12 // 나중에 SetUnhandledExceptionFilter 가 다시 호출되며 덮어쓸때까지 유효함
13 ::SetUnhandledExceptionFilter(EngineUnhandledExceptionFilter);
14}
15
16LONG WINAPI EngineUnhandledExceptionFilter(LPEXCEPTION_POINTERS ExceptionInfo)
17{
18 ReportCrash(ExceptionInfo);
19 return EXCEPTION_CONTINUE_SEARCH;
20}
언리얼은 CoreDelegates, CoreUObjectDelegates, WorldDelegates 등 델리게이트 클래스 형태로 원하는 위치에 코드를 주입하는 방법이 자주 쓰인다.
위의 델리게이트들 처럼 특정 시점(엔진 초기화, 월드 초기화 등)에 호출할때 사용할수도 있고, 콜백처럼 사용해서 특정 행동(또는 애니메이션 등)이 끝날때 호출해서 감시하지 않고 연결된 객체의 상태변화를 수행할수도 있다.
- Singlecast Delegate : 함수 한개를 바인드 해두고, 원하는 위치에서 바인드된 함수를 호출한다.
- Multicast Delegate : 함수 여러개를 바인드 해두고, 원하는 위치에서 함수를 호출해준다. 한꺼번에 실행하는 함수 뭉치정도로 생각하면 된다.
- Dynamic Delegate : 런타임에 동적으로 대상을 추가하거나 제거할 수 있는 델리게이트
struct EngineLoopCleanupGuard { … } CleanupGuard; #
1 struct EngineLoopCleanupGuard
2 {
3 ~EngineLoopCleanupGuard()
4 {
5 EngineExit();
6 }
7 } CleanupGuard;
함수 내부에서 구조체와 소멸자를 정의하고 CleanupGuard; 객체를 생성했는데, 이 객체는 함수가 종료될때 자동으로 소멸자가 호출되며 EngineExit() 도 같이 호출된다. (RAII 패턴)
EnginePreInit( CmdLine ); #
(FEngineLoop)GEngineLoop.PreInit(); 을 호출하면서 엔진을 초기화한다. FEngineLoop 싱글톤 객체가 언리얼엔진의 모든걸 컨트롤하게 된다.
- PreInitPreStartupScreen
시작화면 표시 전 초기화 과정이다. 게임이름, 프로젝트 경로 등 프로젝트에 대한 정보들을 추출하고, 엔진 실행모드 (에디터, 데디케이티드서버, 클라이언트 등)를 결정한다. 이후에도 코어모듈을 로드하고, 로깅, 파일시스템, 물리엔진, 렌더링, 쉐이더, UI 등의 초기화를 진행한다.
초기화과정에서 수집된 정보를 PreInitContext에 저장한다. - StartupScreen 표시됨 : 사용자가 심심하지 않도록 시각적인 효과를 먼저 출력해준다.
- PreInitPostStartupScreen
시작화면이 표시된 이후의 초기화 과정이다. 이전 초기화 과정에서 생성된 PreInitContext를 복원해서 초기화에 사용한다. 공유경로 마운트, 오브젝트 초기화, 가비지컬렉션 최적화, 로딩화면 모듈 로드, 인터페이스 초기화 등을 수행한다.
EditorInit(GEngineLoop); #
1if (GIsEditor)
2{
3 ErrorLevel = EditorInit(GEngineLoop);
4}
5else
6{
7 ErrorLevel = EngineInit();
8}
EditorInit 인 경우 EngineInit 보다 초기화할게 많다. 디버그 도구, 엔진을 초기화하고, 에디터 관련 이벤트를 업데이트하고 기록한다. 여러가지 모듈을 로드하고 에디터의 시작시간을 기록한다.
double EngineInitializationTime = FPlatformTime::Seconds() - GStartTime; #
GStartTime은 엔진 시작할때의 Seconds()의 결과이고, 이 코드는 엔진 초기화시간이 얼마나 걸렸는지를 계산하는 코드이다.
EngineTick(); #
1FEngineLoop GEngineLoop;
2
3void EngineTick()
4{
5 GEngineLoop.Tick();
6}
7
8int32 GuardMain(const TCHAR* CmdLine)
9{
10 while( !IsEngineExitRequested() )
11 {
12 EngineTick();
13 }
14}
엔진의 메인 루프이며, 내부에서 GEngineLoop.Tick(); 을 호출해서 프레임당 게임의 작업을 실행하게 되는 구조이다. 게임 엔진이 종료될때까지 나갈일이 없다.
GEngineLoop 은 전역변수이며, 이전에 EnginePreInit() 함수에서 초기화했다.
EditorExit(); #
에디터는 종료할때도 리소스를 정리해야한다.