Engine Tick

게임 엔진 틱 코드 흐름

코드 흐름

  • Engine Tick (UGameEngine::Tick)
    • Malloc Tick (FMalloc::Tick)
    • FPSChart Tick (UEngine::TickFPSChart)
    • MemoryChart Tick (UEngine::TickMemoryChart)
    • AnimationUsage Tick (UAnimSet::TickAnimationUsage)
    • Client Tick (UWindowsClient::Tick)
    • Clean up game viewports (UEngine::CleanupGameViewport)
    • Viewport Closed Exit
    • Speed compensation (UGameViewportClient::SetDropDetail)
    • Static Tick (UObject::StaticTick)
    • Handle seamless traveling (FSeamlessTravelHandler::Tick)
    • Tick the world (UWorld::Tick)
    • Update the viewport (UGameViewportClient::Tick)
    • Handle server traveling
    • Handle client traveling
    • Update Pending Level
    • Loading the pending map
    • Update memory stats
    • Update the transition screen
    • Render everything (UGameEngine::RedrawViewports)
    • Block on async loading if requested
    • Update Audio (UXAudio2Device::Update)
    • Update resource streaming (FStreamingManagerCollection::UpdateResourceStreaming)
    • ScriptCallGraph Tick (FScriptCallGraph::Tick)
    • Update constraints (UEngine::UpdateConstraintActors)
    • Commit map changes (UGameEngine::ConditionalCommitMapChange)

Malloc Tick

call stack : UGameEngine::Tick → FMalloc::Tick

UE3 에 사용되는 메모리 할당자에 대한 틱을 처리한다. 헌데 실제 처리하는 내용은 아무것도 없다. 일단은 빈 껍데기

FPSChart Tick

call stack : UGameEngine::Tick → UEngine::TickFPSChart

FPS (Frame Per Seond) 및 Hitch1) 정보에 대하여 구간별로 기록해둔다.

ChartCreation.h
// FPS
struct FFPSChartEntry
{
    INT     Count;             // 이 구간의 hit count
    DOUBLE  CummulativeTime;   // 이 구간의 DeltaSeconds 들의 합. (seconds)
};
extern FFPSCharEntry     GFPSChart[ 13 ];       // 0-5 FPS, 5-10, 10-15, ... ,60 이상 으로 분류된 13개의 배열. 현재 frame 의 FPS 정보는 해당 element 에 기록되어진다.
 
UINT  GNumFramesBound_GameThread = 0;           // GameThread 에 의해 30 FPS 미만이 된 횟수
UINT  GNumFramesBound_RenderThread = 0;         // RenderThread 에 의해 30 FPS 미만이 된 횟수
UINT  GNumFramesBound_GPU = 0;                  // GPU 에 의해 30 FPS 미만이 된 횟수
 
DOUBLE  GTotalFrameBoundTime_GameThread = 0;    // GameThread 에 의해 30 FPS 미만이 되었을 때 경과된 DeltaSeconds 들의 합
DOUBLE  GTotalFrameBoundTime_RenderThread = 0;  // RenderThread 에 의해 30 FPS 미만이 되었을 때 경과된 DeltaSeconds 들의 합
DOUBLE  GTotalFrameBoundTime_GPU = 0;           // GPU 에 의해 30 FPS 미만이 되었을 때 경과된 DeltaSeconds 들의 합
 
 
// Hitch
struct FHitchChartEntry
{
    INT     HitchCount;                    // 이 구간의 총 hit count
    INT     GameThreadBoundHitchCount;     // GameThread 에 의해 hitch 된 count
    INT     RenderThreadBoundHitchCount;   // RenderThread 에 의해 hitch 된 count
    INT     GPUBoundHitchCount;            // GPU 에 의해 hitch 된 count
};
extern FHitchChartEntry   GHitchChart[ 11 ];    // 5000ms 이상, 2500-5000, 2000-2500, ... , 150-200, 100-150 으로 분류된 11개의 배열, 현재 frame 의 DeltaSeconds 에 해당하는 구간에 기록되어진다.
  • 파라메터로 들어가는 DeltaSeconds 를 내부적으로 다시 계산한다.2)
  • GameThread, RenderThread, GPU 에서 소요된 CPU Cycle 중 가장 많은 것을 worst case 로 선정한다.
  • Hitch 의 경우, 10 FPS 가 안되고 이전 frame 에 비해 DeltaSeconds 가 1.75 배 늘어난 경우 Hitch 라 가정한다.

MemoryChart Tick

call stack : UGameEngine::Tick → UEngine::TickMemoryChart

