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"));
}
< 애셋 참조와 로딩 >
애셋 정보의 저장과 로딩 전략
- 게임 제작 단계에서 애셋 간의 연결 작업을 위해 직접 패키지를 불러 할당하는 작업은 부하가 크다.
- 애셋 로딩 대신 패키지와 오브젝트를 지정한 문자열을 대체해 사용하는데, 이를 오브젝트 경로라고 한다.
- 프로젝트 내에 오브젝트 경로 값은 유일함을 보장한다.
- 그렇기 때문에 오브젝트 간의 연결은 오브젝트 경로 값으로 기록될 수 있다.
- 오브젝트 경로를 사용해 다양한 방법으로 애셋을 로딩할 수 있다.
- 애셋 로딩 전략
- 프로젝트에서 애셋이 반드시 필요한 경우: 엔진이 초기화될 때 실행되는 생성자 코드에서 해당 애셋을 미리 로딩
( 이 경우, 게임이 실행되기 전에 해당 애셋이 로딩이 된다. ) - 런타임에서 필요한 때 바로 로딩하는 경우: 런타임 로직에서 정적으로 로딩
( 이 경우, 다른 프로세스의 실행을 막아 게임이 멈추게 된다. ) - 런타임에서 비동기적으로 로딩하는 경우: 런타임 로직에서 언리얼이 제공하는 관리자를 사용해 애셋을 비동기 방식으로 로딩
( 이 경우, 기존의 게임 로직을 진행하면서 비동기적으로 로딩하므로 게임이 멈추지 않는다. )
- 프로젝트에서 애셋이 반드시 필요한 경우: 엔진이 초기화될 때 실행되는 생성자 코드에서 해당 애셋을 미리 로딩
오브젝트 경로(Object Path)
- 패키지 이름과 애셋 이름을 한 데 묶은 문자열
- 애셋 클래스 정보는 생략할 수 있다.
- 패키지 내 데이터를 모두 로드하지 않고 오브젝트 경로를 사용해 필요한 애셋만 개별적으로 로드할 수 있다.
- {애셋클래스정보}'{패키지이름}.{애셋이름}' or {패키지이름}.{애셋이름}
- https://dev.epicgames.com/documentation/ko-kr/unreal-engine/referencing-assets-in-unreal-engine?application_version=5.1
애셋 스트리밍 관리자(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"));
}