Unreal5 Package, Asset

이승한·2024년 4월 6일
0

Unreal5

목록 보기
10/11

패키지

  • 애셋 저장과 다양한 읽기방법

언리얼 패키지에 에셋 저장 로드 ( 이전에 직렬화때 이어서 진행)

// Fill out your copyright notice in the Description page of Project Settings.

#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 const FString AssetName;

	UPROPERTY()
	TObjectPtr<class UStudent> StudentSrc;
};
  1. cpp
// Fill out your copyright notice in the Description page of Project Settings.


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

const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
const FString UMyGameInstance::AssetName = TEXT("TopStudent");

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

}

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

	FStudentData RawDataSrc(16, TEXT("테스트 이름"));

	// 우리가 생성한 언리얼프로젝트 파일안에 Saved 폴더의 경로를 얻어온다.
	const FString SaveDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SaveDir);

	{
		//우리가 저장할 파일의 이름
		const FString RawDataFileName(TEXT("RawData.bin"));

		//이 경우 절대경로값을 가져오지만 이상한 값으로 보인다.
		FString RawDataAbsolutePath = FPaths::Combine(*SaveDir, *RawDataFileName);
		UE_LOG(LogTemp, Log, TEXT("저장할 파일의 전체 경로 : %s"), *RawDataAbsolutePath);

		//전체 경로 다듬기
		FPaths::MakeStandardFilename(RawDataAbsolutePath);
		UE_LOG(LogTemp, Log, TEXT("변경할 파일의 전체 경로 : %s"), *RawDataAbsolutePath);

		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);
		if (nullptr != RawFileWriterAr)
		{
			*RawFileWriterAr << RawDataSrc;
			RawFileWriterAr->Close();
			delete RawFileWriterAr;
			RawFileWriterAr = nullptr;
		}

		//이제 데이터를 읽어와보자
		FStudentData RawDataDest;
		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath);
		if (nullptr != RawFileReaderAr)
		{
			*RawFileReaderAr << RawDataDest;
			RawFileReaderAr->Close();
			delete RawFileReaderAr;
			RawFileReaderAr = nullptr;
		}

		UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
	}

	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("이승한"));
	StudentSrc->SetOrder(60);

	{
		const FString ObjectDataFileName(TEXT("ObjectData.bin"));
		FString ObjectDataAbsolutePath = FPaths::Combine(*SaveDir, *ObjectDataFileName);
		FPaths::MakeStandardFilename(ObjectDataAbsolutePath);

		//메모리에 언리얼 오브젝트의 내용 저장
		//바이트 스트림은 TArray<uint8>로 지정 ( 직렬화를 위한 버퍼)
		TArray<uint8> BufferArray;
	    //버퍼와 연동되는 메모리Writer
		FMemoryWriter MemoryWriterAr(BufferArray);
		//이렇게 하면 메모리의 BufferArray에는 Student오브젝트의 내용이 저장
		StudentSrc->Serialize(MemoryWriterAr);
		
		//이것을 파일로 저장 (아까 구조체에서는 delete , nullptr 을 지정해줫는데 이번에는 스마트 포인터 사용)
		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 MemoeryReaderAr(BufferArrayFromFile);
		UStudent* StudentDest = NewObject<UStudent>();
		StudentDest->Serialize(MemoeryReaderAr);

		PrintStudentInfo(StudentDest, TEXT("ObjectData"));

		
	}

	{
		const FString JsonDataFileName(TEXT("StudentJsonData.text"));
		FString JsonDataAbsolutePath = FPaths::Combine(*SaveDir, *JsonDataFileName);
		FPaths::CreateStandardFilename(JsonDataAbsolutePath);

		//언리얼 오브젝트 -> Json 오브젝트로 바꿔줘야한다. ( 헤더 선언 필요 : JsonObjectConverter.h
		
		//레퍼런스를 편하게 사용할 수 있게 공유 레퍼런스 설정
		//MakeShared 하는 이유 : 공유 레퍼런스는 NotNull을 보장해야하기에 객체를 만들어줌
		TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
		FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);

		FString JsonOutString;
		TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
		if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
		{
			FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
		}

		//다시 파일을 불러들이는 작업
		FString JsonInString;
		FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);

		TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);

		//여기서 문자열에 이상한 값이 들어와서 오브젝트가 안만들어질수도 있다. 널이 들어갈수있는 Ptr 포인터로 선언
		TSharedPtr<FJsonObject> JsonObjectDest;
		if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
		{
			UStudent* JsonStudentDest = NewObject<UStudent>();
			if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
			{
				PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
			}
		}
	}

	//패키지를 사용한 에셋 저장
	SaveStudentPackage();
	LoadStudentPackage();
}