메모리 사용 상태를 기록한다. 커맨드라인에 특정 옵션3)을 넣어줘야 작동된다.

작동할 경우 주기적으로 아래의 코드를 수행한다.

FMemoryChartEntry NewMemoryEntry;
NewMemoryEntry.UpdateMemoryChartStats();
GMemoryChart.AddItem( NewMemoryEntry );

AnimationUsage Tick

call stack : UGameEngine::Tick → UAnimSet::TickAnimationUsage

10분마다 에니메이션들의 사용상태를 조회하여 CSV 파일포멧으로 기록한다. 실행 파라메터에 -TRACEANIMUSAGE 를 넣어줘야 작동4)한다.

void UAnimSet::TickAnimationUsage()
{
    if ( GWorld )
    {
        if ( GWorld->GetTimeSeconds() - GLastOutputTime > 600 ) // 10분마다 수행
        {
            OutoutAnimationUsage();
            GLastOutputTime = GWorld->GetTimeSeconds();
        }
    }
}
 
void UAnimSet::OutputAnimationUsage()
{
    if ( GShouldTraceAnimatinUsage && GAnimsetUsageMap.Num() > 0 )
    {
        ...
    }
}

생성된 CSV 포멧의 파일은 <ProjectName>\Logs\AnimationUsage\ 하위에 저장된다.

Client Tick

call stack : UGameEngine::Tick → UWindowsClient::Tick

Client 코드를 수행한다. MouseLock 및 Input 처리와 같은 플랫폼에 의존적인 업데이트를 수행한다.

INT LocalClientCycles = 0;
if ( Client )
{
    CLOCK_CYCLES( LocalClientCycles );
    Client->Tick( DeltaSeconds );
    UNCLOCK_CYCLES( LocalClientCycles );
}
ClientCycles = LocalClientCycles;

Clean up game viewports

call stack : UGameEngine::Tick → UEngine::CleanupGameViewport

Closed 된 UGameViewportClient 들을 제거한다.

Viewport Closed Exit

모든 UGameViewportClient 가 Closed 되었다면 게임을 종료한다.

if ( GIsClient && GameViewport == NULL )
{
    appRequestExit( 0 );
    return;
}

Speed compensation

call stack : UGameEngine::Tick → UGameViewportClient::SetDropDetail

일정 frame rate 이하로 떨어지면 detail 을 떨어뜨린다.

UnPlayer.cpp
void UGameViewportClient::SetDropDetail( FLOAT DeltaSeconds )
{
    if ( GEngine->Client )
    {
        ...
        FLOAT FrameRate = 1 / DeltaSeconds;                   // 현재 frame rate
        AWorldInfo* WorldInfo       = GWorld->GetWorldInfo();
        WorldInfo->bDropDetail      = FrameRate < Clamp( GEngine->Client->MinDesiredFrameRate, 1.f, 100.f );
        WorldInfo->bAggressiveLOD   = FrameRate < clamp( GEngine->Client->MinDesiredFrameRate - 5.f, 1.f, 100.f );
    }
}

기준이 되는 frame rate (UClient.MinDesiredFrameRate) 은 아래와 같이 지정가능하다.

DefaultEngine.ini
[Engine.Client]
MinDesiredFrameRate=35.0

Static Tick

call stack : UGameEngine::Tick → UObject::Tick

약간의 시간을 할애하여 비동기 로딩을 수행한다.

UnObj.cpp
void UObject::StaticTick( FLOAT DeltaTime )
{
    ProcessAsyncLoading( TRUE, 0.005f );        // 5ms 의 시간을 들여 비동기 로드 수행
    ...
}

Handle seamless traveling

call stack : UGameEngine::Tick → FSeamlessTravelHandler::Tick

심리스 레벨 로딩을 수행한다.

if ( GSeamlessTravelHandler.IsInTransiton() )
{
    GSeamlessTravelHandler.Tick();
}

Tick the world

call stack : UGameEngine::Tick → UWorld::Tick

실제 게임이 펼쳐지고 있는 World 를 업데이트 한다. 여기서 모든 Actor 들이 업데이트 된다.

GameCycles = 0;
CLOCK_CYCLES( GameCycles );
GWorld->Tick( LEVELTICK_All, DeltaSeconds );
UNCLOCK_CYCLES( GameCycles );

Update the viewport

call stack : UGameEngine::Tick → UGameViewportClient::Tick

Game Viewport 를 업데이트한다. 여기서 모든 Interaction (UConsole, UUIInteraction, UPlayerManagerInteraction 등이 해당) 또한 업데이트 된다.

