언리얼 패키지와 애셋

Lee Raccoon·2024년 7월 25일
0

언리얼 공부

목록 보기
10/11

패키지

언리얼에서 패키징은 다양한 의미를 갖는다.

  • 언리얼 오브젝트를 감싼 포장 오브젝트
  • 개발된 최종 콘텐츠를 정리해 프로그램으로 만드는 작업
  • DLC 혹은 향후 확장 콘텐츠에 사용되는 별도의 데이터

등의 의미를 갖는다.

이번에 알아본 것은 1번, 언리얼 오브젝트를 감싸는 오브젝트로써의 패키지이다.

사실 지금까지 써온 모든 언리얼 오브젝트들은 Transient라는 패키지에 속해있다.

패키지에는 애셋이라는 서브 오브젝트가 존재하고 그 하위에 또 다양한 서브 오브젝트들이 있다.
언리얼 에디터에 노출되는 친구들은 이 애셋이라는 친구들이다.

일반적으로 한 패키지에는 한 애셋이 존재한다고 한다.

패키지 저장과 로드

패키지 저장

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

	//애셋의 서브 오브젝트를 생성
	const int32 NumofSubs = 10;
	for (int32 ix = 1; ix <= NumofSubs; ++ix)
	{
		FString SubObjectName = FString::Printf(TEXT("Strudent%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, Display, TEXT("패키지가 성공적으로 저장되었습니다."));
	}
}


오. 애셋이 생성되었다.
이제 이 애셋을 한번 로드 해보자

패키지 로딩

void UMyGameInstance::LoadStudentPackage() const
{	
	//패키지 로드
	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
	if (nullptr == StudentPackage)
	{
		UE_LOG(LogTemp, Display, TEXT("패키지를 찾을 수 없습니다."));
		return;
	}

	StudentPackage->FullyLoad();
	
    //패키지에서 애셋을 찾아 TopStudent에 저장 후 출력 해보기
	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
	PrintStudentInfo(TopStudent, TEXT("FindObject"));
}

잘 되는 것을 보니 기분이 좋다.

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

에디터에서 애셋 간의 연결 작업을 할 일이 많은데 이럴 때마다 직접 패키지를 불러서 할당하기에는 부하가 크다.

그래서 무조건 로딩을 한다기 보다는 일단 해당 애셋이나 패키지의 경로를 갖고 있다고 생각하면 편하다. (오브젝트 경로)

오브젝트 경로는 프로젝트 내에서 유일한 값이기에 키 값으로 쓰일 수 있게 된다.
->오브젝트 경로로 연결을 가능하게 한다.
오브젝트 경로: {패키지 이름}.{애셋이름}

로딩할 경우에는 크게 세가지가 있다.

  • 프로젝트에서 애셋이 반드시 필요 -> 엔진이 초기화될 때, 생성자 코드에서 미리 로딩
  • 런타임에서 필요한 때 로딩 -> 런타임 로직에서 정적 로딩
    (이 때는 다른 프로세스의 실행을 막아 게임이 멈춤)
  • 런타임에서 비동기적으로 로딩 -> 런타임 로직에서 관리자를 사용해 비동기 로딩

언리얼 엔진 애셋 참조

공식 문서에서 자세히 알아볼 수 있다.
문서에서는 크게 4가지로 나누고 있다.

직접 프로퍼티 참조

가장 기본적인 경우이며 UPROPERTY 매크로를 통해 노출되는 프로퍼티이다.

생성 시간 참조

로드 해야할 애셋이 명확한 경우에 그 프로퍼티를 오브젝트 생성자에서 설정해주는 경우이다.
ConstructorHelpers 같은 특수 클래스가 사용된다.
해당 애셋을 찾을 수 없는 경우에는 nullptr이 반환되고 이를 참조하려 한다면 크래시가 뜨기 때문에 조심해야한다.

간접 프로퍼티 참조

애셋 로드 시점을 쉽게 제어할 수 있는 방법이다.
TSoftObjectPtr를 사용하고 LoadObject<>()이나 StaticLoadObject(),FStreamingManager를 사용해서 오브젝트를 로드할 수 있다.
LoadObject같은 경우에는 애셋을 동기식으로 로드한다. 그러므로 호출 시점에 따라 프레임이 출렁일 수 있으니 사용 시 조심해야한다.
FStreamingManager는 비동기 로딩을 지원한다.

오브젝트 검색/로드

이미 생성, 로드 된 UObject를 사용하는 경우에 FindObject<>()
아직 로드되지 않은 오브젝트를 로드하려면 LoadObject<>()를 사용한다.

오브젝트 로드 테스트

패키지로써 저장하고 로드하는 것은 해보았으니 이번에는 LoadObject로 오브젝트를 불러와보자.

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

그 다음 이 함수를 실행시켜 보면?

성공!

생성 시간 참조 테스트

이번엔 생성자에서 한번 로딩을 해보자.
그렇다면 이 애셋은 게임이 시작하기 전에 이미 메모리에 올라와있다는 것을 뜻할 것이다.

생성자에서 애셋을 로딩할 때는 LoadObject를 사용하는 것이 아니라.
ConstructorHelpers라는 클래스를 사용하게 된다.

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

한번 실행해보면,

Constructor 태그가 붙은 로그가 제일 먼저 찍혀있는 것을 볼 수 있다.
당연하다. 생성자에 있는 함수였으니까

근데 왜 두번 실행이 되는가?가 문제인데

이는 에디터가 로딩될 때 생성자들을 모두 호출하면서 찍힌 것이고
두번째는 에디터 내에 게임이 실행될 때 또 생성자에 관련된 함수들이 자동으로 호출되므로 두 번 찍히게 된다.

생성자 코드에서 애셋을 로딩할 경우 해당 애셋이 반드시 존재해야하며
존재하지 않는다면 무시무시한 일이..

비동기 로딩 테스트

이번에는 비동기로딩이다. FStreamableManager라는 클래스와 FStreamableHandle을 사용하게 된다.

//헤더
#include "Engine/StreamableManager.h"

FStreamableManager StreamableManager;
TSharedPtr<FStreamableHandle> Handle;

//구현부
void UMyGameInstance::LoadStudnetAsync()
{
	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();
				}
			}
		});
}

이를 실행해보면?

야호~

마무리

이렇게 애셋을 저장하고 로딩하는 다양한 방법에 대해 알아보았다.

사실 프로젝트를 진행하며 이미 계속 쓰고 있는 방법이지만 ConstructorHelpersLoadObject가 뭐가 다른건지는 정확히 알고 쓰지는 않아서 이제 보니 큰일이 날 뻔 했다.

사실 데모 패키징할 때 게임에서 fatal error의 등장에 정신이 나가버릴 뻔 하기도 했지만..

애셋 저장과 로딩에 대해 몰랐다면 이제라도 제대로 알고 넘어가는 것이 좋지 않을까!

profile
영차 영차

0개의 댓글