원래는 ULevel이나 ClientTravel 부분부터 상세히 찾아들어가보려 했는데 그러기에 앞서 Level 정보를 담고 있는 FWorldContext의 데이터나 대략적인 변수 용처를 먼저 알고 들어가는 것이 개념 정리에 좋을 것 같아 FWorldContext를 먼저 공부 해보았다.

 

/** FWorldContext
 *	A context for dealing with UWorlds at the engine level. As the engine brings up and destroys world, we need a way to keep straight
 *	what world belongs to what.
 *
 *	WorldContexts can be thought of as a track. By default we have 1 track that we load and unload levels on. Adding a second context is adding
 *	a second track; another track of progression for worlds to live on. 
 *
 *	For the GameEngine, there will be one WorldContext until we decide to support multiple simultaneous worlds.
 *	For the EditorEngine, there may be one WorldContext for the EditorWorld and one for the PIE World.
 *
 *	FWorldContext provides both a way to manage 'the current PIE UWorld*' as well as state that goes along with connecting/travelling to 
 *  new worlds.
 *
 *	FWorldContext should remain internal to the UEngine classes. Outside code should not keep pointers or try to manage FWorldContexts directly.
 *	Outside code can still deal with UWorld*, and pass UWorld*s into Engine level functions. The Engine code can look up the relevant context 
 *	for a given UWorld*.
 *
 *  For convenience, FWorldContext can maintain outside pointers to UWorld*s. For example, PIE can tie UWorld* UEditorEngine::PlayWorld to the PIE
 *	world context. If the PIE UWorld changes, the UEditorEngine::PlayWorld pointer will be automatically updated. This is done with AddRef() and
 *  SetCurrentWorld().
 *
 */

FWorldContext 구조체 선언 위에 적혀 있는 설명 주석입니다.

대략 번역해보면 다음과 같다.

  • 엔진 단위에서 UWorlds를 관리하기 위한 Context
  • 기본적으로 1개를 소지 하고 있다.
  • World간의 이동 뿐만 아니라 현재 PIE(Play In Editor, 에디터에서 실행하는 경우)의 UWorld까지 관리하는 방안을 제공
  • 반드시 UEngine 내부에서만 접근되어야 하며 외부에서 직접적으로 관리되어서는 안됨.
  • 외부에서는 UWorld*를 사용할 수 있으며 이를 엔진 단위 함수로 전달할 수 있다.
    이때 엔진 코드는 주어진 UWorld*와 연관된 WorldContext를 탐색할 수 있다.
  • 편의를 위해 UWorld*의 외부 포인터를 유지할 수 있다.
    예를 들어, PIE는 UEditorEngine::PlayWorld 포인터를 PIE WorldContext에 의존시킬 수 있다.
    만약 PIE UWorld가 바뀌면, PlayWorld 포인터는 AddRef()와 SeetCurrentWorld()함수로 인해 자동으로 갱신이 된다.

 

정확히 이해 못하겠지만 여기서 확실한 것이 하나 있습니다.

우리는 ULevel을 공부하기 전에 UWorld를 먼저 알아야 합니다.

이에 대해 매우 잘 정리된 블로그 포스트를 공유드립니다.

https://www.zhihu.com/column/insideue4

 

InsideUE5

深入UE5剖析源码,浅出游戏引擎架构理念

www.zhihu.com

중국에서 작성된 포스트인데 구글 번역으로 보면 내용 설명과 더불어 구조적인 고민이나 설명들이 여타 튜토리얼이나 문서에서는 볼 수 없는 것들이 많습니다.

원래는 출처 넣고 번역을 하려고 했으나 펌을 금지하기 때문에 링크로 공유 하겠습니다.

 

UWORLD

https://docs.unrealengine.com/5.0/en-US/API/Runtime/Engine/Engine/UWorld/

 

UWorld

The World is the top level object representing a map or a sandbox in which Actors and Components will exist and be rendered.

docs.unrealengine.com

 

UWorld는 일종의 ULevel의 대표 오브젝트다.

하나의 Persistent Level과 함께 월드 볼륨상에서 로드되거나 취소될 수 있는 Streaming Level을 사용할 수 있고, 여러가지 Level들을 WorldComposition으로 정리해 사용할 수 있게 해준다.

Stand Alone Game에서는 일반적으로 하나의 World만 존재하나, 에디터 안에서는 많은 World들이 수정될 수 있다.

 

ULEVEL

https://docs.unrealengine.com/4.27/en-US/API/Runtime/Engine/Engine/ULevel/

 

ULevel

The level object.

docs.unrealengine.com

말 그대로 Level 오브젝트이다.

Level 내에 존재하는 Actor 리스트나 BSP 정보, Brush List들을 들고 있다.

모든 Level은 Outer로 World을 가지고 있고, Persistent Level로 지정될 수 있으며, 여러 Sub Level들을 가질 수 있다.

 

몇가지 용어 설명을 추가하자면

Persistent Level: 아무것도 없을 때에 호출되는 기본 Level.

Sub Level: Persistent Level에서 일정부분을 담당하는 Level. 지역 뿐 아니라 사운드나 레이아웃 등 기능 별로도 분리가 가능하다. 이는 동시 작업이 불가한 Binary 파일인 레벨 작업에서 최소한의 동시작업을 지원한다.

 

즉 UWorld와 ULevel들의 관계는 대략 이런 식인 것이다.

이에 대한 부분은 차후 UWorld를 공부 할 때 자세히 다뤄보겠다.

 

본론으로 돌아가, FWorldContext는 위에서 우리가 길게 핥아본 UWorld를 관리하는 구조체이다.

이제 코드 부분을 다시 살펴보자.

더보기
USTRUCT()
struct FWorldContext
{
	GENERATED_USTRUCT_BODY()

	/**************************************************************/
	
	TEnumAsByte<EWorldType::Type>	WorldType;

	FSeamlessTravelHandler SeamlessTravelHandler;

	FName ContextHandle;

	/** URL to travel to for pending client connect */
	FString TravelURL;

	/** TravelType for pending client connects */
	uint8 TravelType;

	/** URL the last time we traveled */
	UPROPERTY()
	struct FURL LastURL;

	/** last server we connected to (for "reconnect" command) */
	UPROPERTY()
	struct FURL LastRemoteURL;

	UPROPERTY()
	TObjectPtr<UPendingNetGame>  PendingNetGame;

	/** A list of tag/array pairs that is used at LoadMap time to fully load packages that may be needed for the map/game with DLC, but we can't use DynamicLoadObject to load from the packages */
	UPROPERTY()
	TArray<struct FFullyLoadedPackagesInfo> PackagesToFullyLoad;

	/**
	 * Array of package/ level names that need to be loaded for the pending map change. First level in that array is
	 * going to be made a fake persistent one by using ULevelStreamingPersistent.
	 */
	TArray<FName> LevelsToLoadForPendingMapChange;

	/** Array of already loaded levels. The ordering is arbitrary and depends on what is already loaded and such.	*/
	UPROPERTY()
	TArray<TObjectPtr<class ULevel>> LoadedLevelsForPendingMapChange;

	/** Human readable error string for any failure during a map change request. Empty if there were no failures.	*/
	FString PendingMapChangeFailureDescription;

	/** If true, commit map change the next frame.																	*/
	uint32 bShouldCommitPendingMapChange:1;

	/** Handles to object references; used by the engine to e.g. the prevent objects from being garbage collected.	*/
	UPROPERTY()
	TArray<TObjectPtr<class UObjectReferencer>> ObjectReferencers;

	UPROPERTY()
	TArray<struct FLevelStreamingStatus> PendingLevelStreamingStatusUpdates;

	UPROPERTY()
	TObjectPtr<class UGameViewportClient> GameViewport;

	UPROPERTY()
	TObjectPtr<class UGameInstance> OwningGameInstance;

	/** A list of active net drivers */
	UPROPERTY(transient)
	TArray<FNamedNetDriver> ActiveNetDrivers;

	/** The PIE instance of this world, -1 is default */
	int32	PIEInstance;

	/** The Prefix in front of PIE level names, empty is default */
	FString	PIEPrefix;

	/** The feature level that PIE world should use */
	ERHIFeatureLevel::Type PIEWorldFeatureLevel;

	/** Is this running as a dedicated server */
	bool	RunAsDedicated;

	/** Is this world context waiting for an online login to complete (for PIE) */
	bool	bWaitingOnOnlineSubsystem;

	/** Handle to this world context's audio device.*/
	uint32 AudioDeviceID;

	/** Custom description to be display in blueprint debugger UI */
	FString CustomDescription;

	// If > 0, tick this world at a fixed rate in PIE
	float PIEFixedTickSeconds  = 0.f;
	float PIEAccumulatedTickSeconds = 0.f;

	/**************************************************************/

