언리얼 오브젝트 패키지
- 단일 언리얼 오브젝트가 가진 정보는 저장할 수 있지만, 오브젝트들이 조합되어 있다면?
- 저장된 언리얼 오브젝트 데이터를 효과적으로 찾고 관리하는 방법은?
- 복잡한 계층 구조를 가진 언리얼 오브젝트를 효과적으로 저장과 불러들이는 방법을 통일해야 함.
- 언리얼 엔진은 이를 위해 패키지 단위로 언리얼 오브젝트를 관리함.
- 패키지의 중의적 개념
- 언리얼 엔진은 다양한 곳에서 단어 패키지를 사용
- 언리얼 오브젝트를 감싼 포장 오브젝트를 의미 (이번 주제)
- 또한 개발된 최종 콘텐츠를 정리해 프로그램으로 만드는 작업을 의미
- DLC와 같이 향후 확장 콘텐츠에 사용되는 별도의 데이터 묶음을 의미
패키지와 에셋
- 언리얼 오브젝트 패키지는 다수의 언리얼 오브젝트를 포장하는데 사용되는 언리얼 오브젝트.
- 모든 언리얼 오브젝트는 패키지에 소속되어 있음
- 언리얼 오브젝트 패키지의 서브 오브젝트를 에셋이라고 하며 에디터에는 이들이 노출됨
- 구조상 패키지는 다수의 언리얼 오브젝트를 소유할 수 있으나, 일반적으로 하나의 에셋만 가짐.
- 에셋은 다시 다수의 서브 오브젝트를 가질 수 있으며, 모두 언리얼 오브젝트 패키지에 포함됨.

에셋 정보의 저장과 로딩 전략
- 게임 제작 단계에서 에셋 간의 연결 작업을 위해 직접 패키지를 불러 할당하는 작업은 부하가 큼.
- 에셋 로딩 대신 패키지와 오브젝트를 지정한 문자열을 대체해 사용. 이를 오브젝트 경로라고 함.
- 프로젝트 내에 오브젝트 경로 값은 유일함을 보장함.
- 그렇기에 오브젝트 간의 연결은 오브젝트 경로 값으로 기록될 수 있음.
- 오브젝트 경로를 사용해 다양한 방법으로 엣세을 로딩할 수 있음.
- 에셋의 로딩 전략
- 프로젝트에서 에셋이 반드시 필요한 경우 : 생성자 코드에서 미리 로딩
- 런타임에서 필요한 때 바로 로딩하는 경우 : 런타임 로직에서 정적 로딩
- 런타임에서 비동기적으로 로딩하는 경우 : 런타임 로직에서 관리자를 사용해 비동기 로딩
오브젝트 경로
- 패키지 이름과 에셋 이름을 한 데 묶은 문자열
- 에셋 클래스 정보는 생략 가능
- 패키지 내 데이터를 모두 로드하지 않고 오브젝트 경로를 사용해 필요한 에셋만 로드할 수 있음.

