나만의 작은 도서관
[TIL][C++] 251112 MMO 서버 개발 135일차: [언리얼] LoadMap 안에서 일어나는 과정을 알아보자 본문
Today I Learn
[TIL][C++] 251112 MMO 서버 개발 135일차: [언리얼] LoadMap 안에서 일어나는 과정을 알아보자
pledge24 2025. 11. 13. 00:45주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
[언리얼] LoadMap 안에서 일어나는 과정을 알아보자
===LoadMap전 엔진 초기화 과정(처음 켰을때 한 번만 진행)===
1. GEngine::Start
- 엔진을 시작
{
SCOPED_BOOT_TIMING("GEngine->Start()");
GEngine->Start();
}
2. GEngine::Init()
- GameInstance, GameViewportClient, LocalPlayer를 생성 및 초기화

LoadMap을 기점으로 생성되는 객체

- LoadMap 이후에 생성되는 모든 객체는 UWorld에 종속되며, 레벨 전환 시 UWorld가 파괴되기 때문에 이에 종속된 객체들도 전부 파괴된다. 따라서, 위 사진에서 UEngine::LoadMap아래에 있는 모든 객체는 파괴된다고 볼 수 있다.
- 요약: LoadMap 후에 생성된 객체들은 해당 레벨에서만 유효하다.
LoadMap 첫 번째 과정. 맵 데이터 메모리에 올리기(Load)
- 우리가 흔히 Map이라고 부르는 레벨(ULevel)은 *. umap 파일 형식으로 되어있다.
- 이 파일에는 World, Level, Actor를 저장하고 있으며, 데이터는 직렬화된 상태로 존재한다.

- 직렬화를 왜 하는가? 에 대해서는 워낙 많이 다뤘으니 설명은 안 하겠다. 직렬화 전의 구조는 아래와 같이 생겼고, 이 구조가 실제로 플레이했을 때의 구조이다.

LoadMap 두 번째 과정. UWorld 초기화
- 메모리에 UWorld를 올렸으니 초기화를 해야 한다. (객체를 만들고 Init 메서드를 부르는 과정)
- GameInstance가 소유하고 있는 WorldContext에 새롭게 만든 UWorld 객체를 넣어 실행할 월드를 교체한다. (이 과정에 의해 UWorld와 GameInstance가 서로에 대해 알게 되어 UWorld에서 GI에, GI에서 UWorld에 접근이 가능)

- 실행할 World가 교체되었으니 GC의 대상으로 두기 위해 AddToRoot로 World를 등록하고, InitWorld를 호출하여 물리 / 내비게이션(Navigation) / AI / 오디오 같은 월드 시스템을 초기화한다.

- 월드 시스템 초기화가 완료되었다면, World는 GI에게 AGameMode 생성을 요청하고, 생성에 성공했다면 Map을 완전히 가져온다(Loading)


LoadMap 세 번째 과정. InitiailizeActorsForPlay() 호출
- InitiailizeActorsForPlay() 함수는 게임 플레이를 시작하기 위해 **“월드 내 모든 액터의 초기 상태를 설정”**한다.
- 이 함수를 거치게 되면 모든 액터의 Component들이 UWorld에 등록되며, 액터는 PostLoad가, 컴포넌트들은 InitializeComponent가 호출된다.
LoadMap 네 번째 과정. GameMode::InitGame() 호출
- InitGame은 GameMode를 초기화하며, 내부에서 AGameSession를 생성하게 된다.
- 잠깐 말하는 거지만, 모든 액터는 항상 UWorld를 통해서 스폰한다.

LoadMap 다섯 번째 과정. 월드에 존재하는 각 Level의 Actor 초기화