	/** Outside pointers to CurrentWorld that should be kept in sync if current world changes  */
	TArray<UWorld**> ExternalReferences;

	/** Adds an external reference */
	void AddRef(UWorld*& WorldPtr)
	{
		WorldPtr = ThisCurrentWorld;
		ExternalReferences.AddUnique(&WorldPtr);
	}

	/** Removes an external reference */
	void RemoveRef(UWorld*& WorldPtr)
	{
		ExternalReferences.Remove(&WorldPtr);
		WorldPtr = nullptr;
	}

	/** Set CurrentWorld and update external reference pointers to reflect this*/
	ENGINE_API void SetCurrentWorld(UWorld *World);

	/** Collect FWorldContext references for garbage collection */
	void AddReferencedObjects(FReferenceCollector& Collector, const UObject* ReferencingObject);

	FORCEINLINE UWorld* World() const
	{
		return ThisCurrentWorld;
	}

	FWorldContext()
		: WorldType(EWorldType::None)
		, ContextHandle(NAME_None)
		, TravelURL()
		, TravelType(0)
		, PendingNetGame(nullptr)
		, bShouldCommitPendingMapChange(0)
		, GameViewport(nullptr)
		, OwningGameInstance(nullptr)
		, PIEInstance(INDEX_NONE)
		, PIEWorldFeatureLevel(ERHIFeatureLevel::Num)
		, RunAsDedicated(false)
		, bWaitingOnOnlineSubsystem(false)
		, AudioDeviceID(INDEX_NONE)
		, ThisCurrentWorld(nullptr)
	{ }

private:

	UWorld*	ThisCurrentWorld;
};

 

WorldType

WorldType은 현재 World가 어떤 케이스인지 지칭한다.

타입인 EWorldType은 다음과 같다.

/** Specifies the goal/source of a UWorld object */
namespace EWorldType
{
	enum Type
	{
		/** An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels */
		None,

		/** The game world */
		Game,

		/** A world being edited in the editor */
		Editor,

		/** A Play In Editor world */
		PIE,

		/** A preview world for an editor tool */
		EditorPreview,

		/** A preview world for a game */
		GamePreview,

		/** A minimal RPC world for a game */
		GameRPC,

		/** An editor world that was loaded but not currently being edited in the level editor */
		Inactive
	};
}

여기서 잘 모르는 것이 몇 있다.

Preview가 붙은 것은 오브젝트나 머테리얼에서 미리보기를 하는 World들을 지칭한다.

 

SeamlessTravelHandler

Seamless World Travel을 관리하는 구조체의 변수이다.

https://docs.unrealengine.com/5.0/ko/travelling-in-multiplayer-in-unreal-engine/

 

멀티플레이어에서의 이동(Travel)

멀티플레이어에서의 travel, 이동 작동방식에 대한 개요입니다.

docs.unrealengine.com

구조체인만큼 추가적으로 확인해야 할 사항이 많기 때문에 차후 추가로 공부하는 기회를 갖도록 하겠다.

 

ContextHandle

FWorldContext를 구분짓는 일종의 Key이다.

이 값으로 WorldContext를 검색한다.

 

TravelURL

아래 포스트에서 어느정도 설명을 했으나 한번 더 짚어보겠다.

https://redchiken.tistory.com/310

 

UGameplayStatics::OpenLevel 분석 - 1

UGamePlayStatics::OpenLevel은 C++에서 레벨을 변경하고자 할 때 사용하는 함수이다. /** * Travel to another level * * @param LevelName the level to open * @param bAbsolute if true options are reset, if..

redchiken.tistory.com

TravelURL은 client connect를 할 Level의 URL이다.

형식은 [LevelName]?[Option]으로 구성되어 있다.

 

TravelType

TravelURL과 마찬가지로 어떤 형태로 Travel을 할지에 대한 정보이다.

enum ETravelType
{
    TRAVEL_Absolute,
    TRAVEL_Partial,
    TRAVEL_Relative,
    TRAVEL_MAX,
}

Absolute와 Relative는 각각 절대경로, 상대경로로 이해하면 된다.

하지만 Partial은 어떤 경로인지 감이 잘 안잡힐 수 있다.

TRAVEL_Partial의 의미를 유추할 수 있는 코드 부분은 UEngine::HandleDisconnect이다.

void UEngine::HandleDisconnect( UWorld *InWorld, UNetDriver *NetDriver )
{
	// There must be some context for this disconnect
	check(InWorld || NetDriver);

	// If the NetDriver that failed was a pending netgame driver, cancel the PendingNetGame
	CancelPending(NetDriver);

	// InWorld might be null. It might also not map to any valid world context (for example, a pending net game disconnect)
	// If there is a context for this world, setup client travel.
	if (FWorldContext* WorldContext = GetWorldContextFromWorld(InWorld))
	{
		// Remove ?Listen parameter, if it exists
		WorldContext->LastURL.RemoveOption( TEXT("Listen") );
		WorldContext->LastURL.RemoveOption( TEXT("LAN") );

		// Net driver destruction will occur during LoadMap (prevents GetNetMode from changing output for the remainder of the frame)
		SetClientTravel( InWorld, TEXT("?closed"), TRAVEL_Absolute );
	}
	else if (NetDriver)
	{
		// Shut down any existing game connections
		if (InWorld)
		{
			// Call this to remove the NetDriver from the world context's ActiveNetDriver list
			DestroyNamedNetDriver(InWorld, NetDriver->NetDriverName);
		}
		else
		{
			NetDriver->Shutdown();
			NetDriver->LowLevelDestroy();

			// In this case, the world is null and something went wrong, so we should travel back to the default world so that we
			// can get back to a good state.
			for (FWorldContext& PotentialWorldContext : WorldList)
			{
				if (PotentialWorldContext.WorldType == EWorldType::Game)
				{
					FURL DefaultURL;
					DefaultURL.LoadURLConfig(TEXT("DefaultPlayer"), GGameIni);
					const UGameMapsSettings* GameMapsSettings = GetDefault<UGameMapsSettings>();
					if (GameMapsSettings)
					{
						PotentialWorldContext.TravelURL = FURL(&DefaultURL, *(GameMapsSettings->GetGameDefaultMap() + GameMapsSettings->LocalMapOptions), TRAVEL_Partial).ToString();
						PotentialWorldContext.TravelType = TRAVEL_Partial;
					}
				}
			}
		}
	}
}

마지막 부분을 보면 주석으로 "무언가 잘못 되었을 때 이전 World로 되돌린다"는 설명이 되어있다.

그리고 이 때 TravelType은 TRAVEL_Partial로 지정되어 있다.

이를 통해 TRAVEL_Partial은 일부 정보만 리셋하는 것으로 예상해볼 수 있다.

실제 도큐먼트에서도 carry name, reset server로 기재가 되어있기도 하구 말이다.

 

LastURL

가장 마지막에 접근했던 Level의 URL이다.

 

LastRemoteURL

가장 마지막에 connect 했던 Level의 URL이다.

일반적으로는 사용하지 않지만 reconnect를 할 때에는 이 값을 저장한다.

 

PendingNetGame

이 변수에 대해서는 이렇다 할 정보를 찾지 못했다.

우선 UPendingNetGame 타입이라 별도 클래스이므로 나중에 추가로 확인해보도록 하겠다.

 

PackagesToFullyLoad

 

'UE5 > Level' 카테고리의 다른 글

[Level] UGameplayStatics::OpenLevel 분석 - 1  (0) 2022.07.02

UGamePlayStatics::OpenLevel은 C++에서 레벨을 변경하고자 할 때 사용하는 함수이다.

/**
* Travel to another level
*
* @param	LevelName			the level to open
* @param	bAbsolute			if true options are reset, if false options are carried over from current level
* @param	Options				a string of options to use for the travel URL
*/
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", AdvancedDisplay = "2", DisplayName = "Open Level (by Name)"), Category="Game")
static void OpenLevel(const UObject* WorldContextObject, FName LevelName, bool bAbsolute = true, FString Options = FString(TEXT("")));

 

함수 설명을 하기 앞서 Specifier를 살펴보자.

관련 사항은 아래 링크에 모든 것이 정리되어 있지만 이 글을 보는 사람들이 바로 알아볼 수 있게 따로 언급을 하겠다.

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/GameplayArchitecture/Functions/Specifiers/

 

Function Specifiers

Keywords used when declaring UFunctions to specify how the function behaves with various aspects of the Engine and Editor.

docs.unrealengine.com

BlueprintCallable: 이 함수를 Blueprint에서 호출할 수 있도록 한다.

Meta: Specifier 중에서 특정 커맨드가 아니라 파라미터 입력을 받는 것들을 지칭한다.

WorldContext: 해당 함수 작업이 수행 될 World를 지칭합니다. BlueprintCallable 옵션이 있을 때 사용 됩니다.

