원래는 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

+ Recent posts