본문으로 바로가기

UnrealEngine의 초보자들이 GameFramework(GameMode, GameState, PlayerController, Pawn, PlayerState와 같은 클래스)를 제대로 이해하고 다루고 싶어 할 것이다.

 

익숙해지기 가장 좋은 방법 중 하나는 언리얼 엔진의 소스코드를 살펴보고 게임이 어떻게 부팅되는지를 확인해보는 것이다.

 

엔진 코드의 LaunchEngineLoop.cpp를 봐보자

약 7000줄의 코드가 전역 상태를 설정하고, 다양한 시스템을 초기화하는 코드들로 구성되어 있다.

약 20년전부터 만들어진 코드기도 하고, 더 높은 추상화를 위한 기반이므로 어쩔 수 없는 행동들이다.

 

이렇게 분석이 어려움에도, 엔진 진입점부터 시작해서 프로그램을 살펴보는게 좋다고 생각한다.

 

엔진의 시작은 Launch.cppGuardedMain함수부터 시작한다.

 

여기서 불필요한 코드들을 잘라내고, 실제 기본적인 게임 루프를 확인해보자

 

엔진의 메인 루프는 FEngineLoop라는 클래스에서 구현된다.

엔진 루프에 PreInit 단계가 있고, 엔진이 완전히 초기화 된 다음 종료할 준비가 될 때까지 모든 프레임을 체크(Tick) 하는 것을 볼 수 있다.

이런 함수들에서 어떤 일이 발생하는지 분석해보자

 

PreInit함수는 대부분의 모듈이 로드되는 곳 이다.

 

C++ 소스 코드가 포함된 게임 프로젝트나 플러그인을 만들때 소스 모듈을 정의하면 LoadingPhase를 지정하여 해당 모듈이 로드되는 시점을 정할 수 있다.

 

엔진은 다양한 소스 모듈로 분할된다.

일부 모듈은 다른 모듈보다 더 일찍 초기화되어야 하며 일부는 특정 플랫폼이나 특정 상황에서만 로드된다.

따라서 모듈 시스템은 코드베이스에서 여러 부분간의 종속성을 관리할 수 있도록하고, 특정 구성에 필요한 것만 로드할 수 있도록 해준다.

 

FEngineLoopPreInit단계를 시작하면, 일부 하위 수준 엔진 모듈을 로드하여 필수 시스템이 초기화 되고 필수 유형이 정의된다.

 

일단 첫번째로 LoadCoreModules를 통해 CoreUObject가 로드된다.

 

다음은 LoadPreInitModules를 통해 필수 모듈들을 로드한다.

 

그런 다음 프로젝트 또는 활성화된 플러그인에 초기 로드 단계에있는 모듈이 있는경우, 해당 모듈이 다음에 로드된다.

다음, 더 높은 수준의 엔진 모듈들이 로드된다.

 

언리얼 엔진의 PreInit에서 로드하는 모듈은 다음과 같다.

 

게임 모듈은 모든 필수 엔진 기능이 로드되고 초기화되는 시점이고, 게임 상태가 생성되기 전이다.

 

그럼 모듈이 로드되면 어떻게 될까?

 

일단 엔진은 모듈에 정의된 UObject 클래스들을 등록한다.

이렇게 되면 리플렉션 시스템이 해당 클래스를 인식하여, 각 클래스에 대해 Class Default Object를 구성한다.

그렇기때문에, 우리가 어떠한 ActorUCLASS() 를 사용하여 정의했다면, 생성자에 게임 플레이 관련 코드가 포함되서는 안되는 이유중 하나이다.

 

따라서 이 시점에서 FEngineLoopPreInit()은 필요한 모든 엔진, 프로젝트 및 플러그인 모듈을 로드하고

클래스들을 등록했으며, 하위 수준 시스템들을 초기화했습니다.

PreInit()이 완료되었으므로, 다음 Init()으로 넘어갈 수 있습니다.

 

FEngineLoop를 단순화 해보면 다음과 같습니다.

 

UEngine라는 객체에 작업들을 넘겨주는것을 볼 수 있습니다.

 

이 엔진은 UEditorEngineUGameEngine 모두 상속받아 구현된, UEngine이라는 클래스가 정의되어 있습니다.

 