AdvancedDisplay: Blueprint에서 몇번째 옵션까지 노출시킬지를 나타냅니다. AdvancedDisplay가 2라면 함수 파라미터 중 2개까지 기본으로 노출되고, 나머지는 확장을 해야만 노출이 됩니다.

DisplayName: Blueprint Node에서 따로 표기가 될 이름입니다.

Category: Blueprint 창에서 분류될 카테고리입니다.

 

사실 이 부분은 함수 기능 그 자체를 이해하는 것과는 직접적인 연관은 없다고 생각합니다.

다만 이러한 함수를 만들거나, 목적을 이해하는데 있어 범용적으로 요구되는 기본지식이라고 생각합니다.

나중에 기회가 되면 따로 정리를 해보겠습니다.

 

함수 파라미터는 WorldContextObject를 제외하고는 주석으로 잘 설명이 되어 있습니다.

하지만 직관적으로 어떤건지 파악이 되는 것은 bAbsolute 하나 뿐입니다.

우리는 이 파라미터가 "현재 레벨의 옵션을 그대로 유지할지 여부" 정부라는 것만 기억을 하고 구현부로 넘어가겠습니다.

 

void UGameplayStatics::OpenLevel(const UObject* WorldContextObject, FName LevelName, bool bAbsolute, FString Options)
{
	UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
	if (World == nullptr)
	{
		return;
	}

	const ETravelType TravelType = (bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative);
	FWorldContext &WorldContext = GEngine->GetWorldContextFromWorldChecked(World);
	FString Cmd = LevelName.ToString();
	if (Options.Len() > 0)
	{
		Cmd += FString(TEXT("?")) + Options;
	}
	FURL TestURL(&WorldContext.LastURL, *Cmd, TravelType);
	if (TestURL.IsLocalInternal())
	{
		// make sure the file exists if we are opening a local file
		if (!GEngine->MakeSureMapNameIsValid(TestURL.Map))
		{
			UE_LOG(LogLevel, Warning, TEXT("WARNING: The map '%s' does not exist."), *TestURL.Map);
		}
	}

	GEngine->SetClientTravel( World, *Cmd, TravelType );
}

 

 

첫번째 함수 호출부분을 보자.

GEngine::GetWorldFromContextObject를 통해 World를 불러온다.

해당 함수의 선언부는 아래와 같은데, 이는 파라미터로 입력된 UObject가 소속된 UWorld 포인터를 반환하는 함수이다.

/** 
* Obtain a world object pointer from an object with has a world context.
*
* @param Object		Object whose owning world we require.
* @param ErrorMode		Controls what happens if the Object cannot be found
* @return				The world to which the object belongs or nullptr if it cannot be found.
*/
UWorld* GetWorldFromContextObject(const UObject* Object, EGetWorldErrorMode ErrorMode) const;

이 함수에 대해서는 UObject와 GEngine 모두를 분석해야 정확히 설명이 가능할 것 같아 기능만 짚겠다.

 

이 다음에는 bAbsolute를 통해 ETravelType을 지정한다.

이 ETravelType에는 Travel_Absolute, Travel_Relative 외에 Travel_Partial도 존재한다.

이에 대한 명확한 의미를 찾지는 못했지만 대략적인 사용 의의를 찾을 수는 있었습니다.

출처:&nbsp;https://blog.csdn.net/u012999985/article/details/78484511

더 명확한 옵션은 지금 당장 확인하고 찾기에는 힘들 것 같아 타입을 지정한다는 점만 짚고 넘어가도록 하겠다.

 

그 다음으로 봐야 할 함수는 UEngine::GetWorldContextFromHandleChecked이다.

해당 함수는 파라미터로 전달된 UWorld 포인터를 들고 있는 FWorldContext를 반환한다.

FWorldContext& UEngine::GetWorldContextFromWorldChecked(const UWorld *InWorld)
{
	if (FWorldContext* WorldContext = GetWorldContextFromWorld(InWorld))
	{
		return *WorldContext;
	}
	return HandleInvalidWorldContext();
}

 

코드를 보면 알겠지만, 아니 어쩌면 이름으로도 알았겠지만 이 함수는 GetWorldContextFromWorld의 반호나 값을 Valid 체크까지 진행 한 함수다.

즉, 실제 동작은 다음과 같다.

FWorldContext* UEngine::GetWorldContextFromWorld(const UWorld* InWorld)
{
	for (FWorldContext& WorldContext : WorldList)
	{
		if (WorldContext.World() == InWorld)
		{
			return &WorldContext;
		}
	}
	return nullptr;
}

이제 우리가 우회해야 할 또 한가지 사항이 생긴다.

바로 FWorldContext이다. 이는 해당 Struct에 적혀있는 설명 주석을 먼저 기재하고 작성하겠다.

/** FWorldContext
 *	A context for dealing with UWorlds at the engine level. As the engine brings up and destroys world, we need a way to keep straight
 *	what world belongs to what.
 *
 *	WorldContexts can be thought of as a track. By default we have 1 track that we load and unload levels on. Adding a second context is adding
 *	a second track; another track of progression for worlds to live on. 
 *
 *	For the GameEngine, there will be one WorldContext until we decide to support multiple simultaneous worlds.
 *	For the EditorEngine, there may be one WorldContext for the EditorWorld and one for the PIE World.
 *
 *	FWorldContext provides both a way to manage 'the current PIE UWorld*' as well as state that goes along with connecting/travelling to 
 *  new worlds.
 *
 *	FWorldContext should remain internal to the UEngine classes. Outside code should not keep pointers or try to manage FWorldContexts directly.
 *	Outside code can still deal with UWorld*, and pass UWorld*s into Engine level functions. The Engine code can look up the relevant context 
 *	for a given UWorld*.
 *
 *  For convenience, FWorldContext can maintain outside pointers to UWorld*s. For example, PIE can tie UWorld* UEditorEngine::PlayWorld to the PIE
 *	world context. If the PIE UWorld changes, the UEditorEngine::PlayWorld pointer will be automatically updated. This is done with AddRef() and
 *  SetCurrentWorld().
 *
 */

 

간단히 요약을 하면, UWorld 포인터와 이를 들고 있는 Level에 대한 정보를 들고 있다.

그렇기에 우리는 UWorld 포인터를 통해 FWorldContext 포인터를 검색을 할 수 있는 것이다.

 

이에 대한 설명을 하기 앞서 한가지 짚어야 할 것은 Cmd라는 FString 생성 과정이다.

Cmd는 LevelName과 Option을 ? 좌우로 붙여서 생성된다.

이 포맷은 무엇이고 Option은 무엇인가?

그에 대한 설명은 아래 문서에 자세히 기재되어 있다.

https://docs.unrealengine.com/4.27/en-US/ProductionPipelines/CommandLineArguments/

 

Command-Line Arguments

Collection of arguments that can be passed to the engine's executable to configure options controlling how it runs.

docs.unrealengine.com

여기서도 한가지 의문이 생긴다.

위 문서에서 나온 예시는 레벨을 경로로 지정하는데 우리가 입력한 LevelName은 파일명만 지칭하고 있다.

그럼 이 둘은 전혀 다른 것인가?

 

결론부터 말하자면 두 옵션은 완전히 동일하게 작성된다.

정확히는 포맷이 정확히 동일하다.

다만 OpenLevel의 내부에서는 이를 다른 형태로 사용하고 있을 뿐이다.

때문에 우리가 봐야할 것은 그다음 부분인 FURL 구조체와 FWorldContext::LastURL() 함수이다.

 

FURL은 Unreal 내부에서 경로를 지칭할 때 사용하는 구조체이다.

이는 URL이라는 String에 여러 편의기능을 덧붙인 Wrapping 구조체로 봐도 무방하다.

FWorldContext::LastURL은 FWorldContext가 가장 마지막에 접속한 FURL을 반환한다.

 

정리를 하면 TestURL은 LastURL와 파일명?옵션으로 이루어진 Cmd, 그리고 TravelType으로 재생성이 된다는 점이다.

이에 대한 중요한 코드 부분은 다음과 같다.

USTRUCT()
struct ENGINE_API FURL
{
	GENERATED_USTRUCT_BODY()

	// Protocol, i.e. "unreal" or "http".
	UPROPERTY()
	FString Protocol;

	// Optional hostname, i.e. "204.157.115.40" or "unreal.epicgames.com", blank if local.
	UPROPERTY()
	FString Host;

	// Optional host port.
	UPROPERTY()
	int32 Port;

	UPROPERTY()
	int32 Valid;

	// Map name, i.e. "SkyCity", default is "Entry".
	UPROPERTY()
	FString Map;

	// Optional place to download Map if client does not possess it
	UPROPERTY()
	FString RedirectURL;

	// Options.
	UPROPERTY()
	TArray<FString> Op;

