게임 엔진 틱 코드 흐름
call stack : UGameEngine::Tick → FMalloc::Tick
UE3 에 사용되는 메모리 할당자에 대한 틱을 처리한다. 헌데 실제 처리하는 내용은 아무것도 없다. 일단은 빈 껍데기
call stack : UGameEngine::Tick → UEngine::TickFPSChart
FPS (Frame Per Seond) 및 Hitch1) 정보에 대하여 구간별로 기록해둔다.
// 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 에 해당하는 구간에 기록되어진다.
call stack : UGameEngine::Tick → UEngine::TickMemoryChart
메모리 사용 상태를 기록한다. 커맨드라인에 특정 옵션3)을 넣어줘야 작동된다.
작동할 경우 주기적으로 아래의 코드를 수행한다.
FMemoryChartEntry NewMemoryEntry; NewMemoryEntry.UpdateMemoryChartStats(); GMemoryChart.AddItem( NewMemoryEntry );
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\ 하위에 저장된다.
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;
call stack : UGameEngine::Tick → UEngine::CleanupGameViewport
Closed 된 UGameViewportClient 들을 제거한다.
모든 UGameViewportClient 가 Closed 되었다면 게임을 종료한다.
if ( GIsClient && GameViewport == NULL ) { appRequestExit( 0 ); return; }
call stack : UGameEngine::Tick → UGameViewportClient::SetDropDetail
일정 frame rate 이하로 떨어지면 detail 을 떨어뜨린다.
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) 은 아래와 같이 지정가능하다.
[Engine.Client] MinDesiredFrameRate=35.0
call stack : UGameEngine::Tick → UObject::Tick
약간의 시간을 할애하여 비동기 로딩을 수행한다.
void UObject::StaticTick( FLOAT DeltaTime ) { ProcessAsyncLoading( TRUE, 0.005f ); // 5ms 의 시간을 들여 비동기 로드 수행 ... }
call stack : UGameEngine::Tick → FSeamlessTravelHandler::Tick
심리스 레벨 로딩을 수행한다.
if ( GSeamlessTravelHandler.IsInTransiton() ) { GSeamlessTravelHandler.Tick(); }
call stack : UGameEngine::Tick → UWorld::Tick
실제 게임이 펼쳐지고 있는 World 를 업데이트 한다. 여기서 모든 Actor 들이 업데이트 된다.
GameCycles = 0; CLOCK_CYCLES( GameCycles ); GWorld->Tick( LEVELTICK_All, DeltaSeconds ); UNCLOCK_CYCLES( GameCycles );
call stack : UGameEngine::Tick → UGameViewportClient::Tick
Game Viewport 를 업데이트한다. 여기서 모든 Interaction (UConsole, UUIInteraction, UPlayerManagerInteraction 등이 해당) 또한 업데이트 된다.
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 ); } }
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; } }
client side 에서 레벨 이동에 관한 처리를 한다.
if ( TravelURL != TEXT("") ) { // 이전 게임에 작별을 고한다. if ( GWorld->GetGameInfo() != NULL ) { GWorld->GetGameInfo()->eventGameEnding(); } // 이동 FString Error, TravelURLCopy = TravelURL; Browse( FURL( &LastURL, *TravelURLCopy, (ETravelType)TravelType ), Error ); return; }
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 역시 이런 로직으로 수행된다고 하지만 직접 확인한 바로는 상기 로직이 수행되지 않았다.
상기 Update Pending Level 단계에서 GPendingLevel→Tick 은 아래 두가지 역할을 수행한다. (물론 다른 것들도 더 있겠지만)
상기 두가지 역할을 모두 수행하여 만족된다면 (성공적으로 접속했으며 실행에 필요한 패키지를 모두 가지고 있다는 조건) 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; } }
메모리 스탯을 업데이트 한다.
// 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 역시 마찬가지 ...
TransitionType (Paused, Loading, Saving, Connecting, Precaching) 은 Viewport 에서 메시지를 출력하는데 사용된다. 접속중.. 혹은 멈춤.. 등을 출력
여기선 다음과 같은 일을 수행한다.
call stack : UGameEngine::Tick → UGameEngine::DrawViewports
모든 Physics 가 계산되고, 모든 Actor, Pawn, Characters, Players 등의 위치가 결정되고 나면 이제는 그것들을 그릴 차례다.
실질적인 그리기 로직을 모두 여기서 수행한다.
void UGameEngine::DrawViewports( UBOOL bShouldPresent ) { if ( GameViewport != NULL ) { GameViewport->eventLayoutPlayers(); if ( GameViewport->Viewport != NULL ) { GameViewport->Viewport->Draw( bShouldPresent ); } } }
비동기 로딩방식을 멈추고 모든 것을 로드한다.
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 을 이용하여 사용해야 하는 듯 하다.
call stack : UGameEngine::Tick → UXAudio2Device::Update
Aidio 를 업데이트한다. Rendering 과정이 Listener 의 위치를 업데이트 하므로 그 이후에 Audio 업데이트가 이루어져야 한다.
if ( Client & Client->GetAudioDevice() ) { Client->GetAudioDevice()->Update( !GWorld->IsPaused() ); }
리소스 스트리밍을 수행한다. Viewport 가 view information 을 업데이트하고 나면 보여져야 할 리소스목록들이 명확해지고, 그 후에 그 정보를 토대로 리소스 스트리밍을 하는 방식이다.6)
if ( GIsClient ) { GStreamingManager->UpdateResourceStreaming( DeltaSeconds ); }
UnrealScript 함수 호출에 관한 프로파일링 정보를 수집한다.
if ( GScriptCallGraph ) { GScriptCallGraph->Tick( DeltaSeconds ); }
GScriptCallGraph 인스턴스는 NULL 인 상태로 시작되며, 인게임 중 PROFILESCRIPT START (혹은 SCRIPTPROFILER START) 커맨드를 입력하면 인스턴스가 생성되며 프로파일링을 시작한다. (Tick 이 돌기 시작한다.)
반대로 프로파일링을 종료하려면 PROFILESCRIPT STOP (혹은 SCRIPTPROFILER STOP) 을 입력한다. 기존 수집된 정보를 없애고 다시 수집을 시작하려면 PROFILESCRIPT RESET 을 입력한다.
더 자세한 사항은 언리얼 스크립트 프로파일링을 참조
call stack : UGameEngine::Tick → UEngine::UpdateConstraintActors
게임 내 모든 Constraint 를 업데이트 한다. 인게임에서 이 작업이 수행되면 안된다.
void UEngine::UpdateConstraintActors() { if ( bAreConstraintsDirty ) { for ( FActorInterator It; It; ++It ) { ARB_ConstraintActor* ConstraintActor = Cast<ARB_ConstraintActor>( *It ); if ( ConstraintActor ) { ConstraintActor->UpdateConstraintFramesFromActor(); } } bAreConstraintsDirty = FALSE; } }
Map Change 에 대한 요청이 들어왔고 때가 되었다면 이를 수행한다.
void UGameEngine::ConditionalCommitMapChange() { if ( bShouldCommitPendingMapChange && IsPreparingMapChange() ) { if ( !IsReadyForMapChange() ) { FlushAsyncLoading(); } CommitMapChange(); bShouldCommitPendingMapChange = FALSE; } }