[Unreal] 이득우 Part 1. 14강 언리얼 오브젝트 관리 II - 패키지

Kim Dongil·2024년 3월 19일
0

언리얼엔진

목록 보기
17/18

언리얼 오브젝트 패키지

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

패키지와 에셋

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

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

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

오브젝트 경로

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

에셋 참조

언리얼 엔진 에셋 참조

직접 프로퍼티 참조

  • 타입을 명시적으로 지정

생성 시간 참조

  • 강 참조를 진행할 때 해당 오브젝트가 가리키고 있는 에셋을 생성자 코드에서 생성
    • 생성자 코드는 엔진이 초기화될 때 실행
    • 즉, 게임이 실행되기 전에 해당 에셋이 로딩

간접 프로퍼티 참조

  • TSoftObjectPtr 을 사용
  • LoadObject<>() 메서드나 StaticLoadObject()나FStreamingManager 를 사용하여 오브젝트를 로드할 수 있음
  • FSoftObjectPath 가 오브젝트 경로를 의미
    • 이 값을 이용해 스트리밍 매니저를 사용해 비동기적이나 동기적으로 에셋을 로딩

오브젝트 검색/로드

  • 생성 또는 로드된 오브젝트: FindObject
  • 로드되지 않은 오브젝트: LoadObject

에셋 스트리밍 관리자

  • 에셋의 비동기 로딩을 지원하는 관리자 객체
  • 콘텐츠 제작과 무관한 싱글턴 클래스에 FStreamableManager를 선언해두면 좋음.
    • GameInstance는 좋은 선택지
  • 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 를 이용
    • 오브젝트 경로와 람다 함수 이용

0개의 댓글