UnPlayer.cpp
void UGameViewportClient::Tick( FLOAT  DeltaTime )
{
    // Unreal Script 틱
    eventTick( DeltaTime );
 
    // 모든 Interaction 에 대한 틱
    for ( INT i = 0; i < GlobalInteractions.Num(); i++ )
    {
        UInteraction* Interaction = GlobalInteractions( i );
        Interaction->Tick( DeltaTime );
    }
}

Handle server traveling

server side 에서 레벨 이동에 관한 처리를 한다.

if ( GWorld->GetWorldInfo()->NextURL != TEXT("") )
{
    if ( (GWorld->GetWorldInfo()->NextSwitchCountdown -= DeltaSeconds) <= 0.f )
    {
        // 이전 게임에 작별을 고한다.
        if ( GWorld->GetGameInfo() != NULL )
        {
            GWorld->GetGameInfo()->eventGameEnding();
        }
 
        // 이동
        FString Error;
        Browse(
            FURL( &LastURL, *GWorld->GetWorldInfo()->NextURL, (ETravelType)GWorld->GetWorldInfo()->NextTravelType ),
            Error
        );
        GWorld->GetWorldInfo()->NextURL = TEXT("");
        return;
    }
 
}

Handle client traveling

client side 에서 레벨 이동에 관한 처리를 한다.

if ( TravelURL != TEXT("") )
{
    // 이전 게임에 작별을 고한다.
    if ( GWorld->GetGameInfo() != NULL )
    {
        GWorld->GetGameInfo()->eventGameEnding();
    }
 
    // 이동
    FString Error, TravelURLCopy = TravelURL;
    Browse(
        FURL( &LastURL, *TravelURLCopy, (ETravelType)TravelType ),
        Error
    );
    return;
}

Update Pending Level

client traveling 으로 레벨이 로드가 완료되면 GPendingLevel 로써 업데이트가 시작된다.

if ( GPendingLevel )
{
    // 로딩이 완료된 레벨의 업데이트를 시작. 주로 network 관련 작업 수행
    GPendingLevel->Tick( DeltaSeconds );
 
    // 에러 처리
    if ( GPendingLevel->ConnectionError.Len() > 0 )
    {
        // 동영상 재생을 중지
        StopMovie( FALSE );
 
        // Pending connect 실패 메세지 출력
        SetProgress( PMT_ConnectionFailure, LocalizeError( TEXT("ConnectionFailed_Title"), TEXT("Engine") ), GPendingLefel->ConnectionError );
 
        GPendingLevel = NULL;
    }
 
    ... // 접속 성공에 대한 처리. 아래에서 계속
}

client side 에서 server 로 접속할 때, 레벨 로드가 완료되면 GPendingLevel 로써 처리가 시작되며, network 관련 tick 을 돌기 시작한다. 여기서 server 로의 접속여부 및 현 레벨 실행에 필요한 패키지 등을 server 로부터 확인한다.

:!: UDN 에서는 server side 역시 이런 로직으로 수행된다고 하지만 직접 확인한 바로는 상기 로직이 수행되지 않았다.

Loading the pending map

상기 Update Pending Level 단계에서 GPendingLevel→Tick 은 아래 두가지 역할을 수행한다. (물론 다른 것들도 더 있겠지만)

  1. 성공적으로 server에 접속 했는지 여부 확인 ( UNetPenndingLevel::Tick → UTcpNetDriver::TickDispatch → UNetConnection::ReceivedRawPacket → UNetConnection::ReceivedPacket → UChannel::ReceivedRawBunch → UChannel::ReceivedSequecedBunch → UControlChannel::ReceivedBunch → UNetPendingLevel::NotifyControlMessage 에서 NMT_Welcome 메세지 타입을 받게될 때 세팅 )
  2. 이 레벨에 필요한 패키지가 있는지 여부 확인. 만약 있다면 server 에서 다운로드를 시도 ( 위 콜스택에서 NMT_Use 메세지 타입을 받게될 때 세팅 )

상기 두가지 역할을 모두 수행하여 만족된다면 (성공적으로 접속했으며 실행에 필요한 패키지를 모두 가지고 있다는 조건) client 는 UGameEngine::LoadMap 을 통해 map 로딩을 시작한다. 로딩 과정에서 에러가 발생하지 않으면 client 는 JOIN 커맨드를 날리고 게임에 참여한다.