	// Portal to enter through, default is "".
	UPROPERTY()
	FString Portal;
    ....
}

FURL의 선언부다.

눈여겨 볼 정보는 Protocol/Host/Port와 Map이다.

Protocol은 해당 FURL이 파일 경로인지 서버 경로인지를 지칭한다.

만약 서버 경로라면 Host와 Port 값이 Valid 한 것이고, 그렇지 않다면 이 값들은 빈 값이 들어가 있다는 점이다.

그럼 다음을 보자.

 

FURL::FURL( FURL* Base, const TCHAR* TextURL, ETravelType Type )
:	Protocol	( UrlConfig.DefaultProtocol )
,	Host		( UrlConfig.DefaultHost )
,	Port		( UrlConfig.DefaultPort )
,	Valid		( 1 )
,	Map			( UGameMapsSettings::GetGameDefaultMap() )
,	Op			()
,	Portal		( UrlConfig.DefaultPortal )
{
    ....
    
    // Parse optional map
	if (Valid == 1 && URLStr.Len() > 0)
	{
		// Map.
		if (URLStr[0] != '/')
		{
			// find full pathname from short map name
			FString MapFullName;
			FText MapNameError;
			bool bFoundMap = false;

			if (FPaths::FileExists(URLStr) && FPackageName::TryConvertFilenameToLongPackageName(URLStr, MapFullName))
			{
				Map = MapFullName;
				bFoundMap = true;
			}
			else if (!FPackageName::DoesPackageNameContainInvalidCharacters(URLStr, &MapNameError))
			{
				// First try to use the asset registry if it is available and finished scanning
				if (FModuleManager::Get().IsModuleLoaded("AssetRegistry"))
				{
					IAssetRegistry& AssetRegistry = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();

					if (!AssetRegistry.IsLoadingAssets())
					{
						TArray<FAssetData> MapList;
						if (AssetRegistry.GetAssetsByClass(UWorld::StaticClass()->GetFName(), /*out*/ MapList))
						{
							FName TargetTestName(*URLStr);
							for (const FAssetData& MapAsset : MapList)
							{
								if (MapAsset.AssetName == TargetTestName)
								{
									Map = MapAsset.PackageName.ToString();
									bFoundMap = true;
									break;
								}
							}
						}
					}
				}

				if (!bFoundMap)
				{
					// Fall back to a slow AssetRegistry scan for the package
					IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
					FName ExistingPackageName = AssetRegistry.GetFirstPackageByName(URLStr);
					if (!ExistingPackageName.IsNone())
					{
						ExistingPackageName.ToString(Map);
						bFoundMap = true;
					}
				}
			}

			if (!bFoundMap)
			{
				// can't find file, invalidate and bail
				UE_CLOG(MapNameError.ToString().Len() > 0, LogLongPackageNames, Warning, TEXT("URL: %s: %s"), *URLStr, *MapNameError.ToString());
				*this = FURL();
				Valid = 0;
			}
		}
		else
		{
			// already a full pathname
			Map = URLStr;
		}
        .......
	}
}

사실 구현부는 매우매우 길고 많은 경우를 계산하고 있다.

하지만 이를 하나하나 설명하기 힘드니 명확히 우리가 알고자 하는 부분만 발췌했다.

결국 이 Map은 파일명으로 애셋을 검색해 그 경로를 받아와 저장된 값이다.

즉 우리는 이 긴 코드를 통해 FURL의 생성자 안에서 파일명을 넣으면 경로를 반환한다는 것을 알아냈다.

 

이 다음은 비교적 간단하다.

FURL::IsLocalInternal과 UEngine::MakeSureMapNameIsValid를 통해 현재 URL이 파일 경로이고, 실제 존재하는지 확인을 한다.

이 조건을 통과하면 UEngine::SetClientTravel이라는 함수를 호출하며 마무리가 된다.

void UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )
{
	FWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);

	// set TravelURL.  Will be processed safely on the next tick in UGameEngine::Tick().
	Context.TravelURL    = NextURL;
	Context.TravelType   = InTravelType;

	// Prevent crashing the game by attempting to connect to own listen server
	if ( Context.LastURL.HasOption(TEXT("Listen")) )
	{
		Context.LastURL.RemoveOption(TEXT("Listen"));
	}
}

 

함수가 간단해서 안심을 했다면 오산이다.

자세히 보면 FWorldContext를 받아와 TravelURL와 TravelType을 변환하고, 이전 레벨의 Listen 옵션을 제거를 해주는데 그 다음 동작은 UGameEngine::Tick에서 진행 된다고 되어 있다.

그럼 우리는 UGameEngine::Tick을 봐야 한다.

void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
	CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EngineTickMisc);
	SCOPE_TIME_GUARD(TEXT("UGameEngine::Tick"));
	SCOPE_CYCLE_COUNTER(STAT_GameEngineTick);
	NETWORK_PROFILER(GNetworkProfiler.TrackFrameBegin());
    
	.......

	// -----------------------------------------------------
	// Begin ticking worlds
	// -----------------------------------------------------

	bool bIsAnyNonPreviewWorldUnpaused = false;

	FName OriginalGWorldContext = NAME_None;
	for (int32 i=0; i < WorldList.Num(); ++i)
	{
		if (WorldList[i].World() == GWorld)
		{
			OriginalGWorldContext = WorldList[i].ContextHandle;
			break;
		}
	}

	for (int32 WorldIdx = 0; WorldIdx < WorldList.Num(); ++WorldIdx)
	{
		FWorldContext &Context = WorldList[WorldIdx];
		if (Context.World() == NULL || !Context.World()->ShouldTick())
		{
			continue;
		}

		GWorld = Context.World();

		// Tick all travel and Pending NetGames (Seamless, server, client)
		{
			QUICK_SCOPE_CYCLE_COUNTER(STAT_UGameEngine_Tick_TickWorldTravel);
			TickWorldTravel(Context, DeltaSeconds);
		}

		if (!bIdleMode)
		{
			SCOPE_TIME_GUARD(TEXT("UGameEngine::Tick - WorldTick"));

			// Tick the world.
			Context.World()->Tick( LEVELTICK_All, DeltaSeconds );
		}

		if (!IsRunningDedicatedServer() && !IsRunningCommandlet())
		{
			QUICK_SCOPE_CYCLE_COUNTER(STAT_UGameEngine_Tick_CheckCaptures);
			// Only update reflection captures in game once all 'always loaded' levels have been loaded
			// This won't work with actual level streaming though
			if (Context.World()->AreAlwaysLoadedLevelsLoaded())
			{
				// Update sky light first because it's considered direct lighting, sky diffuse will be visible in reflection capture indirect specular
				USkyLightComponent::UpdateSkyCaptureContents(Context.World());
				UReflectionCaptureComponent::UpdateReflectionCaptureContents(Context.World());
			}
		}
        .....
}

World를 Tick 하는 부분이다.

여기서 다음으로 넘어가야 할 부분은 TickWorldTravel이다.

왜냐하면 이 뒤에 Context.World()->Tick이 있는데 이는 Level의 Tick이고, GWorld에서 Context.World()가 저장되기 때문에 이 둘 사이에 초기화가 되기 때문이다.

void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
	// Handle seamless traveling
	if (Context.SeamlessTravelHandler.IsInTransition())
	{
		// Note: SeamlessTravelHandler.Tick may automatically update Context.World and GWorld internally
		Context.SeamlessTravelHandler.Tick();
	}

	// Handle server traveling.
	if (Context.World() == nullptr)
	{
		UE_LOG(LogLoad, Error, TEXT("UEngine::TickWorldTravel has no world after ticking seamless travel handler."));
		BrowseToDefaultMap(Context);
		BroadcastTravelFailure(Context.World(), ETravelFailure::ServerTravelFailure, TEXT("UEngine::TickWorldTravel has no world after ticking seamless travel handler."));
		return;
	}

	if( !Context.World()->NextURL.IsEmpty() )
	{
		Context.World()->NextSwitchCountdown -= DeltaSeconds;
		if( Context.World()->NextSwitchCountdown <= 0.f )
		{
			UE_LOG(LogEngine, Log,  TEXT("Server switch level: %s"), *Context.World()->NextURL );
			if (Context.World()->GetAuthGameMode() != NULL)
			{
				Context.World()->GetAuthGameMode()->StartToLeaveMap();
			}
			FString Error;
			FString NextURL = Context.World()->NextURL;
			EBrowseReturnVal::Type Ret = Browse( Context, FURL(&Context.LastURL,*NextURL,(ETravelType)Context.World()->NextTravelType), Error );
			if (Ret != EBrowseReturnVal::Success )
			{
				UE_LOG(LogLoad, Warning, TEXT("UEngine::TickWorldTravel failed to Handle server travel to URL: %s. Error: %s"), *NextURL, *Error);
				ensureMsgf(Ret != EBrowseReturnVal::Pending, TEXT("Server travel should never create a pending net game"));

				// Failed to load a new map
				if (Context.World() != nullptr)
				{
					// If we didn't change worlds, clear out NextURL so we don't do this again next frame.
					Context.World()->NextURL = TEXT("");
				}
				else
				{
					// Our old world got stomped out. Load the default map
					BrowseToDefaultMap(Context);
				}

				// Let people know that we failed to server travel
				BroadcastTravelFailure(Context.World(), ETravelFailure::ServerTravelFailure, Error);
			}
			return;
		}
	}

	// Handle client traveling.
	if( !Context.TravelURL.IsEmpty() )
	{	
		AGameModeBase* const GameMode = Context.World()->GetAuthGameMode();
		if (GameMode)
		{
			GameMode->StartToLeaveMap();
		}

		FString Error, TravelURLCopy = Context.TravelURL;
		if (Browse( Context, FURL(&Context.LastURL,*TravelURLCopy,(ETravelType)Context.TravelType), Error ) == EBrowseReturnVal::Failure)
		{
			// If the failure resulted in no world being loaded (we unloaded our last world, then failed to load the new one)
			// then load the default map to avoid getting in a situation where we have no valid UWorld.
			if (Context.World() == NULL)
			{
				BrowseToDefaultMap(Context);
			}

			// Let people know that we failed to client travel
			BroadcastTravelFailure(Context.World(), ETravelFailure::ClientTravelFailure, Error);
		}
		check(Context.World() != NULL);
		return;
	}

