Unreal5/Unreal C++

[U C++] 언리얼 오브젝트 패키지(Package)와 애셋(Asset)

taene_ 2024. 7. 12. 17:19

< 언리얼 오브젝트 패키지 >

언리얼 오브젝트 패키지

  • 단일 언리얼 오브젝트가 가진 정보는 직렬화를 통해 저장할 수 있지만, 오브젝트들이 조합되어 있다면?
    • 저장된 언리얼 오브젝트 데이터를 효과적으로 찾고 관리하는 방법은?
    • 복잡한 계층 구조를 가진 언리얼 오브젝트를 효과적으로 저장과 불러들이는 방법을 통일해야 한다.
  • 언리얼 엔진은 이를 위해 패키지(UPackage)단위로 언리얼 오브젝트를 관리한다.
  • 언리얼 엔진 "패키지"의 여러 의미
    • 언리얼 오브젝트를 감싼 포장 오브젝트 ( 언리얼 오브젝트 패키지 )
    • 개발된 최종 콘텐츠를 정리해 프로그램으로 만드는 작업 ( 예) 게임 패키징 )
    • DLC와 같은 향후 확장 콘텐츠에 사용되는 별도의 데이터 묶음 ( 예) pkg 파일 )

 

패키지(Package)와 애셋(Asset)

  • 언리얼 오브젝트 패키지는 다수의 언리얼 오브젝트를 포장하는데 사용하는 언리얼 오브젝트이다.
    • 모든 언리얼 오브젝트는 Transient Package(임시 패키지)에 소속되어 있다.
  • 언리얼 오브젝트 패키지의 서브 오브젝트를 애셋(Asset)이라고 하며 에디터에는 애셋들이 노출된다.
  • 구조상 패키지는 다수의 언리얼 오브젝트를 소유할 수 있지만, 일반적으로는 하나의 애셋만 가지도록 한다.
  • 애셋은 다시 다수의 서브 오브젝트를 가질 수 있고, 모두 언리얼 오브젝트 패키지에 포함된다.
    • 하지만 서브 오브젝트들은 에디터에는 노출되지 않는다.

 

패키지의 활용

// MyGameInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

struct FStudentData
{
	FStudentData() {}
	FStudentData(int32 InOrder, const FString& InName) :Order(InOrder), Name(InName) {}

	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
	{
		Ar << InStudentData.Order;
		Ar << InStudentData.Name;
		return Ar;
	}

	int32 Order = -1;
	FString Name = TEXT("홍길동");

};

/**
 *
 */
UCLASS()
class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
public:
	UMyGameInstance();

	virtual void Init() override;

	void SaveStudentPackage() const;
	void LoadStudentPackage() const;

private:

	static const FString PackageName;	// static 변수이므로 cpp에서 초기화
	static const FString AssetName;

	UPROPERTY()
	TObjectPtr<class UStudent> StudentSrc;
};
// MyGameInstance.cpp


#include "MyGameInstance.h"
#include "Student.h"
#include "JsonObjectConverter.h"
#include "UObject/SavePackage.h"

// 패키지 이름 설정: /Game/만들 패키지 이름
const FString UMyGameInstance::PackageName = TEXT("/Game/Student");	// 게임에서 사용되는 애셋들을 모아놓은 대표 폴더: /Game
// Student 패키지가 메인으로 관리할 애셋 이름 설정
const FString UMyGameInstance::AssetName = TEXT("TopStudent");

void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)	// UE_LOG 출력함수
{
	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
}

UMyGameInstance::UMyGameInstance()
{
}