처음 FEngineLoop::Init() 에 진입하면, 엔진 구성 파일을 확인하여

어떤 Engine클래스를 사용해야 하는지 파악합니다.

 

다음 클래스는 인스턴스를 생성하고, GEngine에 대입시킵니다.

해당 GEngineUnrealEngine.cpp에 선언되어 있습니다.

GEngine이 초기화 된 후, 모든 Delegate들에게 엔진이 초기화 되었다고 알립니다.

그 후, 늦게 로드되도록 구성된 모든 프로젝트 & 플러그인 모듈을 로드합니다.

 

그 후, 엔진이 Start 되고 초기화가 완료됩니다.

 

그래서 GEngine은 무엇을 할까요?

우리가 실제 게임을 만들고, 플레이 하려면 Map을 로드해야합니다.

우리의 프로젝트에서 Default Map을 설정하면, 엔진이 로드할 때 자동으로 해당 맵을 탐색하도록 지시하게 됩니다.

 

그럼 이제 GEngineInit을 확인해보겠습니다.

엔진은 Map을 로드하기전에, 초기화 되며 GameInstance, GameViewportClient, LocalPlayer등 몇 가지 중요한 개체를 생성하여 초기화합니다.

 

 

LoadMap을 기준으로 

GEngineInit에서는 UGameEngine, UGameInstance, UGameViewportClient, ULocalPlayer

GEngineLoadMap에서는 모든 액터가 포함된 UWorld가 생성되고, GameFramework의 핵심을 형성하는 GameMode와 같은 것들이 생성됩니다.

 

구분하는 주요 요소중 하나는 수명입니다.

높은 수준에서, 서로 다른 두 가지 수명이 있습니다.

즉, 지도가 로드되기 전에 일어나는 일들 / 그 후에 일어나는 일들 로 나뉩니다.

 

LoadMap 이전에 발생하는 모든 일은 프로세스의 수명과 연결되어 있습니다.

GameMode, GameState, PlayerController 같은 다른 모든 것들은 지도가 로드된 후에 생성되며 해당 맵에서 플레이하는 동안에만 유지됩니다.

즉, 새 맵을 열거나, 다른 서버에 연결하거나 기본 메뉴로 돌아가면 UWorld는 정리되며 LoadMap이 호출될때 만들어진 모든 오브젝트가 삭제됩니다.

 

그럼 LoadMap에서는 무슨 일이 일어날까요?

 

먼저 엔진은 전역 Delegate를 실행하며, Map이 곧 변경 될 것임을 알립니다.

 

우리가 LoadMap을 호출했을때, WorldContext가 파라미터에 있다는것을 확인했었습니다.

ContextUGameInstance가 만들어냈는데, 현재 로드되는 World를 추적하는 객체입니다.

 

다음은 UWorld를 로드해야합니다.

 

우리가 언리얼 에디터에서 Map을 만들었을때, 이 맵에는 우리가 배치한 액터가 포함된 하나 이상의 ULevel이 있습니다.

 

사실은 UWorld가 최상위 객체로서, ULevel 을 포함하게되고

ULevel은 각각의 AActor들을 포함하게 됩니다.

 

이 배치한 맵을 저장하게 되면

해당 World, Level, Actor를 저장하게 되는것이고 직렬화되어 *.umap 파일에 저장되게 됩니다.

 

따라서 LoadMap중에서 해당 Package를 찾아 로드합니다.

이때 UWorldWorldSetting, Actor들이 메모리에 다시 로드되게 됩니다.

 

그 다음 World를 초기화 합니다.

 

WorldContextGameInstance가 만들어 낸다고 위에서 설명했습니다.

WorldContext는 자신을 소유중인 GameInstance에 대한 ref를 가지고 있는데, 이를 통해 새로운 WorldGameInstance를 정할 수 있습니다.

 

이제 WorldContext에 새로운 World가 정해지게 되고 

가비지 수집이 되면 안되므로 AddToRoot를 통해 등록되고

InitWorld를 통해 물리 / 네비게이션 / AI / 오디오 같은 시스템을 초기화 합니다.

