Main Game Loop

메인 루프 로직의 코드 흐름

코드 흐름

  • Main Game Loop (FEngineLoop::Tick)
    • Update time and Handle maximum tick rate (appUpdateTimeAndHandleMaxTickRate)
    • Enqueue rendering thread task
    • Tick Engine (UGameEngine::Tick)
    • Update RHI (FD3D9DynamicRHI::Tick)
    • Sync game and render thread
    • Handle window manages (appWinPumpMessages)
    • Excute deferred commands

Update time and Handle maximum tick rate

call stack : FEngineLoop::Tick → appUpdateTimeAndHandleMaxTickRate

GCurrentTime, GDeltaTime 등 이번 Frame 에서 사용될 tick 관련 변수들을 채우고, network 가 고려되어 설정된 max tick rate 보다도 (이번프레임이) 빨리 진행되었다면 남은 시간동안 Idle 상태로 있는다.

// tick 관련 변수를 구하고
GCurrentTime = appSeconds();
GDeltaTime = GCurrentTime - LastTime;
LastTime = GCurrentTime;
 
// 너무 빨리 진행되었다면 잠깐 sleep
MaxTickRate = GEngine->GetMaxTickRate( DeltaTime );
if ( MaxTickRate > 0 )
{
    WaitTime = Max( 1.f / MaxTickRate - DeltaTime, 0.f );
}
if ( WaitTime > 0 )
{
    appSleep( WaitTime );
}

Enqueue rendering thread task

Rendering Thread 에게 per-frame 일거리를 던져준다.

ENQUEUE_UNIQUE_RENDER_COMMAND(
    ResetDeferredUpdatesAndTickTickables,
    {
        FDeferredUpdateResource::ResetNeedsUpdate();
        TickRenderingTickables();                    // 랜더링 큐가 가득차있더라도 Tickables 에게 tick 할 기회를 준다.
    }
);

위 매크로에 대해서는 여기서 보면 된다.

나중에 RenderingThread1) 를 보면 알겠지만 랜더링 큐에 있는 커맨드를 우선적으로 처리하고 Idle 시간에 TickRenderingTickables() 를 호출하도록 되어있다. 만약 랜더링 큐가 꽤 오랫동안 비어있지 않게되면 RenderingThread 에서 TickRenderingTickables() 는 호출되지 않게 되는데 이것을 방지하기 위해 GameThread 에서 위 코드가 매 frame 마다 수행된다.

Tick Engine

call stack : FEngineLoop::Tick → UGameEngine::Tick

World, actor, network driver, caching system 등 모든것들의 tick 을 처리한다. 아래서 자세히 살펴보겠다.

Update RHI

call stack : FEngineLoop::Tick → FD3D9DynamicRHI::Tick (Direct3D 9 의 경우)

Direct3D 의 Device 의 Lost 여부를 모니터링하고 이를 복구한다.

Sync game and render thread

Game Thread 와 Render Thread 간 동기화를 수행한다.

static FFrameEndSync FrameEndSync;
FrameEndSync.Sync( GSystemSettings.bAllowOneFrameThreadLag );

위 로직은 Game Thread 에 의해 수행되는 만큼, Game Thread 가 Render Thread 의 수행을 기다려주게 된다.

이 때, Game Thread 는 Render Thread 의 1 frame lag 를 허락할 것인지를 결정할 수 있는데, 위 코드에 Sync 함수의 파라메터가 바로 그것이다.

  • 1 frame lag 를 허락하지 않으면 Game Thread 는 Render Thread 와의 동기화를 엄격하게 맞춘다. Render Thread 가 1 frame 모두 지날 동안 Idle 상태로 유지된다.
  • 1 frame log 를 허락하면 Game Thread 는 Render Thread 의 (현재 frame 이 아닌) 이전 frame 수행 여부를 체크한다.2) Game Thread 와 Render Thread 간 1 frame 의 격차가 있긴 하지만 Game Thread 가 Render Thread 의 수행을 기다리느라 Idle 상태에 빠지는 시간을 많이 줄일 수 있다. 즉 병렬 처리에 더 도움이 된다.

Handle window manages

call stack : FEngineLoop::Tick → appWinPumpMessages

윈도우 메세지를 처리한다.

while ( PeekMessage( &Msg, NULL, 0, 0, PM_REMOVE ) )
{
    TranslateMessage( &Msg );
    DispatchMessage( &Msg );
}

Excute deferred commands

지연된 콘솔 커맨드를 처리한다.

for ( INT DeferredCommandIndex = 0; DeferredCommandIndex < GEngine->DeferredCommands.Num(); DeferredCommandsIndex++ )
{
    if ( GEngine->GamePlayers.Num() && GEngine->GamePlayers( 0 ) )
    {
        ULocalPlayer* Player = GEngine->GamePlayers( 0 );
        Player->Exec( *GEngine->DeferredCommands( DeferredCommandsIndex ), *GLog );
    }
}
 
GEngine->DeferredCommands.Empty();

참조

1) RenderingThread.cpp 에 void RenderingThreadMain() 참조
2) 2개의 Event 를 생성한 후, round robin 방식을 이용하여 previous frame 의 수행 여부를 체크한다.