void UMyGameInstance::Init()
{
	Super::Init();

	FStudentData RawDataSrc(16, TEXT("김태연"));

	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));	// 현 프로젝트의 폴더에서 Saved 폴더의 경로를 얻어옴

	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더: %s"), *SavedDir);

	// ======RawData.bin 파일을 직접 생성하고 구조체 내에 있는 데이터를 파일에 쓰고 다시 읽는 방법======
	{. . .}

	// ======언리얼 오브젝트를 저장하고 불러들이는 코드======
	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("김태연"));
	StudentSrc->SetOrder(59);
	{. . .}

	// ======Json 직렬화======
	{. . .}

	// ======패키지를 사용한 에셋 저장 방법======
	{
		// 패키지 저장 함수
		SaveStudentPackage();
		// 저장된 패키지 로딩 함수
		LoadStudentPackage();
	}
}

void UMyGameInstance::SaveStudentPackage() const
{
	// ======만약 패키지가 있다면, 모두 로딩한 후 저장하는 것이 좋다.======
	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
	if (StudentPackage)
	{
		StudentPackage->FullyLoad();
	}

	// ======패키지를 사용하기 위해서 패키지와 패키지가 담고 있는 대표 애셋을 설정해 줘야 한다!!!======
	
	// 패키지 생성 코드
	StudentPackage = CreatePackage(*PackageName);
	// 패키지를 저장하는 옵션(플래그)을 지정하기(패키지를 저장하는 EObjectFlags라 하는 Enum 값을 지정)
	EObjectFlags ObjectFlag = RF_Public | RF_Standalone;

	// 패키지를 저장하기 전에 패키지에 어떤 내용을 담을지 지정해 줘야 한다.
	// 인자에 아무것도 안들어가는 경우: 트래지언트 패키지라는 임시 패키지안에 언리얼 오브젝트가 저장된다.
	UStudent* TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);	
	/* 
	* 생성한 언리얼 오브젝트 NewObject<UStudent>()를 안전하게 패키지에 넣는다.
	* 첫번째 인자: 언리얼 오브젝트를 저장할 패키지 지정, 
	* 두번째 인자: UStudent에 대한 이름(클래스 정보로 강제로 채워도 된다),
	* 세번째 인자: 오브젝트를 생성할 때 고유한 이름을 지정(애셋 이름),
	* 네번째 인자: 저장 플래그 지정
	*/

	// Student 오브젝트에 값 넣기
	TopStudent->SetName(TEXT("김태연"));
	TopStudent->SetOrder(36);

	// TopStudent의 서브 오브젝트 10개 생성해보기
	const int32 NumofSubs = 10;
	for (int32 ix = 1; ix <= NumofSubs; ++ix)
	{
		FString SubObjectName = FString::Printf(TEXT("Student%d"), ix);
		// StudentPackage의 바로 아래에 패키지의 애셋인 TopStudent의 아래에 서브 오브젝트들이 들어가야한다.
		UStudent* SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
		SubStudent->SetName(FString::Printf(TEXT("학생%d"), ix));
		SubStudent->SetOrder(ix);
	}

	// 위의 것들을 저장하기 위해서 패키지가 저장될 경로를 지정해 줘야 하고, 패키지에 대한 확장자를 부여해 줘야 한다.
	const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
	// #include "UObject/SavePackage.h" 추가
	FSavePackageArgs SaveArgs;
	SaveArgs.TopLevelFlags = ObjectFlag;

	// 패키지 저장하기
	// UPackage::SavePackage(저장할 패키지, 저장할 패키지의 대표 애셋, 파일 이름, 세이브 관련 파라미터(아규먼트, 플래그))
	if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
	{
		UE_LOG(LogTemp, Log, TEXT("패키지가 성공적으로 저장되었습니다."));
	}

}

void UMyGameInstance::LoadStudentPackage() const
{
	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);	// (패키지 정보(없어도 됨), 패키지 이름, 옵션(LOAD_None은 기본옵션))
	
	// ======패키지가 없을 경우 로직 - 없으면 함수 끝냄======
	if (nullptr == StudentPackage)
	{
		UE_LOG(LogTemp, Log, TEXT("패키지를 찾을 수 없습니다."));
		return;
	}

	// ======패키지가 있을 경우 로직======
	StudentPackage->FullyLoad();	// FullyLoad() - 안에 갖고있는 애셋을 모두 로딩하는 함수

	// 로딩된 패키지 안에서 원하는 오브젝트 애셋을 찾을 수 있다.
	// FindObject<>(찾을 패키지 지정, 찾을 애셋 이름);
	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
}