if ( GPendingLevel )
{
    ... // 위에서 계속
 
    else if (
        GPendingLevel->bSuccessfullyConnected &&    // 성공적으로 server 에 접속했고
        GPendingLevel->FilesNeeded == 0 &&          // 실행에 필요한 파일이 더이상 없고
        !GPendingLevel->bSentJoinRequest )          // JOIN 메세지를 아직 보내지 않았다.
    {
        // LoadMap 시도
        FString Error;
        const UBOOL bLoadedMapSuccessfully = LoadMap( GPendingLevel->URL, GPendingLevel, Error );
 
        if ( Error != TEXT("") )
        {
            SetProgress( PMT_ConnectionFailure, LocalizeError( TEXT("ConnectionFailed_Title"), TEXT("Engine") ), Error );
        }
        else
        {
            // 접속중이라는 메세지를 보여주고
            TransitionType = TT_Connecting;
            RedrawViewports();
 
            // JOIN 신호를 보낸다.
            GPendingLevel->SendJoin();
            GPendingLevel->NetDriver = NULL;
        }
 
        // pending level 클리어
        GPendingLevel = NULL;
    }
}

Update memory stats

메모리 스탯을 업데이트 한다.

  • virtual & physical 메모리 사용량
  • 현재 frame 에서 Malloc, Realloc, Free, PhysicalAlloc, PhysicalFree 를 호출한 횟수
// virtual & physical 메모리 사용량
FStatGroup* MemGroup = GStatManager.GetGroup( STATGROUP_Memory );
if ( MemGroup->bShowGroup = TRUE )
{
    SIZE_T Virtual = 0;
    SIZE_T Physical = 0;
    GMalloc->GetAllocationInfo( Virtual, Physical );
    SET_DWORD_STAT( STAT_VirtualAllocSize, Virtual );
    SET_DWORD_STAT( STAT_PhyslcalAllocSize, Physical );
}
 
// 현재 frame 에서 Malloc 을 호출한 횟수 기록
static QWORD lastMallocCalls = 0;
DWORD CurrentFrameMallocCalls   = FMalloc::TotalMallocCalls - LastMallocCalls;
SET_DWORD_STAT( STAT_MallocCalls, CurrentFrameMallocCalls );
LastMallocCalls = FMalloc::TotalMallocCalls;
 
// 현재 frame 에서 Realloc 을 호출한 횟수 기록. Free, PhysicalAlloc, PhysicalFree 역시 마찬가지
...

Update the transition screen

TransitionType (Paused, Loading, Saving, Connecting, Precaching) 은 Viewport 에서 메시지를 출력하는데 사용된다. 접속중.. 혹은 멈춤.. 등을 출력

여기선 다음과 같은 일을 수행한다.

  • 게임에 참여하는 모든 플레이어의 PlayerController 정보를 받을때 까지 TransitionType 을 Connecting 상태로 유지한다.
  • 게임이 Paused 상태인지를 확인하여 TransitionType 을 Paused 로 변경한다.

Render everything

call stack : UGameEngine::Tick → UGameEngine::DrawViewports

모든 Physics 가 계산되고, 모든 Actor, Pawn, Characters, Players 등의 위치가 결정되고 나면 이제는 그것들을 그릴 차례다.

실질적인 그리기 로직을 모두 여기서 수행한다.

UnGame.cpp
void UGameEngine::DrawViewports( UBOOL bShouldPresent )
{
    if ( GameViewport != NULL )
    {
        GameViewport->eventLayoutPlayers();
        if ( GameViewport->Viewport != NULL )
        {
            GameViewport->Viewport->Draw( bShouldPresent );
        }
    }
}

Block on async loading if requested

비동기 로딩방식을 멈추고 모든 것을 로드한다.

LevelStreaming 을 비동기로 하는 와중에 Flushing 하거나, USeqAct_WaitForLevelsVisible 등의 시퀀스가 활성화 될 때 작동된다.5)

if ( GWorld->GetWorldInfo()->bRequestedBlockOnAsyncLoading )
{
    UBOOL bWorkdToDo = UObject::IsAsyncLoading();
 
    if ( !bWorkdToDo )
    {
        GWorld->UpdateLevelStreaming();
        bWorkdToDo = GWorld->IsVisibilityRequestPending();
    }
 
    if ( bWorkdToDo )
    {
        ...
 
        // RenderThread 정지
        ENQUEUE_UNIQUE_RENDER_COMMAND(
            SuspendingRedering,
            {
                extern UBOOL GGameThreadWantsToSuspendRendering;
                GGameThreadWantsToSuspendRendering = TRUE;
            }
        );
 
        // Render 커맨드 방출
        FlushRenderingCommands();
 
        // 레벨 스트리밍
        GWorld->FlushLevelStreaming();
 
        // RenderThread 가동
        ENQUEUE_UNIQUE_RENDER_COMMAND(
            ResumeRendering,
            {
                extern UBOOL GGameThreadWantsToSuspendRendering;
                GGameThreadWantsToSuspendRendering = FALSE;
                RHIResumeRendering();
            }
        );
    }
}

