언리얼 프레임워크 구조 2
StartGameInstance 이후 흐름, Tick 구조, 월드 생성 과정
언리얼엔진 프레임워크 구조 2
전체 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GEngine->Start()
└ GameInstance->StartGameInstance()
└ Browse
└ LoadMap
├ 기존 월드 정리
├ UWorld 생성 (InitWorld)
├ ULevel / Actor 로드
├ InitializeActorsForPlay
└ UWorld::BeginPlay
EngineTick (매 프레임)
└ FEngineLoop::Tick
└ GEngine->Tick
└ UWorld::Tick
├ TickGroup(TG_PrePhysics) → Actor / Component Tick
├ 물리 시뮬레이션
└ TickGroup(TG_PostPhysics) → Actor / Component Tick
1. 엔진 시작
LaunchEngineLoop.cpp
1
GEngine->Start();
GameEngine.cpp
1
2
3
4
5
6
void UGameEngine::Init(IEngineLoop* InEngineLoop)
{
...
GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);
GameInstance->InitializeStandalone();
}
2. 인스턴스 초기화
1
2
3
4
5
6
7
8
9
10
11
12
void UGameInstance::InitializeStandalone(const FName InPackageName, UPackage* InWorldPackage)
{
WorldContext = &GetEngine()->CreateNewWorldContext(EWorldType::Game);
WorldContext->OwningGameInstance = this;
// 맵 로드 전 임시 더미 월드 생성
UWorld* DummyWorld = UWorld::CreateWorld(EWorldType::Game, false, InPackageName, InWorldPackage);
DummyWorld->SetGameInstance(this);
WorldContext->SetCurrentWorld(DummyWorld);
Init();
}
GameInstance.cpp
1
2
3
4
5
6
7
8
9
10
void UGameInstance::Init()
{
ReceiveInit();
UClass* SpawnClass = GetOnlineSessionClass();
OnlineSession = NewObject<UOnlineSession>(this, SpawnClass);
OnlineSession->RegisterOnlineDelegates();
...
}
GameInstance는 레벨이 바뀌어도 유지되는 객체다.
Init에서는 온라인 세션, 콘솔 입력 리스너 등 게임 전체에서 공유되는 요소들을 준비한다.
3. StartGameInstance
1
2
3
4
void UGameEngine::Start()
{
GameInstance->StartGameInstance();
}
GameInstance.cpp
1
2
3
4
5
6
7
8
9
void UGameInstance::StartGameInstance()
{
FURL DefaultURL;
FURL URL(&DefaultURL, *GetDefault<UGameMapsSettings>()->GetGameDefaultMap(), TRAVEL_Partial);
FString Error;
EBrowseReturnVal::Type BrowseRet = GetEngine()->Browse(*WorldContext, URL, Error);
...
}
여기서 설정 파일(GameMapsSettings)에 지정된 기본 맵 URL을 파싱하고, Browse를 호출한다.
이 시점부터 실제 게임 맵 로드가 시작된다.
4. Browse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EBrowseReturnVal::Type UEngine::Browse(FWorldContext& WorldContext, FURL URL, FString& Error)
{
if (URL.IsLocalInternal())
{
return LoadMap(WorldContext, URL, nullptr, Error)
? EBrowseReturnVal::Success
: EBrowseReturnVal::Failure;
}
else if (URL.IsInternal())
{
// 네트워크 연결 (PendingNetGame 생성)
...
}
...
}
Browse는 URL을 분석해 로컬 맵인지, 원격 서버 연결인지를 판단한다.
일반 싱글플레이 게임은 항상 로컬 경로이므로 LoadMap으로 바로 이어진다.
네트워크 게임에서는 PendingNetGame을 생성해 연결을 먼저 시도한 후 LoadMap을 호출한다.
5. LoadMap
LoadMap은 언리얼에서 가장 복잡한 함수 중 하나다.
기존 월드를 정리하고, 새 UWorld를 만들고, 레벨을 로드하고, BeginPlay를 실행하는 전 과정이 이 함수 안에서 진행된다.
Engine.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool UEngine::LoadMap(FWorldContext& WorldContext, FURL URL, UPendingNetGame* Pending, FString& Error)
{
// 1. 기존 월드 정리
CleanupWorld(WorldContext);
// 2. 패키지에서 맵 로드 or 새 UWorld 생성
UPackage* WorldPackage = LoadPackage(nullptr, *URL.Map, LOAD_None);
UWorld* NewWorld = UWorld::FindWorldInPackage(WorldPackage);
// 3. 월드 초기화
NewWorld->InitWorld();
// 4. WorldContext에 등록
WorldContext->SetCurrentWorld(NewWorld);
NewWorld->SetGameInstance(WorldContext.OwningGameInstance);
// 5. 레벨과 액터 준비
NewWorld->InitializeActorsForPlay(URL);
// 6. 게임 시작
NewWorld->BeginPlay();
return true;
}
6. UWorld 생성 과정
월드는 두 가지 방식으로 만들어진다.
맵 파일이 있는 경우에는 패키지에서 역직렬화로 복원되고, 런타임에 빈 월드가 필요한 경우에는 CreateWorld로 직접 생성된다.
1
2
3
4
5
6
7
8
9
10
UWorld* UWorld::CreateWorld(const EWorldType::Type InWorldType, bool bInformEngineOfWorld, ...)
{
UPackage* WorldPackage = InWorldPackage ? InWorldPackage : CreatePackage(nullptr);
UWorld* NewWorld = NewObject<UWorld>(WorldPackage, ...);
NewWorld->WorldType = InWorldType;
NewWorld->InitializeNewWorld(...);
return NewWorld;
}
월드가 생성되면 InitWorld가 호출되어 월드 단위의 시스템들이 초기화된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void UWorld::InitWorld(const InitializationValues IVS)
{
// 물리 씬 생성
CreatePhysicsScene();
// 네비게이션 시스템
FNavigationSystem::AddNavigationSystemToWorld(*this, ...);
// AI 시스템
CreateAISystem();
// FX 시스템
Scene = GetRendererModule().AllocateScene(...);
// World Subsystem 초기화
SubsystemCollection.Initialize(this);
}
World Subsystem은 UWorldSubsystem을 상속받으면 자동으로 등록된다.
InitWorld 시점에 SubsystemCollection이 해당 서브클래스들을 찾아 인스턴스를 생성한다.
월드가 소멸할 때 함께 소멸한다.
7. ULevel과 Actor 초기화
UWorld는 하나 이상의 ULevel로 구성된다.
항상 존재하는 PersistentLevel이 있고, 스트리밍으로 추가되는 레벨들이 있다.
맵 파일(.umap)은 UPackage 형태로 직렬화되어 있다.
LoadMap 과정에서 이 패키지를 역직렬화해 ULevel과 그 안의 Actor 인스턴스들을 복원한다.
1
2
3
4
UPackage 로드
└ UWorld 역직렬화
└ ULevel 역직렬화
└ Actor 인스턴스 복원 (CDO 복사 기반)
Actor가 패키지에서 복원될 때, 생성자는 이미 CDO 생성 시점에 실행된 상태다.
실제 인스턴스는 CDO를 복사한 형태로 만들어지고, 저장된 프로퍼티 값들로 덮어쓴다.
레벨이 월드에 추가될 때 AddToWorld가 호출된다.
1
2
3
4
5
6
void UWorld::AddToWorld(ULevel* Level, ...)
{
// 레벨의 Actor들을 월드에 등록
// 컴포넌트 등록
// InitializeActorsForPlay
}
8. Component 초기화
InitializeActorsForPlay에서 각 Actor에 대해 컴포넌트 초기화 흐름이 진행된다.
1
2
3
4
5
6
7
8
9
InitializeActorsForPlay
└ 각 Actor
├ PreInitializeComponents
├ InitializeComponents
│ └ 각 Component
│ └ RegisterComponent
│ ├ CreateRenderState (렌더링 등록)
│ └ CreatePhysicsState (물리 등록)
└ PostInitializeComponents
RegisterComponent 시점에 컴포넌트가 렌더링 시스템과 물리 씬에 등록된다.
이 시점에서는 아직 게임 로직이 시작되지 않은 상태다.
PostInitializeComponents는 모든 컴포넌트가 초기화된 이후에 호출된다.
액터 단위에서 추가 설정이 필요한 경우 이 시점에 처리한다.
네트워크 게임에서는 레플리케이션 설정도 이 시점에 완료된다.
9. BeginPlay
모든 Actor와 Component가 초기화되면 월드는 BeginPlay를 실행한다.
1
2
3
4
5
6
7
8
void UWorld::BeginPlay()
{
AGameModeBase* const GameMode = GetAuthGameMode();
if (GameMode)
{
GameMode->StartPlay();
}
}
1
2
3
4
5
6
UWorld::BeginPlay
└ AGameModeBase::StartPlay
└ AGameStateBase::HandleBeginPlay
└ 각 Actor::DispatchBeginPlay
└ Actor::BeginPlay
└ 각 Component::BeginPlay
GameMode가 먼저 StartPlay를 통해 게임 진행을 시작한다.
이후 레벨에 존재하는 Actor들의 BeginPlay가 순서대로 호출된다.
Component의 BeginPlay는 Actor의 BeginPlay 내부에서 호출된다.
10. Tick 흐름
Tick은 매 프레임 실행되는 사이클이다.
언리얼의 틱 시스템은 단순히 오브젝트를 순서대로 업데이트하는 방식이 아니다.
TickGroup과 의존성 그래프를 통해 실행 순서를 제어한다.
TickGroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace ETickingGroup
{
enum Type
{
TG_PrePhysics, // 물리 전
TG_StartPhysics,
TG_DuringPhysics,
TG_EndPhysics,
TG_PostPhysics, // 물리 후
TG_PostUpdateWork,
TG_NewlySpawned,
TG_MAX,
};
}
Actor와 Component는 자신의 Tick이 어느 그룹에서 실행될지 지정할 수 있다.
기본값은 TG_PrePhysics다.
FTickFunction
1
2
3
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PrePhysics;
RegisterActorTickFunction(Level);
Actor와 Component는 생성 시점에 FTickFunction을 통해 FTickTaskManager에 등록된다.
FTickFunction은 실제 Tick 콜백, 그룹, 의존성 정보를 포함한다.
같은 그룹 내에서도 실행 순서를 강제하려면 AddPrerequisite를 사용한다.
1
2
// CharacterMovement의 Tick이 Actor Tick 이후에 실행되도록 강제
CharacterMovement->PrimaryComponentTick.AddPrerequisite(this, PrimaryActorTick);
UWorld::Tick
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void UWorld::Tick(ELevelTick TickType, float DeltaSeconds)
{
// TG_PrePhysics 그룹 실행
TickTaskManager.RunTickGroup(TG_PrePhysics);
// 물리 시뮬레이션 시작
StartPhysicsSim();
// TG_DuringPhysics 그룹 실행 (물리와 병렬)
TickTaskManager.RunTickGroup(TG_DuringPhysics, false);
// 물리 결과 동기화
FetchResultsPhysics();
// TG_PostPhysics 그룹 실행
TickTaskManager.RunTickGroup(TG_PostPhysics);
// TG_PostUpdateWork 그룹 실행
TickTaskManager.RunTickGroup(TG_PostUpdateWork);
}
물리 시뮬레이션을 기준으로 Pre/Post가 나뉜다.
TG_PrePhysics 그룹의 Tick이 모두 끝난 후 물리 연산이 시작되고,
물리 결과가 나온 후 TG_PostPhysics 그룹이 실행된다.
전체 Tick 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
EngineTick
└ FEngineLoop::Tick
└ GEngine->Tick
└ 각 UWorld->Tick
├ RunTickGroup(TG_PrePhysics)
│ └ FTickFunction::ExecuteTick
│ ├ AActor::TickActor → Tick()
│ └ UActorComponent::TickComponent
├ StartPhysicsSim
├ RunTickGroup(TG_PostPhysics)
│ └ FTickFunction::ExecuteTick
│ └ Actor / Component Tick
└ FetchResultsPhysics
UEngine::Tick에서는 등록된 UWorld들을 순서대로 Tick한다.
에디터에서는 에디터 월드와 PIE 월드가 별도로 존재하기 때문에 Tick도 각각 실행된다.
정리
StartGameInstance 이후의 흐름을 크게 세 단계로 볼 수 있다.
첫 번째는 월드 생성 및 시스템 초기화다.
UWorld 오브젝트가 생성되고, 물리·네비게이션·AI·서브시스템 등 월드 단위의 인프라가 구축된다.
두 번째는 레벨과 Actor 로드다.
패키지에서 직렬화된 레벨 데이터를 역직렬화해 Actor 인스턴스들을 복원한다.
컴포넌트가 등록되고, 렌더링과 물리 상태가 만들어지는 시점이다.
세 번째는 BeginPlay다.
GameMode부터 시작해 Actor, Component 순서로 게임 로직이 시작된다.
이 과정이 끝나면 매 프레임 EngineTick이 실행되며,
TickGroup 기반의 스케줄링에 따라 Actor와 Component의 Tick이 순서대로 처리된다.