SetGameMode를 호출하게 되면 WorldGameInstance에게 GameMode 액터를 생성하도록 요청합니다.

 

GameMode가 존재하면 엔진은 지도를 완전히 로드합니다.

 

다음으로 InitailizeActorsForPlay를 호출합니다.

 

해당 함수는 일단, 모든 ActorComponent들을 World에 등록합니다.

 

모든 Actor내의 ActorComponent들은 세 가지 중요한 작업을 합니다.

 

첫째, 로드된 World에 대한 참조를 제공합니다.

 

둘째, OnRegister()를 호출하여 초기화를 할 수 있는 기회를 제공합니다.

 

그리고, 모든 컴포넌트가 PrimitiveComponent인 경우, FPrimitiveSceneProxy가 생성되어 UWorld의 렌더 스레드 버전인 FScene에 추가됩니다.

 

Component들이 등록되면, UWorldGameModeInitGame을 호출합니다.

 

그러면 GameModeGameSession Actor를 생성하게 됩니다.

 

그 후, WorldLevel별 루프를 돌면서 각 레벨의 Actor를 초기화 합니다.

 

이땐 자주봤던 함수가 등장하게 됩니다.

첫번째 루프에서 PreInitializeComponents()를 호출한 후

두번째 루프에서 InitializeComponentsPostInitializeComponents를 호출합니다.

이를 통해 액터는 Component가 등록된 후 초기화 되기 전 / 후 시점에 처리를 할 수 있습니다.

 

물론 GameModePreInitializeComponents도 여기서 호출됩니다.

그럼 GameModeGameState 객체를 생성하여 이를 World와 연결하고 

게임모드의 InitGameState 함수를 호출합니다.

 

InitializeComponent에서는 두 가지 를 확인합니다.

bAutoActivate가 활성화 되어있으면 해당 컴포넌트를 활성화 합니다.

bWantInitializeComponent가 활성화 되어있으면, 해당 컴포넌트를 초기화 합니다.

 

PostInitializeComponent는 컴포넌트들이 다 초기화 된 후 완전한 상태에 있을때에 호출되는 가장 초기 지점입니다.

 

이 시점에서 LoadMap이 대부분 처리 완료되었습니다.

모든 액터가 로드 및 초기화 되었으며, 월드가 플레이용으로 표시되었습니다.

 

이제 게임의 전체 상태를 관리하는데 사용되는 액터들이 있습니다.

 

AGameModeBase : 게임 모드는 규칙을 정의합니다.

AGameSession, AGameNetworkManager : 서버 전용입니다. NetworkManager는 치트 감지 / 움직임 예측과 같은 항목을 구성하는데 사용됩니다.

온라인 게임의 경우 GameSession은 로그인 요청을 승인하고, 온라인 서비스 (Steam같은) 에 대한 인터페이스 역할을 합니다.

AGameStateBase : 서버에서 생성되며, 서버에서만 변경권한이 있지만 모든 클라이언트에 복제됩니다.

따라서 GameState는 게임 상태와 관련된 데이터를 저장하는 곳이며 모든 플레이어가 사용할  수 있어야 합니다.

 

이제 World가 완전히 초기화 되었으며, 게임을 대표하는 게임 프레임워크가 생겼습니다.

 

LoadMapGameInstance에 있는 모든 LocalPlayers를 찾아냅니다.

일반적으론 하나만 있습니다.

해당 LocalPlayerSpawnPlayActor 합니다.

 

여기서 SpawnPlayActor는 내부에서 PlayerController를 생성합니다.

앞서 본 것처럼, ULocalPlayer는 엔진의 플레이어 표현입니다. 

반면 PlayerController는 게임 세계 내 플레이어의 표현입니다. ( UEngine::LoadMap에서 생성됨 )

 

ULocalPlayerUPlayer에서 상속받은 클래스입니다.

네트워크 환경에서 연결된 플레이어를 나타내는 UNetConnection 클래스도 있습니다.

 

모든 플레이어가 게임에 참여하려면, 로컬이든 네트워크는 관계없이 로그인 프로세스를 거쳐야 합니다.

 

해당 프로세스는 GameMode에 의해 처리됩니다.

GameModePreLogin은 네트워크 연결 시도에 대해서만 호출됩니다.