- 한 map에 여러 Level을 넣지 않았다면(SubLevel을 만들지 않았다면) 월드에 존재하는 레벨은 Persistent Level 하나만 존재한다. SubLevel을 만들었다면 여러 레벨이 한 map에 존재하게 되는 거고. 이러한 경우에 맞추기 위해 for문을 돌면서 모든 레벨을 초기화해 준다. 이때 사용하는 함수는 ULevel::RoutActorInitialize() 함수이다.
ULevel::RoutActorInitialize() 내부
- fall-through 방식이라는 게 사용되어 있다. fall-through이란 의도적으로 break문을 안 넣어서 현재 case문에서 문제가 없을 때 다음 case문으로 넘어가게끔 작성한 방식이다. 즉, 이전 case문에서 문제가 없었다면 이에 따른 Enum값을 변경하고, 다음 case문에서 변경된 Enum을 가지면 실행하는 방식. 예시를 들면 아래와 같다.
void func() { // State는 클래스 멤버 변수 // EMyEnum State = EMyEnum::FirstState; switch(State) { case EMyEnum::FirstState: { if(/**문제 있으면 진입*/) return; State = EMyEnum::SecondState; } case EMyEnum::SecondState: { if(/**문제 있으면 진입*/) return; State = EMyEnum::ThridState; } //... } }- ULevel::RoutActorInitialize() 내부에서는 이러한 fall-through을 while문을 돌면서 각 Actor에 대해 처리하도록 설계되어 있다. (여기서 처리되는 액터들은 레벨에 포함된 모든 액터들이다.)

- 이 과정을 성공적으로 끝까지 통과한 액터들은 InitializeComponent 삼총사가 호출된 상태가 된다.
GameMode도 일단은 Level에 포함된 Actor이다.
- 물론 GameMode도 일단은 Level에 포함된 Actor이기 때문에 GameMode도 이 과정을 거친다.
- 그래서 GameMode의 PreInitializeComponents도 여기서 호출되는데, 이때 GameState 객체와 GameNetworkManager 객체가 생성된다. 추가로, GameState는 InitGameState() 호출에 의해 초기화까지 진행된다.

- 이 시점에 도달했다면 LoadMap의 대부분이 처리된 상태이다. 모든 액터가 로드 및 초기화되었고, 월드가 Play 상태로 되었기 때문.
- 게임의 전체 상태를 관리하는 액터들도 전부 있는 상태. 위 과정에서 중간중간에 생성한 객체들이 있었는데, 이것들이 전부 이에 해당한다. 생성된 애들을 보면 아래와 같다.
- AGameModeBase
- UWorld 초기화(두 번째 과정)할 때 UWorld::SetGameMode()로 생성됨
- AGameSession
- GameMode 초기화(네 번째 과정)할 때 GameMode::InitGame()에 의해 생성됨
- AGameNetworkManager / AGameStateBase
- ULevel::RoutActorInitialize() 호출(다섯 번째 과정)할 때 AGameModeBase::PreInitializeComponents()에 의해 생성됨
- AGameModeBase
- 이제 World는 완전히 초기화가 되었으며, 게임을 대표하는 게임 프레임워크가 생겼다.
LoadMap 여섯 번째 과정. ULocalPlayer::SpawnPlayActor()
- 이제 로컬 플레이어에 대해서 처리한다. ULocalPlayer는 GI에서 관리하기 때문에 GI에서 가져온다.

- 위 코드처럼 각 ULocalPlayer마다 SpawnPlayActor를 호출하게 되는데, ULocalPlayer::SpawnPlayActor내부에선 아래와 같이 PlayerController를 생성하게 된다.

잠깐 이야기하는 로그인 프로세스
- 멀티플레이어 게임의 경우, 모든 플레이어는
- UWorld::SpawnPlayerActor → GameMode::PreLogin()
- PreLogin은 로그인 요청을 승인하거나 거부하는 역할을 담당한다.
- 로그인을 승인시킨 플레이어가 로컬이라면, 해당 플레이어를 게임에 추가하기 위해 AGameMode::Login() 함수가 호출된다.
- 당연히 GameMode는 서버만 가지고 있으므로 호출되는 위치는 서버이다.
- Login함수는 PlayerController를 만들어 반환한다.

- 물론 게임 플레이를 위해 World를 가져온 후, APlayerController를 생성하므로, 액터 생성 과정을 거치게 된다. 즉, PlayerController는 PostInitializecomponents가 호출된다.
- 그리고 이어서 컨트롤러는 AController::InitPlayerState()를 호출하여 내부에서 APlayerState를 생성한다.

- Login() 함수 처리가 끝나면, World는 통신을 위해 역할(SetRole)과 레플리케이션(SetReplicates)을 세팅하고, 완성된 Player객체를 연결한다.
- 모든 작업이 완료되면 GameMode::PostLogin 함수가 호출되며 이 플레이어가 참가한 결과로 발생해야 하는 모든 설정을 처리할 수 있는 기회가 제공된다.
- 기본적으로, GameMode는 새 PC에 대한 Pawn 생성을 PostLogin에서 처리한다.
GameMode의 관전자 처리
- 플레이어가 관전해야 함을 나타내도록 PlayerState를 구성하거나, 처음에 모든 플레이어가 관전자로 시작하도록 GameMode를 구성할 수도 있다.
- 관전자로 처리할 경우, GameMode는 Pawn을 생성하지 않고(관전 자니까!), 대신 날라댕기는 SpectatorPawn을 생성한다.
GameMode의 플레이어 처리
- PostLogin다음으로 RestartPlayer를 호출한다.
- RestartPlayer는 PC가 주어지면, 새 Pawn이 생성되어야 하는 위치를 나타내는 APlayerStart를 찾는다. → GetDefaultPawnClassForController를 통해 생성되어야 할 새 Pawn을 스폰 → 스폰된 폰은 SetPawn()을 통해 PC에 폰을 연결하고, FinishRestartPlayer를 통해 RestartPlayer가 완료되었음을 알린다.
마지막 단계: World::BeginPlay()
- Engine은 World::BeginPlay()가 호출한다.

- World는 BeginPlay()에서 GameMode의 StartPlay()를 호출 + 월드가 BeginPlay를 했음을 알리는 OnWorldBeginPlay 델리게이트를 Broadcast

- GameMode::StartPlay는 GameState에게 BeginPlay를 알리고,
- GameState는 모든 Actor들에게 BeginPlay를 알린다. (이때 Actor들의 BeginPlay가 호출된다)

- 이렇게 BeginPlay까지 처리가 전부 끝나면 LoadMap의 처리는 끝나게 된다.
