언리얼 오브젝트 관리(2) - 패키지

myeongrangcoding·2023년 12월 2일

언리얼 오브젝트 패키지

  • 단일 언리얼 오브젝트가 가진 정보는 저장할 수 있지만, 오브젝트들이 조합되어 있다면?
    • 저장된 언리얼 오브젝트 데이터를 효과적으로 찾고 관리하는 방법은?
    • 복잡한 계층 구조를 가진 언리얼 오브젝트를 효과적으로 저장과 불러들이는 방법을 통일해야 함.
  • 언리얼 엔진은 패키지(UPackage)단위로 언리얼 오브젝트를 관리함.
  • 언리얼 오브젝트 패키지: 언리얼 오브젝트를 감싼 포장 오브젝트를 의미함.

패키지와 애셋

  • 애셋: 언리얼 오브젝트 패키지의 서브 오브젝트, 에디터에는 이들이 노출됨
  • 패키지는 일반적으로는 하나의 애셋만 가짐.
  • 애셋은 다시 다수의 서브오브젝트를 가질 수 있으며, 모두 언리얼 오브젝트 패키지에 포함되지만 에디터에는 노출되지 않음.
  • 직렬화 클래스를 사용해서 수동으로 언리얼 오브젝트 정보를 저장하는 것(지난번)이 아니고 패키지를 사용해서 언리얼 에디터에서 볼 수 있는 애셋을 저장해 보도록 함.

// 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("Student");	
// 5.3버전에서는 PackageName과 AssetName을 동일하게 사용해야 콘텐츠 브라우져에서 보임.

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();

	SaveStudentPackage();
	LoadStudentPackage();
}

void UMyGameInstance::SaveStudentPackage() 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 ix = 1; ix <= NumofSubs; ++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);
	}

	const FString PackageFileName = FPackageName::LongPackageNameToFilename(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, Log, TEXT("패키지를 찾을 수 없습니다."));
		return;
	}

	StudentPackage->FullyLoad();

	// 패키지 안에서 UStudent 타입의 언리얼 오브젝트를 찾아서 반환을 시켜준다.
	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
}

애셋 참조와 로딩

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

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

오브젝트 경로(Object Path)

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

  • 이런 오브젝트 경로를 우리가 안다면 패키지 내에 있는 모든 애셋을 로드하지 않고 필요한 애셋만 개별적으로 로드를 할 수 있음.

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

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

UMyGameInstance::UMyGameInstance()
{
	// 생성자에서 애셋을 로딩하는 경우는 게임이 시작하기 전에 미리 다 메모리에 올라와 있어야 된다는 것을 의미.
	// 생성자에서 애셋을 로드할 때는 LoadObject를 쓰는 것이 아니고 언리얼에서 제공하는 컨스트럭터 핼퍼스 사용을 해줘야 한다.
	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
	static ConstructorHelpers::FObjectFinder<UStudent> UASSET_TopStudent(*TopSoftObjectPath);

	// Succeeded(): 로딩이 제대로 됐는지를 확인할 수 있는 함수.
	if (UASSET_TopStudent.Succeeded())
	{
		PrintStudentInfo(UASSET_TopStudent.Object, TEXT("Constructor"));
	}

	/*
	LogTemp: [Constructor] 이름 명랑코딩 순번 36
	LogTemp: [Constructor] 이름 명랑코딩 순번 36
	
	로그가 두 번 찍힌 것을 볼 수가 있다.
	첫 번째는 에디터가 로딩될 때 찍혔다.
	두 번째는 에디터 내 게임이 실행할 때 컨스트럭터에 관련된 함수들이 자동으로 호출되기 때문에 두 번 찍히게 된다.
	
	생성자 코드에서 지정한 애셋이 존재하지 않는 경우에는 시작할 때 경고와 에러 메시지가 뜬다.
	*/
}
void UMyGameInstance::Init()
{
	Super::Init();

	SaveStudentPackage();
	//LoadStudentPackage();
	LoadStudentObject();
}

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

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

#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 LoadStudentObject() const;

private:
	// 패키지를 사용하기 위해서는 패키지와 패키지가 담고 있는 대표 asset을 설정해줘야 된다.
	// 이것들의 이름을 지정.
	static const FString PackageName;
	static const FString AssetName;

	UPROPERTY()
	TObjectPtr<class UStudent> StudentSrc;

	// 비동기 로딩.
	FStreamableManager StreamableManager;
	// 스트리밍된 asset을 관리할 수 있는 핸들 지정.
	TSharedPtr<FStreamableHandle> Handle;
};
void UMyGameInstance::Init()
{
	Super::Init();

	SaveStudentPackage();
	//LoadStudentPackage();
	//LoadStudentObject();

	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();
				}
			}
		}
	);
}
profile
명랑코딩!

0개의 댓글