서버일 경우와 클라이언트인 경우로 코드가 크게 나뉜다.

여기서는 좀 더 함축해서 바로 클라이언트 부분을 살펴 봐 그 다음 우리가 봐야할 것이 Browse 함수라는 것을 알리겠다.

서버 쪽으로 탐색을 해도 결국 Browse로 귀결이 되니 이는 따로 확인을 해보면 된다.

EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
{
	Error = TEXT("");
	WorldContext.TravelURL = TEXT("");

	if (WorldContext.World() && WorldContext.World()->GetNetDriver())
	{
		const TCHAR* InTickErrorString = TEXT("Attempting to call UEngine::Browse and destroy the net driver while the net driver is ticking. Instead try using UWorld::ServerTravel or APlayerController::ClientTravel.");
		if (!ensureMsgf(!WorldContext.World()->GetNetDriver()->IsInTick(), TEXT("%s"), InTickErrorString))
		{
			Error = InTickErrorString;
			return EBrowseReturnVal::Failure;
		}
	}

	// Convert .unreal link files.
	const TCHAR* LinkStr = TEXT(".unreal");//!!
	if( FCString::Strstr(*URL.Map,LinkStr)-*URL.Map==FCString::Strlen(*URL.Map)-FCString::Strlen(LinkStr) )
	{
		UE_LOG(LogNet, Log,  TEXT("Link: %s"), *URL.Map );
		FString NewUrlString;
		if( GConfig->GetString( TEXT("Link")/*!!*/, TEXT("Server"), NewUrlString, *URL.Map ) )
		{
			// Go to link.
			URL = FURL( NULL, *NewUrlString, TRAVEL_Absolute );//!!
		}
		else
		{
			// Invalid link.
			Error = FText::Format( NSLOCTEXT("Engine", "InvalidLink", "Invalid Link: {0}"), FText::FromString( URL.Map ) ).ToString();
			return EBrowseReturnVal::Failure;
		}
	}

	// Crack the URL.
	UE_LOG(LogNet, Log, TEXT("Browse: %s"), *URL.ToString() );

	// Handle it.
	if( !URL.Valid )
	{
		// Unknown URL.
		Error = FText::Format( NSLOCTEXT("Engine", "InvalidUrl", "Invalid URL: {0}"), FText::FromString( URL.ToString() ) ).ToString();
		BroadcastTravelFailure(WorldContext.World(), ETravelFailure::InvalidURL, Error);
		return EBrowseReturnVal::Failure;
	}
	else if (URL.HasOption(TEXT("failed")) || URL.HasOption(TEXT("closed")))
	{
		if (WorldContext.PendingNetGame)
		{
			CancelPending(WorldContext);
		}
		if (WorldContext.World() != NULL)
		{
			ResetLoaders( WorldContext.World()->GetOuter() );
		}

		if (WorldContext.WorldType == EWorldType::GameRPC)
		{
			UE_LOG(LogNet, Log, TEXT("RPC connection failed; retrying..."));

			// Clean up all current net-drivers before we create a new one
			// Need to copy the array as DestroyNamedNetDriver_Local mutates it
			{
				TArray<FNamedNetDriver> ActiveNetDrivers = WorldContext.ActiveNetDrivers;
				for (const FNamedNetDriver& ActiveNetDriver : ActiveNetDrivers)
				{
					if (ActiveNetDriver.NetDriver)
					{
						DestroyNamedNetDriver_Local(WorldContext, ActiveNetDriver.NetDriver->NetDriverName);
					}
				}
				CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
			}

			{
				LLM_SCOPE(ELLMTag::Networking);

				// Just reload the RPC world (as we have no real map to load)
				WorldContext.PendingNetGame = NewObject<UPendingNetGame>();
				WorldContext.PendingNetGame->Initialize(WorldContext.LastURL);
				WorldContext.PendingNetGame->InitNetDriver();
			}

			if (!WorldContext.PendingNetGame->NetDriver)
			{
				// UPendingNetGame will set the appropriate error code and connection lost type, so
				// we just have to propagate that message to the game.
				BroadcastTravelFailure(WorldContext.World(), ETravelFailure::PendingNetGameCreateFailure, WorldContext.PendingNetGame->ConnectionError);
				WorldContext.PendingNetGame = NULL;
				return EBrowseReturnVal::Failure;
			}

			return EBrowseReturnVal::Pending;
		}

		// Browsing after a failure, load default map
		UE_LOG(LogNet, Log, TEXT("Connection failed; returning to Entry"));
		
		const UGameMapsSettings* GameMapsSettings = GetDefault<UGameMapsSettings>();
		const FURL DefaultURL = FURL(&URL, *(GameMapsSettings->GetGameDefaultMap() + GameMapsSettings->LocalMapOptions), TRAVEL_Partial);

		if (!LoadMap(WorldContext, DefaultURL, NULL, Error))
		{
			HandleBrowseToDefaultMapFailure(WorldContext, DefaultURL.ToString(), Error);
			return EBrowseReturnVal::Failure;
		}

		CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS );

		// now remove "failed" and "closed" options from LastURL so it doesn't get copied on to future URLs
		WorldContext.LastURL.RemoveOption(TEXT("failed"));
		WorldContext.LastURL.RemoveOption(TEXT("closed"));
		return EBrowseReturnVal::Success;
	}
	else if( URL.HasOption(TEXT("restart")) )
	{
		// Handle restarting.
		URL = WorldContext.LastURL;
	}

	// Handle normal URL's.
	if (GDisallowNetworkTravel && URL.HasOption(TEXT("listen")))
	{
		Error = NSLOCTEXT("Engine", "UsedCheatCommands", "Console commands were used which are disallowed in netplay.  You must restart the game to create a match.").ToString();
		BroadcastTravelFailure(WorldContext.World(), ETravelFailure::CheatCommands, Error);
		return EBrowseReturnVal::Failure;
	}
	if( URL.IsLocalInternal() )
	{
		// Local map file.
		return LoadMap( WorldContext, URL, NULL, Error ) ? EBrowseReturnVal::Success : EBrowseReturnVal::Failure;
	}
	else if( URL.IsInternal() && GIsClient )
	{
		// Network URL.
		if( WorldContext.PendingNetGame )
		{
			CancelPending(WorldContext);
		}

		// Clean up the netdriver/socket so that the pending level succeeds
		if (WorldContext.World() && ShouldShutdownWorldNetDriver())
		{
			ShutdownWorldNetDriver(WorldContext.World());
		}

		WorldContext.PendingNetGame = NewObject<UPendingNetGame>();
		WorldContext.PendingNetGame->Initialize(URL); //-V595
		WorldContext.PendingNetGame->InitNetDriver(); //-V595

		if( !WorldContext.PendingNetGame )
		{
			// If the inital packet sent in InitNetDriver results in a socket error, HandleDisconnect() and CancelPending() may be called, which will null the PendingNetGame.
			Error = NSLOCTEXT("Engine", "PendingNetGameInitFailure", "Error initializing the network driver.").ToString();
			BroadcastTravelFailure(WorldContext.World(), ETravelFailure::PendingNetGameCreateFailure, Error);
			return EBrowseReturnVal::Failure;
		}

		if( !WorldContext.PendingNetGame->NetDriver )
		{
			// UPendingNetGame will set the appropriate error code and connection lost type, so
			// we just have to propagate that message to the game.
			BroadcastTravelFailure(WorldContext.World(), ETravelFailure::PendingNetGameCreateFailure, WorldContext.PendingNetGame->ConnectionError);
			WorldContext.PendingNetGame = NULL;
			return EBrowseReturnVal::Failure;
		}
		return EBrowseReturnVal::Pending;
	}
	else if( URL.IsInternal() )
	{
		// Invalid.
		Error = NSLOCTEXT("Engine", "ServerOpen", "Servers can't open network URLs").ToString();
		return EBrowseReturnVal::Failure;
	}
	{
		// External URL - disabled by default.
		// Client->Viewports(0)->Exec(TEXT("ENDFULLSCREEN"));
		// FPlatformProcess::LaunchURL( *URL.ToString(), TEXT(""), &Error );
		return EBrowseReturnVal::Failure;
	}
}