에셋 참조
언리얼 엔진 에셋 참조
직접 프로퍼티 참조
생성 시간 참조
- 강 참조를 진행할 때 해당 오브젝트가 가리키고 있는 에셋을 생성자 코드에서 생성
- 생성자 코드는 엔진이 초기화될 때 실행
- 즉, 게임이 실행되기 전에 해당 에셋이 로딩
간접 프로퍼티 참조
- TSoftObjectPtr 을 사용
- LoadObject<>() 메서드나 StaticLoadObject()나FStreamingManager 를 사용하여 오브젝트를 로드할 수 있음
- FSoftObjectPath 가 오브젝트 경로를 의미
- 이 값을 이용해 스트리밍 매니저를 사용해 비동기적이나 동기적으로 에셋을 로딩
오브젝트 검색/로드
- 생성 또는 로드된 오브젝트: FindObject
- 로드되지 않은 오브젝트: LoadObject
에셋 스트리밍 관리자
- 에셋의 비동기 로딩을 지원하는 관리자 객체
- 콘텐츠 제작과 무관한 싱글턴 클래스에 FStreamableManager를 선언해두면 좋음.
- FStreamableManager를 활용해 에셋의 동기/비동기 로딩을 관리할 수 있음.
- 다수의 오브젝트 경로를 입력해 다수의 에셋을 로딩하는 것도 가능
에셋 세이브 로드 실습
void UMyGameInstance::Init()
{
Super::Init();
/// Mk.1
// 구조체에 데이터 넣음
FStudentData RawDataSrc(16, TEXT("이득우"));
// 현재 프로젝트 경로에 있는 Saved 폴더 경로를 가져옴
const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);
{
// Saved폴더 안에 RawData.bin 파일을 만듬
const FString RawDataFileName(TEXT("RawData.bin"));
FString RawDataAbosolutePath = FPaths::Combine(SavedDir, "RawDataFileName");
UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
// RawData.bin 까지의 경로를 다시 표준 형식으로 만듬
FPaths::MakeStandardFilename(RawDataAbosolutePath);
UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
// 아카이브 쓰기전용 클래스 생성
FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
if (nullptr != RawFileWriterAr)
{
// bin 파일에 처음에 만든 구조체 정보를 넣음 (<< 는 구조체에서 오퍼레이터 연산자 구현 했음)
*RawFileWriterAr << RawDataSrc;
RawFileWriterAr->Close();
delete RawFileWriterAr;
RawFileWriterAr = nullptr;
}
// 아카이브 읽거 전용 클래스 생성
FStudentData RawDataDest;
FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath);
if (nullptr != RawFileReaderAr)
{
// 이번에는 역으로 bin 파일에서 구조체로 정보를 가져옴
*RawFileReaderAr << RawDataDest;
RawFileReaderAr->Close();
delete RawFileReaderAr;
RawFileReaderAr = nullptr;
UE_LOG(LogTemp, Log, TEXT("{RawData} 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
}
}
/// Mk.2
// 새로운 학생 데이터 생성
StudentSrc = NewObject<UStudent>();
StudentSrc->SetName(TEXT("이득우"));
StudentSrc->SetOrder(59);
{
// Saved 폴더에 bin 파일 새로 생성
const FString ObjectDataFileName(TEXT("ObjectData.bin"));
FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
// 경로 표준화
FPaths::MakeStandardFilename(ObjectDataAbsolutePath);
// 학생 정보를 시리얼라이즈할 배열 생성
TArray<uint8> BufferArray;
// 배열과 writer 연결
FMemoryWriter MemoryWriterAr(BufferArray);
StudentSrc->Serialize(MemoryWriterAr);
// 쓰기 아카이브 생성
if (TUniquePtr<FArchive> FileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
{
// 시리얼라이즈한 배열을 아카이브에 쓰기
*FileWriterAr << BufferArray;
FileWriterAr->Close();
}
// 파일로부터 읽어와서 저장할 배열 생성
TArray<uint8> BufferArrayFromFile;
// 읽기 아카이브 생성
if (TUniquePtr<FArchive> FileReaderAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)))
{
// 파일로부터 배열로 읽어들이기
*FileReaderAr << BufferArrayFromFile;
FileReaderAr->Close();
}
// 배열과 읽기 아카이브 연결
FMemoryReader MemoryReaderAr(BufferArrayFromFile);
UStudent* StudentDest = NewObject<UStudent>();
// 학생 정보 생성
StudentDest->Serialize(MemoryReaderAr);
// 테스트 출력
PrintStudentInfo(StudentDest, TEXT("ObjectData"));
}
/// Mk.3 Json + 스마트포인터
{
/// Json 쓰기
// 파일 생성 및 경로 표준화
FString JsonDataFileName(TEXT("StudentJsonData.txt"));
FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
FPaths::MakeStandardFilename(JsonDataFileName);
// Json 오브젝트 생성 (스마트 포인터로)
TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
// 학생 정보를 Json 오브젝트로 변환
FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);
// Json 쓰기 아카이브 생성 및 string 연결
FString JsonOutString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
// Json 오브젝트 시리얼라이즈
if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
{
// 시리얼라이즈 해서 얻은 string 을 사용하여 json 파일에 저장
FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
}
/// Json 읽기
// Json 파일정보를 string으로 가져오기
FString JsonInString;
FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);
// Json 읽기 아카이브 생성 및 string 연결
TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);
// Json 오브젝트 생성
TSharedPtr<FJsonObject> JsonObjectDest;
// Json 오브젝트 디시리얼라이즈
if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
{
// 테스트용 학생 생성
UStudent* JsonStudentDest = NewObject<UStudent>();
// Json 오브젝트 정보를 학생에 기입
if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
{
// 테스트 출력
PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
}
}
}
SavedStudentPackage();
LoadStudentPackage();
}
void UMyGameInstance::SavedStudentPackage() const
{
// 패키지 저장전 이미 패키지가 있는지 체크
// 이미 있다면 다 로딩하고 저장하는 것이 안전하다.
UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
if (StudentPackage)
{
StudentPackage->FullyLoad();
}
// 패키지 생성
StudentPackage = CreatePackage(*PackageName);
// 패키지 플래그 설정
EObjectFlags ObjectFlag = RF_Public | RF_Standalone;
// 오브젝트 생성 + 패키지 지정
UStudent* TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);
TopStudent->SetName(TEXT("이득우"));
TopStudent->SetOrder(36);
// 패키지에 들어갈 서브 오브젝트 생성
const int32 NumofSubs = 10;
for (int32 index = 1; index <= NumofSubs; ++index)
{
FString SubObjectName = FString::Printf(TEXT("Student%d"), index);
UStudent* SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
SubStudent->SetName(FString::Printf(TEXT("학생%d"), index));
SubStudent->SetOrder(index);
}
// 패키지 파일 이름 설정
//const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
const FString PackageFileName = FString::Printf(TEXT("%s.%s"), *PackageName, *FPackageName::GetAssetPackageExtension());
// 플래그 설정
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = ObjectFlag;
// 패키지 저장
if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
{
UE_LOG(LogTemp, Log, TEXT("패키지가 성공적으로 저장되었습니다."));
}
}
void UMyGameInstance::LoadStudentPackage() const
{
// 패키지 로드 (메타데이터만 로드하고 실제 데이터(텍스처,메쉬,애니메이션 등)는 로드하지 않는다.)
UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
if (nullptr == StudentPackage)
{
UE_LOG(LogTemp, Warning, TEXT("패키지를 찾을 수 없습니다."));
return;
}
// 패키지 내의 모든 리소스 로드
StudentPackage->FullyLoad();
// 패키지 안에 해당하는 에셋일므으로 오브젝트 찾기
UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
// 테스트 출력
PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
}
const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
const FString UMyGameInstance::AssetName = TEXT("TopStudent");
오브젝트 경로를 통한 로딩
void UMyGameInstance::LoadStudentObject() const
{
const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
UStudent* TopStudent = LoadObject<UStudent>(nullptr, *TopSoftObjectPath);
PrintStudentInfo(TopStudent, TEXT("LoadObject Asset"));
}
- LoadObject<> 와 오브젝트 경로를 통해서 로딩
생성자에서 로딩
UMyGameInstance::UMyGameInstance()
{
const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
static ConstructorHelpers::FObjectFinder<UStudent> UASSET_TopStudent(*TopSoftObjectPath);
if (UASSET_TopStudent.Succeeded())
{
PrintStudentInfo(UASSET_TopStudent.Object, TEXT("Constructor"));
}
}
- ConstructorHelpers::FObjectFinder<> 를 이용
- Succeeded 를 통해 로딩이 됐는지 확인 가능
- .Object 로 접근해서 사용 가능
- 생성자에서 로딩하는 경우 에셋이 반드시 있다는 가정하에 진행 됨
- 없으면 시작할 때 강력한 경고와 에러 메시지가 나옴
비동기 로딩
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();
}
}
}
)
- #include "Engine/StreamableManager.h" 헤더에 인클루드
- FStreamableManager StreamableManager 멤버변수
- TSharedPtr< FStreamableHandle > Handle 멤버변수
- StreamableManager.RequestAsyncLoad 를 이용