패키지 이름 - Student, 애셋 이름 - TopStudent

 

< 애셋 참조와 로딩 >

애셋 정보의 저장과 로딩 전략

  • 게임 제작 단계에서 애셋 간의 연결 작업을 위해 직접 패키지를 불러 할당하는 작업은 부하가 크다.
    • 애셋 로딩 대신 패키지와 오브젝트를 지정한 문자열을 대체해 사용하는데, 이를 오브젝트 경로라고 한다.
    • 프로젝트 내에 오브젝트 경로 값은 유일함을 보장한다.
    • 그렇기 때문에 오브젝트 간의 연결은 오브젝트 경로 값으로 기록될 수 있다.
    • 오브젝트 경로를 사용해 다양한 방법으로 애셋을 로딩할 수 있다.
  • 애셋 로딩 전략
    • 프로젝트에서 애셋이 반드시 필요한 경우: 엔진이 초기화될 때 실행되는 생성자 코드에서 해당 애셋을 미리 로딩
      ( 이 경우, 게임이 실행되기 전에 해당 애셋이 로딩이 된다. )
    • 런타임에서 필요한 때 바로 로딩하는 경우: 런타임 로직에서 정적으로 로딩
      ( 이 경우, 다른 프로세스의 실행을 막아 게임이 멈추게 된다. )
    • 런타임에서 비동기적으로 로딩하는 경우: 런타임 로직에서 언리얼이 제공하는 관리자를 사용해 애셋을 비동기 방식으로 로딩
      ( 이 경우, 기존의 게임 로직을 진행하면서 비동기적으로 로딩하므로 게임이 멈추지 않는다. )

 

오브젝트 경로(Object Path)

 

애셋 스트리밍 관리자(Streamable Manager)

  • 애셋의 비동기 로딩을 지원하는 언리얼 엔진의 관리자 객체
  • 콘텐츠 제작과 무관한 싱글턴 클래스에 FStreamableManager를 선언해두면 좋다.
    • GameInstance는 좋은 선택지이다.
  • FStreamableManager를 활용해 애셋의 동기/비동기 로딩을 관리할 수 있다.
  • 다수의 오브젝트 경로를 입력해 다수의 애셋을 로딩하는 것도 가능하다.

 

오브젝트 경로를 사용한 애셋의 동기/비동기 로딩 활용

// MyGameInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "Engine/StreamableManager.h"
#include "MyGameInstance.generated.h"

struct FStudentData
{
	FStudentData() {}
	FStudentData(int32 InOrder, const FString& InName) :Order(InOrder), Name(InName) {}

	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
	{
		Ar << InStudentData.Order;
		Ar << InStudentData.Name;
		return Ar;
	}

	int32 Order = -1;
	FString Name = TEXT("홍길동");

};

/**
 *
 */
UCLASS()
class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
public:
	UMyGameInstance();

	virtual void Init() override;

	void SaveStudentPackage() const;
	void LoadStudentPackage() const;

	void LoadStudentByObjectPath() const;


private:

	static const FString PackageName;	// static 변수이므로 cpp에서 초기화
	static const FString AssetName;

	UPROPERTY()
	TObjectPtr<class UStudent> StudentSrc;

	// 애셋 비동기 로딩 방식
	FStreamableManager StreamableManager;	// #include "Engine/StreamableManager.h" 추가
	TSharedPtr<FStreamableHandle> Handle; // 스트리밍된 애셋을 관리할 수 있는 핸들 지정
};
// MyGameInstance.cpp


#include "MyGameInstance.h"
#include "Student.h"
#include "JsonObjectConverter.h"
#include "UObject/SavePackage.h"