일반적인 경우에 (스트리밍 로드되므로) 작동되는 일이 없었으며, 모든 씬이 완벽히 로드된 후에 보여져야 하는 특수한 상황이 요구되면 프로그래머가 명시적으로 Flush 함수호출을 하거나 SeqAction 을 이용하여 사용해야 하는 듯 하다.

Update Audio

call stack : UGameEngine::Tick → UXAudio2Device::Update

Aidio 를 업데이트한다. Rendering 과정이 Listener 의 위치를 업데이트 하므로 그 이후에 Audio 업데이트가 이루어져야 한다.

if ( Client & Client->GetAudioDevice() )
{
    Client->GetAudioDevice()->Update( !GWorld->IsPaused() );
}

Update resource streaming

리소스 스트리밍을 수행한다. Viewport 가 view information 을 업데이트하고 나면 보여져야 할 리소스목록들이 명확해지고, 그 후에 그 정보를 토대로 리소스 스트리밍을 하는 방식이다.6)

if ( GIsClient )
{
    GStreamingManager->UpdateResourceStreaming( DeltaSeconds );
}

ScriptCallGraph Tick

UnrealScript 함수 호출에 관한 프로파일링 정보를 수집한다.

if ( GScriptCallGraph )
{
    GScriptCallGraph->Tick( DeltaSeconds );
}

GScriptCallGraph 인스턴스는 NULL 인 상태로 시작되며, 인게임 중 PROFILESCRIPT START (혹은 SCRIPTPROFILER START) 커맨드를 입력하면 인스턴스가 생성되며 프로파일링을 시작한다. (Tick 이 돌기 시작한다.)

반대로 프로파일링을 종료하려면 PROFILESCRIPT STOP (혹은 SCRIPTPROFILER STOP) 을 입력한다. 기존 수집된 정보를 없애고 다시 수집을 시작하려면 PROFILESCRIPT RESET 을 입력한다.

더 자세한 사항은 언리얼 스크립트 프로파일링을 참조

Update constraints

call stack : UGameEngine::Tick → UEngine::UpdateConstraintActors

게임 내 모든 Constraint 를 업데이트 한다. 인게임에서 이 작업이 수행되면 안된다.

UnEngine.cpp
void UEngine::UpdateConstraintActors()
{
    if ( bAreConstraintsDirty )
    {
        for ( FActorInterator It; It; ++It )
        {
            ARB_ConstraintActor* ConstraintActor = Cast<ARB_ConstraintActor>( *It );
            if ( ConstraintActor )
            {
                ConstraintActor->UpdateConstraintFramesFromActor();
            }
        }
 
        bAreConstraintsDirty = FALSE;
    }
}

Commit map changes

Map Change 에 대한 요청이 들어왔고 때가 되었다면 이를 수행한다.

UnGame.cpp
void UGameEngine::ConditionalCommitMapChange()
{
    if ( bShouldCommitPendingMapChange && IsPreparingMapChange() )
    {
        if ( !IsReadyForMapChange() )
        {
            FlushAsyncLoading();
        }
 
        CommitMapChange();
 
        bShouldCommitPendingMapChange = FALSE;
    }
}

참조

1) 일종의 갑작스러운 렉으로 생각할 수 있다. 이전 frame 에 비해 갑작스럽게 FPS 가 급감하는 경우가 이에 해당된다.
2) 엔진 내부적으로 frame time clamping 이 활성화 되었거나 benchmarking 테스트 중이라면 DeltaSeconds 를 신뢰할 수 없다.
3) 코드를 보면 알겠지만 실행 파라메터에 -TimeBetweenMemoryChartUpdates=갱신주기 -CaptureMemoryChartInfo=1 을 추가해주면 작동한다.
4) 이 파라메터를 추가하면 GShouldTraceAnimationUsage 변수가 TRUE 가 된다.
5) AWorldInfo.bRequestedBlockOnAsyncLoading 을 TRUE 로 해줌으로써 활성화 시킨다.
6) 내부적으로 FStreamingViewInfo 정보를 참조한다.