링크에서 .unreal 확장자를 제거하고 추적을 해서 함수 실행을 하고 있다.

우리가 원하는 Level을 여는 조건은 탐색을 하면 LoadMap이라는 것을 알 수 있다.

bool UEngine::LoadMap( FWorldContext& WorldContext, FURL URL, class UPendingNetGame* Pending, FString& Error )
{
	TRACE_LOADTIME_REQUEST_GROUP_SCOPE(TEXT("LoadMap - %s"), *URL.Map);
	STAT_ADD_CUSTOMMESSAGE_NAME( STAT_NamedMarker, *(FString( TEXT( "LoadMap - " ) + URL.Map )) );
	TRACE_BOOKMARK(TEXT("LoadMap - %s"), *URL.Map);

	DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UEngine::LoadMap"), STAT_LoadMap, STATGROUP_LoadTime);

	LLM_SCOPE(ELLMTag::LoadMapMisc);

	FDisableHitchDetectorScope SuspendHitchDetector;

	NETWORK_PROFILER(GNetworkProfiler.TrackSessionChange(true,URL));
	MALLOC_PROFILER( FMallocProfiler::SnapshotMemoryLoadMapStart( URL.Map ) );
	FMoviePlayerProxyBlock MoviePlayerBlock;
	Error = TEXT("");

	FLoadTimeTracker::Get().ResetRawLoadTimes();

	// make sure level streaming isn't frozen
	if (WorldContext.World())
	{
		WorldContext.World()->bIsLevelStreamingFrozen = false;
	}

	// send a callback message
	FCoreUObjectDelegates::PreLoadMapWithContext.Broadcast(WorldContext, URL.Map);
	FCoreUObjectDelegates::PreLoadMap.Broadcast(URL.Map);
	FMoviePlayerProxy::BlockingTick();

	// make sure there is a matching PostLoadMap() no matter how we exit
	struct FPostLoadMapCaller
	{
		FPostLoadMapCaller()
			: bCalled(false)
		{}

		~FPostLoadMapCaller()
		{
			if (!bCalled)
			{
				FCoreUObjectDelegates::PostLoadMapWithWorld.Broadcast(nullptr);
			}
		}

		void Broadcast(UWorld* World)
		{
			if (ensure(!bCalled))
			{
				bCalled = true;
				FCoreUObjectDelegates::PostLoadMapWithWorld.Broadcast(World);
			}
		}

	private:
		bool bCalled;

	} PostLoadMapCaller;

	// Cancel any pending texture streaming requests.  This avoids a significant delay on consoles 
	// when loading a map and there are a lot of outstanding texture streaming requests from the previous map.
	UTexture2D::CancelPendingTextureStreaming();

	// play a load map movie if specified in ini
	bStartedLoadMapMovie = false;

	// clean up any per-map loaded packages for the map we are leaving
	if (WorldContext.World() && WorldContext.World()->PersistentLevel)
	{
		CleanupPackagesToFullyLoad(WorldContext, FULLYLOAD_Map, WorldContext.World()->PersistentLevel->GetOutermost()->GetName());
	}

	// cleanup the existing per-game pacakges
	// @todo: It should be possible to not unload/load packages if we are going from/to the same GameMode.
	//        would have to save the game pathname here and pass it in to SetGameMode below
	CleanupPackagesToFullyLoad(WorldContext, FULLYLOAD_Game_PreLoadClass, TEXT(""));
	CleanupPackagesToFullyLoad(WorldContext, FULLYLOAD_Game_PostLoadClass, TEXT(""));
	CleanupPackagesToFullyLoad(WorldContext, FULLYLOAD_Mutator, TEXT(""));


	// Cancel any pending async map changes after flushing async loading. We flush async loading before canceling the map change
	// to avoid completion after cancellation to not leave references to the "to be changed to" level around. Async loading is
	// implicitly flushed again later on during garbage collection.
	FlushAsyncLoading();
	CancelPendingMapChange(WorldContext);
	WorldContext.SeamlessTravelHandler.CancelTravel();

	double	StartTime = FPlatformTime::Seconds();

	UE_LOG(LogLoad, Log,  TEXT("LoadMap: %s"), *URL.ToString() );
	GInitRunaway();

#if !UE_BUILD_SHIPPING
	const bool bOldWorldWasShowingCollisionForHiddenComponents = WorldContext.World() && WorldContext.World()->bCreateRenderStateForHiddenComponentsWithCollsion;
#endif

	// Unload the current world
	if( WorldContext.World() )
	{
		WorldContext.World()->BeginTearingDown();

		if(!URL.HasOption(TEXT("quiet")) )
		{
			TransitionType = ETransitionType::Loading;
			TransitionDescription = URL.Map;
			if (URL.HasOption(TEXT("Game=")))
			{
				TransitionGameMode = URL.GetOption(TEXT("Game="), TEXT(""));
			}
			else
			{
				TransitionGameMode = TEXT("");
			}

			// Display loading screen.		
			// Check if a loading movie is playing.  If so it is not safe to redraw the viewport due to potential race conditions with font rendering
			bool bIsLoadingMovieCurrentlyPlaying = FCoreDelegates::IsLoadingMovieCurrentlyPlaying.IsBound() ? FCoreDelegates::IsLoadingMovieCurrentlyPlaying.Execute() : false;
			if(!bIsLoadingMovieCurrentlyPlaying)
			{
				LoadMapRedrawViewports();
			}

			TransitionType = ETransitionType::None;
		}

		// Clean up networking
		ShutdownWorldNetDriver(WorldContext.World());

		// Make sure there are no pending visibility requests.
		WorldContext.World()->FlushLevelStreaming(EFlushLevelStreamingType::Visibility);

		// send a message that all levels are going away (NULL means every sublevel is being removed
		// without a call to RemoveFromWorld for each)
		//if (WorldContext.World()->GetNumLevels() > 1)
		{
			// TODO: Consider actually broadcasting for each level?
			FWorldDelegates::LevelRemovedFromWorld.Broadcast(nullptr, WorldContext.World());
		}

		// Disassociate the players from their PlayerControllers in this world.
		if (WorldContext.OwningGameInstance != nullptr)
		{
			for(auto It = WorldContext.OwningGameInstance->GetLocalPlayerIterator(); It; ++It)
			{
				ULocalPlayer *Player = *It;
				if(Player->PlayerController && Player->PlayerController->GetWorld() == WorldContext.World())
				{
					if(Player->PlayerController->GetPawn())
					{
						WorldContext.World()->DestroyActor(Player->PlayerController->GetPawn(), true);
					}
					WorldContext.World()->DestroyActor(Player->PlayerController, true);
					Player->PlayerController = nullptr;
				}
				// reset split join info so we'll send one after loading the new map if necessary
				Player->bSentSplitJoin = false;
				// When loading maps, clear out mids that are referenced as they may prevent the world from shutting down cleanly and the local player will not be cleaned up until later
				Player->CleanupViewState(); 
			}
		}

		for (FActorIterator ActorIt(WorldContext.World()); ActorIt; ++ActorIt)
		{
			ActorIt->RouteEndPlay(EEndPlayReason::LevelTransition);
		}

		// Do this after destroying pawns/playercontrollers, in case that spawns new things (e.g. dropped weapons)
		WorldContext.World()->CleanupWorld();

		if( GEngine )
		{
			// clear any "DISPLAY" properties referencing level objects
			if (GEngine->GameViewport != nullptr)
			{
				ClearDebugDisplayProperties();
			}

			GEngine->WorldDestroyed(WorldContext.World());
		}
		WorldContext.World()->RemoveFromRoot();

		// mark everything else contained in the world to be deleted
		for (auto LevelIt(WorldContext.World()->GetLevelIterator()); LevelIt; ++LevelIt)
		{
			const ULevel* Level = *LevelIt;
			if (Level)
			{
				CastChecked<UWorld>(Level->GetOuter())->MarkObjectsPendingKill();
			}
		}

		for (ULevelStreaming* LevelStreaming : WorldContext.World()->GetStreamingLevels())
		{
			// If an unloaded levelstreaming still has a loaded level we need to mark its objects to be deleted as well
			if (LevelStreaming->GetLoadedLevel() && (!LevelStreaming->ShouldBeLoaded() || !LevelStreaming->ShouldBeVisible()))
			{
				CastChecked<UWorld>(LevelStreaming->GetLoadedLevel()->GetOuter())->MarkObjectsPendingKill();
			}
		}

		// Stop all audio to remove references to current level.
		if (FAudioDevice* AudioDevice = WorldContext.World()->GetAudioDeviceRaw())
		{
			AudioDevice->Flush(WorldContext.World());
			AudioDevice->SetTransientMasterVolume(1.0f);
		}

		WorldContext.SetCurrentWorld(nullptr);
	}

	FMoviePlayerProxy::BlockingTick();
	// trim memory to clear up allocations from the previous level (also flushes rendering)
	if (GDelayTrimMemoryDuringMapLoadMode == 0)
	{
		TrimMemory();
	}

	// Cancels the Forced StreamType for textures using a timer.
	if (!IStreamingManager::HasShutdown())
	{
		IStreamingManager::Get().CancelForcedResources();
	}

	if (FPlatformProperties::RequiresCookedData())
	{
		appDefragmentTexturePool();
		appDumpTextureMemoryStats(TEXT(""));
	}

	// if we aren't trimming memory above, then the world won't be fully cleaned up at this point, so don't bother checking
	if (GDelayTrimMemoryDuringMapLoadMode == 0)
	{
		// Dump info
		VerifyLoadMapWorldCleanup();
	}

	FMoviePlayerProxy::BlockingTick();

	MALLOC_PROFILER( FMallocProfiler::SnapshotMemoryLoadMapMid( URL.Map ); )

	WorldContext.OwningGameInstance->PreloadContentForURL(URL);

	UPackage* WorldPackage = NULL;
	UWorld*	NewWorld = NULL;

	// If this world is a PIE instance, we need to check if we are traveling to another PIE instance's world.
	// If we are, we need to set the PIERemapPrefix so that we load a copy of that world, instead of loading the
	// PIE world directly.
	if (!WorldContext.PIEPrefix.IsEmpty())
	{
		for (const FWorldContext& WorldContextFromList : WorldList)
		{
			// We want to ignore our own PIE instance so that we don't unnecessarily set the PIERemapPrefix if we are not traveling to
			// a server.
			if (WorldContextFromList.World() != WorldContext.World())
			{
				if (!WorldContextFromList.PIEPrefix.IsEmpty() && URL.Map.Contains(WorldContextFromList.PIEPrefix))
				{
					FString SourceWorldPackage = UWorld::RemovePIEPrefix(URL.Map);

					// We are loading a new world for this context, so clear out PIE fixups that might be lingering.
					// (note we dont want to do this in DuplicateWorldForPIE, since that is also called on streaming worlds.
					GPlayInEditorID = WorldContext.PIEInstance;
					UpdatePlayInEditorWorldDebugString(&WorldContext);
					FLazyObjectPtr::ResetPIEFixups();

					NewWorld = UWorld::DuplicateWorldForPIE(SourceWorldPackage, nullptr);
					if (NewWorld == nullptr)
					{
						NewWorld = CreatePIEWorldByLoadingFromPackage(WorldContext, SourceWorldPackage, WorldPackage);
						if (NewWorld == nullptr)
						{
							Error = FString::Printf(TEXT("Failed to load package '%s' while in PIE"), *SourceWorldPackage);
							return false;
						}
					}
					else
					{
						WorldPackage = CastChecked<UPackage>(NewWorld->GetOuter());
					}

					NewWorld->StreamingLevelsPrefix = UWorld::BuildPIEPackagePrefix(WorldContext.PIEInstance);
					GIsPlayInEditorWorld = true;
				}
			}
		}
	}

	// Is this a minimal net RPC world?
	if (WorldContext.WorldType == EWorldType::GameRPC)
	{
		UGameInstance::CreateMinimalNetRPCWorld(*URL.Map, WorldPackage, NewWorld);
	}

	const FString URLTrueMapName = URL.Map;

	// Normal map loading
	if (NewWorld == NULL)
	{
		// Set the world type in the static map, so that UWorld::PostLoad can set the world type
		const FName URLMapFName = FName(*URL.Map);
		UWorld::WorldTypePreLoadMap.FindOrAdd( URLMapFName ) = WorldContext.WorldType;

		// See if the level is already in memory
		WorldPackage = FindPackage(nullptr, *URL.Map);

		bool bPackageAlreadyLoaded = (WorldPackage != nullptr);

		// If the level isn't already in memory, load level from disk
		if (WorldPackage == nullptr)
		{
			WorldPackage = LoadPackage(nullptr, *URL.Map, (WorldContext.WorldType == EWorldType::PIE ? LOAD_PackageForPIE : LOAD_None));
		}

		// Clean up the world type list now that PostLoad has occurred
		UWorld::WorldTypePreLoadMap.Remove( URLMapFName );

		if (WorldPackage == nullptr)
		{
			// it is now the responsibility of the caller to deal with a NULL return value and alert the user if necessary
			Error = FString::Printf(TEXT("Failed to load package '%s'"), *URL.Map);
			return false;
		}

		// Find the newly loaded world.
		NewWorld = UWorld::FindWorldInPackage(WorldPackage);

		// If the world was not found, it could be a redirector to a world. If so, follow it to the destination world.
		if ( !NewWorld )
		{
			NewWorld = UWorld::FollowWorldRedirectorInPackage(WorldPackage);
			if ( NewWorld )
			{
				// Treat this as an already loaded package because we were loaded by the redirector
				bPackageAlreadyLoaded = true;
				WorldPackage = NewWorld->GetOutermost();
			}
		}

		// This can still be null if the package name is ambiguous, for example if there exists a umap and uasset with the same
		// name.
		if (NewWorld == nullptr)
		{
			// it is now the responsibility of the caller to deal with a NULL return value and alert the user if necessary
			Error = FString::Printf(TEXT("Failed to load package '%s'"), *URL.Map);
			return false;
		}


		NewWorld->PersistentLevel->HandleLegacyMapBuildData();

		FScopeCycleCounterUObject MapScope(WorldPackage);

		if (WorldContext.WorldType == EWorldType::PIE)
		{
			// If we are a PIE world and the world we just found is already initialized, then we're probably reloading the editor world and we
			// need to create a PIE world by duplication instead
			if (bPackageAlreadyLoaded || NewWorld->WorldType == EWorldType::Editor)
			{
				if (WorldContext.PIEInstance == -1)
				{
					// Assume if we get here, that it's safe to just give a PIE instance so that we can duplicate the world 
					//	If we won't duplicate the world, we'll refer to the existing world (most likely the editor version, and it can be modified under our feet, which is bad)
					// So far, the only known way to get here is when we use the console "open" command while in a client PIE instance connected to non PIE server 
					// (i.e. multi process PIE where client is in current editor process, and dedicated server was launched as separate process)
					WorldContext.PIEInstance = 0;
				}

				NewWorld = CreatePIEWorldByDuplication(WorldContext, NewWorld, URL.Map);
				// CreatePIEWorldByDuplication clears GIsPlayInEditorWorld so set it again
				GIsPlayInEditorWorld = true;
			}
			// Otherwise we are probably loading new map while in PIE, so we need to rename world package and all streaming levels
			else if (WorldContext.PIEInstance != -1 && ((Pending == nullptr) || (Pending->GetDemoNetDriver() != nullptr)))
			{
				NewWorld->RenameToPIEWorld(WorldContext.PIEInstance);
			}
			ResetPIEAudioSetting(NewWorld);

#if WITH_EDITOR
			// PIE worlds should use the same feature level as the editor
			if (WorldContext.PIEWorldFeatureLevel != ERHIFeatureLevel::Num && NewWorld->FeatureLevel != WorldContext.PIEWorldFeatureLevel)
			{
				NewWorld->ChangeFeatureLevel(WorldContext.PIEWorldFeatureLevel);
			}
#endif
		}
		else if (WorldContext.WorldType == EWorldType::Game)
		{
			// If we are a game world and the world we just found is already initialized, then we're probably trying to load
			// an independent fresh copy of the world into a different context. Create a package with a prefixed name
			// and load the world from disk to keep the instances independent. If this is the case, assume the creator
			// of the FWorldContext was aware and set WorldContext.PIEInstance to a valid value.
			if (NewWorld->bIsWorldInitialized && WorldContext.PIEInstance != -1)
			{
				NewWorld = CreatePIEWorldByLoadingFromPackage(WorldContext, URL.Map, WorldPackage);

				if (NewWorld == nullptr)
				{
					Error = FString::Printf(TEXT("Failed to load package '%s' into a new game world."), *URL.Map);
					return false;
				}
			}
		}
	}
	NewWorld->SetGameInstance(WorldContext.OwningGameInstance);

	GWorld = NewWorld;

	WorldContext.SetCurrentWorld(NewWorld);
	WorldContext.World()->WorldType = WorldContext.WorldType;

#if !UE_BUILD_SHIPPING
	GWorld->bCreateRenderStateForHiddenComponentsWithCollsion = bOldWorldWasShowingCollisionForHiddenComponents;
#endif

	// PIE worlds are not added to root and are initialized differently
	if (WorldContext.WorldType == EWorldType::PIE)
	{
		check(WorldContext.World()->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor));
		WorldContext.World()->ClearFlags(RF_Standalone);

		PostCreatePIEWorld(WorldContext.World());
	}
	else
	{
		WorldContext.World()->AddToRoot();

		// The world should not have been initialized before this
		if (ensure(!WorldContext.World()->bIsWorldInitialized))
		{
			WorldContext.World()->InitWorld();
		}
	}

	// Handle pending level.
	if( Pending )
	{
		check(Pending == WorldContext.PendingNetGame);
		MovePendingLevel(WorldContext);
	}
	else
	{
		check(!WorldContext.World()->GetNetDriver());
	}

	WorldContext.World()->SetGameMode(URL);

	if (FAudioDevice* AudioDevice = WorldContext.World()->GetAudioDeviceRaw())
	{
		if (AWorldSettings* WorldSettings = WorldContext.World()->GetWorldSettings())
		{
			AudioDevice->SetDefaultBaseSoundMix(WorldSettings->DefaultBaseSoundMix);
		}
		else
		{
			UE_LOG(LogInit, Warning, TEXT("Unable to get world settings. Can't initialize default base soundmix."));
		}
	}

	// Listen for clients.
	if (Pending == NULL && (!GIsClient || URL.HasOption(TEXT("Listen"))))
	{
		if (!WorldContext.World()->Listen(URL))
		{
			UE_LOG(LogNet, Error, TEXT("LoadMap: failed to Listen(%s)"), *URL.ToString());
		}
	}

	const TCHAR* MutatorString = URL.GetOption(TEXT("Mutator="), TEXT(""));
	if (MutatorString)
	{
		TArray<FString> Mutators;
		FString(MutatorString).ParseIntoArray(Mutators, TEXT(","), true);

		for (int32 MutatorIndex = 0; MutatorIndex < Mutators.Num(); MutatorIndex++)
		{
			LoadPackagesFully(WorldContext.World(), FULLYLOAD_Mutator, Mutators[MutatorIndex]);
		}
	}

	// Process global shader results before we try to render anything
	// Do this before we register components, as USkinnedMeshComponents require the GPU skin cache global shaders when creating render state.
	if (GShaderCompilingManager)
	{
		GShaderCompilingManager->ProcessAsyncResults(false, true);
	}

	{
		DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UEngine::LoadMap.LoadPackagesFully"), STAT_LoadMap_LoadPackagesFully, STATGROUP_LoadTime);

		// load any per-map packages
		check(WorldContext.World()->PersistentLevel);
		LoadPackagesFully(WorldContext.World(), FULLYLOAD_Map, WorldContext.World()->PersistentLevel->GetOutermost()->GetName());

		// Make sure "always loaded" sub-levels are fully loaded
		WorldContext.World()->FlushLevelStreaming(EFlushLevelStreamingType::Visibility);

		if (!GIsEditor && !IsRunningDedicatedServer())
		{
			// If requested, duplicate dynamic levels here after the source levels are created.
			WorldContext.World()->DuplicateRequestedLevels(FName(*URL.Map));
		}
	}
	FMoviePlayerProxy::BlockingTick();