로그인 요청을 승인하거나 거부하는 역할을 담당합니다.

 

연결요청이 승인되었거나, 플레이어가 로컬이기때문에 플레이어를 게임에 추가하기 위한 작업을 진행하면 Login() 함수가 호출됩니다.

Login함수는 PlayerController를 만들어 반환합니다.

 

물론 게임 플레이를 위해 World를 가져온 후, PlayerControllerActor를 생성하므로, 해당 Actor는 생성 시 초기화 됩니다.

즉, PlayerControllerPostInitializeComponents 가 호출됩니다.

그리고 이어서, PlayerState 액터가 생성됩니다.

 

PlayerControllerPlayerState는 게임에 대한 서버 권한 표현입니다.

PlayerState는 모든 사람이 게임에 대해 알아야 하는 데이터가 포함되어 있습니다.

GameMode & GameState와 유사합니다.

 

Login 함수 처리가 끝나면, World는 네트워킹을 위해 SetRole, SetReplicates 같은 함수를 처리한 후,

Player 객체를 연결합니다.

 

모든 작업이 완료되면 GameModePostLogin 함수가 호출되어 이 플레이어가 참가한 결과로 발생해야 하는 모든 설정을 처리할 수 있는 기회를 제공합니다.

기본적으로, GameMode는 새 PlayerController에 대한 Pawn 생성을 이곳에서 처리합니다.

PawnPlayerController가 소유할 수 있는 특수한 유형의 Actor일 뿐입니다.

 

 

PlayerController는 기본 Controller 클래스를 상속받은 클래스이며, 비 플레이어가 사용하는 컨트롤러인 AIController가 있습니다.

ControllerActor를 처리하는 뇌를 담당하고 Pawn은 액터의 세계 내 표현일 뿐입니다.

 

따라서, 새 플레이어가 게임에 참가하면 기본 GameMode구현은 새 PlayerController가 소유할 Pawn을 생성합니다.

 

GameMode는 관전자도 처리할 수 있습니다.

플레이어가 관전해야 함을 나타내도록 PlayerState를 구성하거나, 처음에 모든 플레이어가 관전자로 시작하도록 GameMode를 구성할 수 있습니다.

이 경우 GameModePawn을 생성하지 않고, 대신 PlayerControllerWorld와 상호작용하지 않고 날아다닐 수 있는 자체 SpectatorPawn을 생성합니다.

 

GameMode는 관전자가 아니라면 PostLogin에서 RestartPlayer 를 처리합니다.

우리가 어떠한 게임을 할때, 게임 중에 사망했다면 부활하기 전까지 근처를 확인할 수 있었다는 게 생각나시나요?

스카이림에서 죽은 후, 살아나기 전까지 캐릭터가 래그돌이 되며 배경이 보인다.

 

이 때, PlayerController는 계속 유지되어 있습니다.

플레이어가 부활할 준비가 됐다면,, 게임은 새로운 Pawn을 생성해야 합니다.

RestartPlayer가 하는 일은 다음과 같습니다.

 

PlayerController가 주어지면, 새 Pawn이 생성되어야 하는 위치를 나타내는 액터를 찾습니다. (APlayerStart)

다음 GetDefaultPawnClassForController를 통해 생성되어야 할 새 Pawn을 생성합니다.

Pawn이 생성되면 SetPawn을 통해 PlayerController가 연결되게 된 후

FinishRestartPlayer를 통해 RestartPlayer가 완료되었음을 처리합니다.

 

 

이제 다시 LoadMap() 함수로 돌아갑니다.

게임을 실제로 시작하기 위한 모든 준비가 완료되었습니다.

이제 남은것은 BeginPlay() 함수를 호출하는 것 뿐입니다.

EngineWorldBeginPlay를 알립니다.

 

World는 또다시 GameModeBeginPlay를 알립니다.

 

GameMode는 또 다시 GameState에 알리고, 

GameState는 모든 Actor들에게 BeginPlay를 알립니다.

 

이렇게 BeginPlay까지 처리를 하게 되면, LoadMap의 모든 처리가 끝나게 되며

실제로 GameLoop에 포함되게 됩니다.

 

 

Ref : Youtube