// 패키지 이름 설정: /Game/만들 패키지 이름
const FString UMyGameInstance::PackageName = TEXT("/Game/Student");	// 게임에서 사용되는 애셋들을 모아놓은 대표 폴더: /Game
// Student 패키지가 메인으로 관리할 애셋 이름 설정
const FString UMyGameInstance::AssetName = TEXT("TopStudent");

void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)	// UE_LOG 출력함수
{
	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
}

UMyGameInstance::UMyGameInstance()
{
	// # 생성자에서 애셋을 로딩하는 방법(2)
	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
	// 생성자에서 로딩할 땐 LoadObject()가 아니라 언리얼에서 제공하는 ConstructorHelpers를 사용해 줘야 한다.
	static ConstructorHelpers::FObjectFinder<UStudent> UASSET_TopStudent(*TopSoftObjectPath);
	if (UASSET_TopStudent.Succeeded())	// 애셋 로딩에 성공했는지 확인
	{
		PrintStudentInfo(UASSET_TopStudent.Object, TEXT("Constructor"));
	}
	/*
	* 실행결과: 2번 로그가 찍힌다!
	* 에디터가 로딩될 때 한 번,
	* 에디터내에 게임이 실행할 때 constructor 관련 함수들이 자동으로 호출되기 때문에 두 번 찍힌다.
	* 생성자에서 애셋을 로딩하는 경우: 그 애셋은 반드시 있다는 가정하에 진행된다.
	*/

}

void UMyGameInstance::Init()
{
	Super::Init();

	FStudentData RawDataSrc(16, TEXT("김태연"));

	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));	// 현 프로젝트의 폴더에서 Saved 폴더의 경로를 얻어옴

	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더: %s"), *SavedDir);

	// ======RawData.bin 파일을 직접 생성하고 구조체 내에 있는 데이터를 파일에 쓰고 다시 읽는 방법======
	{. . .}

	// ======언리얼 오브젝트를 저장하고 불러들이는 코드======
	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("김태연"));
	StudentSrc->SetOrder(59);
	{. . .}

	// ======Json 직렬화======
	{. . .}

	// ======패키지를 사용한 에셋 저장 방법======
	{
		// 패키지 저장 함수
		SaveStudentPackage();
		// 저장된 패키지 로딩 함수
		// LoadStudentPackage();
	}

	// ======오브젝트 경로를 사용해 애셋을 동기/비동기 로딩하는 방법(1,2,3)======
	
	// # 패키지를 로드하지 않고 오브젝트 경로만을 사용해 애셋을 로딩하는 방법(1)
	// LoadStudentByObjectPath();

	// # 애셋을 비동기 방식으로 로딩하는 방법(3) - AsyncLoad
	{
		const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
		Handle = StreamableManager.RequestAsyncLoad(TopSoftObjectPath,
			[&]()
			{
				if (Handle.IsValid() && Handle->HasLoadCompleted())	// 핸들이 유효한지, 핸들이 로딩이 다 끝났는지 검사
				{
					// 문제가 없다면 핸들을 통해 객체를 가져온다.
					UStudent* TopStudent = Cast<UStudent>(Handle->GetLoadedAsset());
					if (TopStudent)	// 객체를 가져오는데 성공했다면 로그를 찍고 다 쓴 핸들을 닫아주기
					{
						PrintStudentInfo(TopStudent, TEXT("AsyncLoad"));

						Handle->ReleaseHandle();
						Handle.Reset();
					}
				}
			}
		);
	}
}

void UMyGameInstance::SaveStudentPackage() const
{. . .}

void UMyGameInstance::LoadStudentPackage() const
{. . .}

void UMyGameInstance::LoadStudentByObjectPath() const
{
	// 애셋의 오브젝트 경로 생성
	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);

	UStudent* TopStudent = LoadObject<UStudent>(nullptr, *TopSoftObjectPath);
	PrintStudentInfo(TopStudent, TEXT("LoadObject Asset ByObjectPath"));
}