개발하다 날리고 개발하다 날리다가 다시 시작을 했습니다.

목적은 예전과 비슷합니다.

게임플레이(코어 컨텐츠) 개발을 지향하고 기회가 된다면 엔진 코드 수정을 적극적으로 병행 할 예정입니다.

그러기 위해 상당시간 프로젝트 구조 잡는데 시간을 들였고, 정상 진행이 가능한 수준의 프로젝트를 생성할 수 있었습니다.

https://redchiken.tistory.com/311

 

Unreal Engine 코드를 포함한 Project 생성하기 (with VCS)

일반적으로 언리얼 엔진 입문을 하게 되면 Epic Games 플랫폼으로 프로젝트를 생성을 해서 개발을 시작하게 됩니다. 여기서 조금 더 발전하게 되면 VCS를 사용하게 되고, 더 나아가 엔진 코드를 따

redchiken.tistory.com

다만 오늘 찾은 문제는 기존의 언리얼 엔진 에셋을 바로 사용할 수는 없더군요.

그래서 더미 프로젝트를 따로 만들어서 해당 프로젝트에 에셋을 적용했습니다.

 

다음 작업은 더미 프로젝트에서 적용된 여러 애니메이션 에셋들을 하나의 스켈레탈로 리타겟팅 할 예정입니다.

메쉬나 머테리얼은 최대한 줄이고 애니메이션이나 포즈는 최대한 챙겨서 파일명 정리하고 본 프로젝트에 옮겨오고자 합니다.

그 뒤에는 이전처럼 키 입력 기반으로 애니메이션 하나씩 넣고, 히트박스 포르노 급으로 콜리젼 작업을 진행해볼 예정입니다.

'개발일지 > 코어 플레이 개발' 카테고리의 다른 글

22년 07월 16일 개발일지  (0) 2022.07.16
22년 07월 15일 개발일지  (0) 2022.07.15
22년 02월 19일 개발일지  (0) 2022.02.19
22년 02월 19일 개발일지  (0) 2022.02.19
공부 다시 시작  (0) 2022.02.19

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

일반적으로 언리얼 엔진 입문을 하게 되면 Epic Games 플랫폼으로 프로젝트를 생성을 해서 개발을 시작하게 됩니다.

여기서 조금 더 발전하게 되면 VCS를 사용하게 되고, 더 나아가 엔진 코드를 따로 Clone을 해서 코드를 읽어보게 됩니다.

그러다 보면 문뜩 여타 기업들처럼 엔진 코드를 따로 빌드해서 프로젝트를 진행하고 싶은 욕심이 듭니다.

이는 엔진 코드를 수정하고자 하는 욕심에서 비롯되죠.

 

그런데 그 방법이 뭔가 명확하게 기재가 되어있지 않습니다.

특히 VCS로 관리를 하게 된다면 "그래서 이걸 어떻게 엔진에다가 관리를 하지?"라고 고민을 하게 됩니다.

오늘은 이러한 제 고민을 해결한 방식을 공유하고자 합니다.

 

미리 말씀드리자면, 제 나름대로의 방식이니 효율 등이 완벽하게 고려된 사항은 아님을 먼저 말씀드립니다.

https://github.com/RedChiken/UnrealPractice

 

프로젝터에 언리얼 엔진 코드를 넣자니 업데이트 때마다 커밋을 해줘야 하고, 그 양이 매우 방대합니다.

개인 프로젝트에서 레포지토리에 그 많은 코드를 통째로 넣는 것은 매우 비효율적이라 생각했습니다.

그리고 이미 UnrealEngine 레포지토리가 공개가 되어 있으니 이걸 최대한 활용을 하고 싶었습니다.

그러다가 제가 생각해낸 것은 submodule이었습니다.

 

https://git-scm.com/book/ko/v2/Git-%EB%8F%84%EA%B5%AC-%EC%84%9C%EB%B8%8C%EB%AA%A8%EB%93%88

 

Git - 서브모듈

gitmodules 파일에 있는 URL은 조건에 맞는 사람이면 누구든지 Clone 하고 Fetch 할 수 있도록 접근할 수 있어야 한다. 예를 들어 다른 사람이 Pull을 하는 URL과 라이브러리의 작업을 Push 하는 URL이 서로

git-scm.com

submodule을 이용하면 등록된 레포지토리를 통해 Github상에서 엔진 코드와 클라이언트 코드를 깔끔하게 분리할 수 있을거라 생각했습니다.

그리고 기회가 된다면 적극적으로 EngineCode를 수정하고 싶었습니다.

때문에 기존의 EpicGames/UnrealEngine 레포지토리를 Fork하여 RedChiken/UnrealEngine 레포지토리를 생성하였고, 이를 submodule로 등록을 했습니다.

 

이 뒤에는 튜토리얼에서 공개된 것과 비슷합니다.

프로젝트 레포지토리를 Clone을 받은 뒤, UnrealEngine 폴더에 들어가 엔진 코드를 빌드를 하고 실행을 합니다.

그럼 UE5 이후로 생성된 Unreal Engine 클라이언트를 실행한 것처럼 아래 창이 노출됩니다.

여기서 프로직트 위치를 Clone 받은 레포지토리로 지정하고, 그 안에 다시 언리얼 게임 프로젝트를 생성을 하면 됩니다.

 

이후 언리얼 게임 프로젝트를 열면 엔진 프로젝트와 비슷하게 매우 많은 항목들이 공개되어 있습니다.

대신 한가지 차이점이 있다면, 거기에 게임 프로젝트도 포함이 되어 있는거죠.

이 게임 프로젝트를 빌드를 한 뒤 개발을 시작하면 Unreal Engine 코드가 프로젝트에 포함된 게임 프로젝트를 Github에 관리하기 걸맞게 생성을 하게 됩니다.

 

이 과정에서 몇가지 고려를 해야 할 사항이 있습니다.

첫번째로, 엔진 빌드, 게임 프로젝트 초기 빌드 시 엔진 쪽 설정 파일 일부가 변경이 됩니다.

때문에 이들을 따로 커밋하거나, ignore 해야 하는데 위에 언급 했듯이 게임 프로젝트까지 모두 빌드를 한 뒤에 한꺼번에 처리하는 편이 용이합니다.

두번째로, 개발을 하다 보면 UE5는 오브젝트들을 바이너리 파일들로 관리를 하는 방식을 제공하는데 별 다른 설정 변경이 없다면 이 정책이 유지가 될겁니다.

이를 ignore하는 것도 좋지만, 저는 lfs를 사용하는 것을 추천해보고 싶습니다.

 

언리얼 엔진 개발을 하다 보면 기능 개발에 대한 설명은 조금이라도 남아 있지만 프로젝트 구성에 대한 설명은 매우 미흡합니다.

이 글이 다른 사람들에게 도움이 되었으면 합니다.

'메모장 > 개발 지식' 카테고리의 다른 글

AWS CodeCommit 사용 후기  (0) 2022.09.17
툴팁(Tooltip) 만들기  (0) 2022.05.05
C++ Lambda Capture 주의사항  (0) 2021.11.13
Print client side log in listen server  (0) 2020.06.25
FObjectInitializer constructor fatal error c1853  (0) 2020.04.20

+ Recent posts