#if WITH_EDITOR
	// Gives a chance to any assets being used for PIE/game to complete
	FAssetCompilingManager::Get().ProcessAsyncTasks();
#endif

	// Note that AI system will be created only if ai-system-creation conditions are met
	WorldContext.World()->CreateAISystem();

	// Initialize gameplay for the level.
	{
		FRegisterComponentContext Context(WorldContext.World());
		WorldContext.World()->InitializeActorsForPlay(URL, true, &Context);
		Context.Process();
	}

	// calling it after InitializeActorsForPlay has been called to have all potential bounding boxed initialized
	FNavigationSystem::AddNavigationSystemToWorld(*WorldContext.World(), FNavigationSystemRunMode::GameMode);

	// Remember the URL. Put this before spawning player controllers so that
	// a player controller can get the map name during initialization and
	// have it be correct
	WorldContext.LastURL = URL;
	WorldContext.LastURL.Map = URLTrueMapName;

	if (WorldContext.World()->GetNetMode() == NM_Client)
	{
		WorldContext.LastRemoteURL = URL;
	}
	FMoviePlayerProxy::BlockingTick();

	// Spawn play actors for all active local players
	if (WorldContext.OwningGameInstance != NULL)
	{
		for(auto It = WorldContext.OwningGameInstance->GetLocalPlayerIterator(); It; ++It)
		{
			FString Error2;
			if(!(*It)->SpawnPlayActor(URL.ToString(1),Error2,WorldContext.World()))
			{
				UE_LOG(LogEngine, Fatal, TEXT("Couldn't spawn player: %s"), *Error2);
			}
		}
	}

	// Prime texture streaming.
	IStreamingManager::Get().NotifyLevelChange();

	if (GEngine && GEngine->XRSystem.IsValid())
	{
		GEngine->XRSystem->OnBeginPlay(WorldContext);
	}
	WorldContext.World()->BeginPlay();

	FMoviePlayerProxy::BlockingTick();
	// send a callback message
	PostLoadMapCaller.Broadcast(WorldContext.World());
	MoviePlayerBlock.Finish();
	FMoviePlayerProxy::BlockingForceFinished();

	WorldContext.World()->bWorldWasLoadedThisTick = true;

	// We want to update streaming immediately so that there's no tick prior to processing any levels that should be initially visible
	// that requires calculating the scene, so redraw everything now to take care of it all though don't present the frame.
	RedrawViewports(false);

	// RedrawViewports() may have added a dummy playerstart location. Remove all views to start from fresh the next Tick().
	IStreamingManager::Get().RemoveStreamingViews( RemoveStreamingViews_All );

	// See if we need to record network demos
	if ( WorldContext.World()->GetAuthGameMode() == NULL || !WorldContext.World()->GetAuthGameMode()->IsHandlingReplays() )
	{
		if ( URL.HasOption( TEXT( "DemoRec" ) ) && WorldContext.OwningGameInstance != nullptr )
		{
			const TCHAR* DemoRecName = URL.GetOption( TEXT( "DemoRec=" ), NULL );

			// Record the demo, optionally with the specified custom name.
			WorldContext.OwningGameInstance->StartRecordingReplay( FString(DemoRecName), WorldContext.World()->GetMapName(), URL.Op );
		}
	}

	STAT_ADD_CUSTOMMESSAGE_NAME( STAT_NamedMarker, *(FString( TEXT( "LoadMapComplete - " ) + URL.Map )) );
	TRACE_BOOKMARK(TEXT("LoadMapComplete - %s"), *URL.Map);
	MALLOC_PROFILER( FMallocProfiler::SnapshotMemoryLoadMapEnd( URL.Map ); )

	double StopTime = FPlatformTime::Seconds();

	UE_LOG(LogLoad, Log, TEXT("Took %f seconds to LoadMap(%s)"), StopTime - StartTime, *URL.Map);
	FLoadTimeTracker::Get().DumpRawLoadTimes();
	WorldContext.OwningGameInstance->LoadComplete(StopTime - StartTime, *URL.Map);

	// perform the delayed TrimMemory if desired
	if (GDelayTrimMemoryDuringMapLoadMode != 0)
	{
		TrimMemory();

		if (GDelayTrimMemoryDuringMapLoadMode == 1)
		{
			// all future map loads should be normal
			GDelayTrimMemoryDuringMapLoadMode = 0;
		}
	}

	// Successfully started local level.
	return true;
}

이 길고 우람한 코드를 보아라.

이를 읽고 읽어 보았지만 이 이상은 추가적인 기반 지식이 요구가 되어 한번 끊어 가도록 하겠다.

 

거의 겉핥기 식으로 보았지만 소득이 없는 것은 아니었다.

우리는 최소한 Level이 어떻게 관리 되는지, 어떻게 열리는지 대략적인 구조를 파악했다.

남은건 Engine.h 안의 내용을 차근차근 익혀나가는 것이다.

 

아마 긴 싸움이 되지 않을까 싶다.

'UE5 > Level' 카테고리의 다른 글

[Level] FWorldContext 분석 - 1  (0) 2022.07.10

+ Recent posts