void UMyGameInstance::SaveStudentPackage() const
{
	//이미 패키지가 존재한다면 다 로딩하고 저장해주는게 좋다.
	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
	if (StudentPackage)
	{
		StudentPackage->FullyLoad();
	}

	//패키지를 사용하기위하여 패키지와 패키지를 담고있는 대표 에셋을 지정
	
	//1. 패키지 생성코드
	StudentPackage = CreatePackage(*PackageName);
	//2. 패키지 저장 옵션
	EObjectFlags ObjectFlag = RF_Public | RF_Standalone;

	//3.패키지에 어떤 내용을 담을지 
	UStudent* TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(),*AssetName, ObjectFlag);
	TopStudent->SetName(TEXT("이승한"));
	TopStudent->SetOrder(35);

	const int32 NumfSubs = 10;
	for (int32 ix = 1; ix <= NumfSubs; ++ix)
	{
		FString SubObjectName = FString::Printf(TEXT("Student%d"), ix);
		UStudent* SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
		SubStudent->SetName(FString::Printf(TEXT("학생%d"), ix));
		SubStudent->SetOrder(ix);
	}

	//4. 패키지에 저장될 경로를 지정
	//5. 패키지에 확장자 부여
	//FPackageName::GetAssetPackageExtension() : 언리얼 엔진에서 지정한 uasset이라는 확장자를 의미
	//우리가 앞서 지정한 PackageName ("/Game/Student") 를 바탕으로 현재 프로젝트의 Content폴더(현재는 Game)를 지정하게 하고 그 다음에 패키지의 Student가 파일 이름이 된다.
	const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());

	//6. 저장
	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();

	//로딩된 패키지에서 AssetName을 찾아줘서 TopStudent에 저장
	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
}

애셋

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

게임 제작 단계에서 애셋 간의 연결 작업을 위해 직접 패키지를 불러 할당하는 작업은 부하가 크다.

  • 애셋 로딩 대신 패키지와 오브젝트를 지정한 문자열을 대체해 사용. 이를 오브젝트 경로라 한다.
  • 프로젝트 내에 오브젝트 경로 값은 유일함을 보장
  • 그렇기에 오브젝트 간의 연결은 오브젝트 경로 값으로 기록될 수 있다.
  • 오브젝트 경로를 사용해 다양한 방법으로 애셋을 로딩할 수 있다.

애셋의 로딩 전략

  • 프로젝트에서 애셋이반드시 필요한 경우: 생성자 코드에서 미리 로딩
  • 런타임에서 필요할 때 바로 로딩하는 경우 : 런타임 로직에서 정적 로딩
  • 런타임에서 비동기적으로 로딩하는 경우 : 런타임 로직에서관리자를 사용해 비동기 로딩

오브젝트 경로(Object Path)

{애셋클래스정보}'{패키지이름}.{애셋이름}'

{패키지이름}.{애셋이름}

  • 패키지 이름과 애셋 이름을 한 데 묶은 문자열
  • 애셋 클래스 정보는 생략할 수 있다.
  • 패키지 내 데이터를 모두 로드하지 않고 오브젝트 경로를 사용해 필요한 애셋만 로드 할 수있다.

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

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

애셋을 동기,비동기 로딩하는 방법에 대해 살펴 보자.

1. 동기 로딩 : 애셋 함수를 통해 불러오기

//MyGameInstance.h 에서 함수 선언
void LoadStudentObject() const; 

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

	//이때 패키지를 로딩하지않기에 nullptr 지정
	UStudent* TopStudent = LoadObject<UStudent>(nullptr, *TopSoftObjectPath);
	PrintStudentInfo(TopStudent, TEXT("LoadObject Asset"));
}

출력 값:
LogTemp: [LoadObject Asset] 이름 이승한 순번 35

2. 생성자에서 애셋 불러오기 -> 생성자에서 애셋을 불러올때 애셋은 반드시 있다는 가정하에 실행된다. ( 없으면 에러)

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 Asset"));
	}
}

출력 값:
LogTemp: [Constructor Asset] 이름 이승한 순번 35
LogTemp: [Constructor Asset] 이름 이승한 순번 35

2번찍히는 이유:
첫번째는 에디터가 로딩될 때 찍힘
두번째는 게임이 실행될 때 찍힘

3. 비동기 로딩
h

//MyGameInstance.h 헤더파일에서 선언
#include "Engine/StreamableManager.h" 

//선언
private:
	//스트리밍 매니저 선언
	FStreamableManager StreamableManager;
    //스트리밍된 애셋을 관리할수있는 핸들 선언
	TSharedPtr<FStreamableHandle> Handle;

cpp

//비동기 방식

//애셋의 오브젝트 경로
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();
			}
		}
	}
);

출력 값:
LogTemp: [AsyncLoad] 이름 이승한 순번 35

0개의 댓글