메모내용
Nav

언리얼 오브젝트 패키지

UObject 참조

PackageAsset
Create
PackageAsset
Reference
PackageAsset
Load
NewObject & SavePackage
PackageName.AssetName
TObjectPtr
TSoftObjectPtr
생성자
ConstructorHelpers::FObjectFinder
런타임
LoadPackage & FindObject | LoadObject
FStreamableManager::SynchronousLoad
FStreamableManager::RequestAsyncLoad
                        
/* UObject 생성 */
{
    UMyClass* MyObject = NewObject<UMyClass>();
}

/* 이미 메모리 상에 할당된 경우 */
{
    UMyClass* MyObject = FindObject<UMyClass>(MyPackageName, MyAssetName);
}

/* 메모리 상에 할당되지 않은 경우 
    내부적으로 FindObject 과 같은 역할을 하므로, 이것만 써도 문제없다.
*/
{
    UMyClass* MyObject = LoadObject<UMyClass>(NULL,TEXT("PackageName.AssetName")/*, NULL, LOAD_None, NULL*/);
}

/* 생성자단계 로드
    생성자에서 에셋을 로딩하는 경우, 게임이 시작되기전에 미리 다 메모리에 올라와야 있어야 함을 의미한다.
    반드시 있다는 전제하에 수행되어야 한다. 없다면, 에디터 초기화시 강력한 에러가 발생하게 된다. ("Error : CDO Constructor")
    해당 생성자코드는 에디터 초기화시와, 게임시작시 총 2번 호출된다.
*/
{
    const FString MyObjectPath = FString::Printf(TEXT("%s,%s"), TEXT("PackageName"), TEXT("AssetName"));
    static ConstructorHelpers::FObjectFinder<UMyClass> MyObject_Loaded(*MyObjectPath);
    if(MyObject_Loaded.Succeeded())
    {
        UMyClass* MyObject = MyObject_Loaded.Object;
    }
}

/* 비동기 로드 */
{
    // .h
    #include "Engine/StreamableManager.h"

    FStreamableManager* StreamableManager;
    TSharedPtr<FStreamableHandle> Handle; // 스트리밍된 에셋을 관리할 수 있는 핸들 

    //.cpp
    const FString MyObjectPath = FString::Printf(TEXT("%s,%s"), TEXT("PackageName"), TEXT("AssetName"));
    Handle = StreamableManager.RequestAsyncLoad(MyObjectPath,
        [&](){
            if(Handle.IsValid() && Handle->HasLoadCompleted())
            {
                UMyClass* MyObject = Cast<UMyClass>(Handle->GetLoadedAsset());
                if(MyObject)
                {
                    // 비동기 로드 완료

                    Handle->ReleaseHandle(); //사용한 핸들 닫기
                    Handle.Reset();
                }
            }
        }
    )
}



                        
                    

UObject 쓰기, 읽기, 찾기

UObject 생성 NewObject to Package
UObject 로드 LoadObject in Package
UObject 찾기 FindObject in Package
in Asset

언리얼 오브젝트 패키지

하위 언리얼 오브젝트가 있는, 언리얼 오브젝트의 관리

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

패키지(Package) 와 에셋(Asset)

  • 언리얼 오브젝트 패키지는 다수의 언리얼 오브젝트를 포장하는데 사용하는 언리얼 오브젝트
    • 모든 언리얼 오브젝트는 패키지에 소속되어 있다. ( 예 - Transient Package : 임시 패키지 )
  • 언리얼 오브젝트 패키지의 Subobject를 Asset 이라고 하며, 에디터에는 이들이 노출된다.
    • 에디터에는 패키지 정보가 노출되는 것이 아니다.
    • Asset 이 노출된다.
    • Asset 은 여러 Subobject 를 가질 수 있으나, Subobject 들은 노출되지 않는다.
    • 이들은 모두 언리얼 오브젝트 패키지에 포함된다.
  • 구조상 패키지는 다수의 언리얼 오브젝트를 소유할 수 있으나, 일반적으로 하나의 에셋만 가진다.
    • 패키지에는 여러개의 에셋을 가질 수 있다.
    • 일반적으로 하나의 에셋을 가진다.
  • 수동으로 언리얼 직렬화 클래스를 사용하여 파일,메모리 에 언리얼 오브젝트 데이터를 저장할 수 도 있지만,
    패키지를 사용해서 언리얼 에디터에서 볼 수 있는 에셋을 만드는 것이다.

패키지 만들기

패키지를 만들기 위해서는 패키지와, 패키지가 담을 대표 에셋을 설정해 주어야 한다. 그리고 이것들의 이름을 지정해주어야 한다.

간단 요약

                        
    #include "UObject/Package.h"
    #include "UObject/SavePackage.h"

    {
		
		FString PackageName = TEXT("/Game/AssetPackage_2");
		FString AssetName = TEXT("Asset_A");
		FString FilePath = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());	// FilePath : ../../../../../UnrealProject/JumpGame/CPPTEST/CPPTEST/Content/AssetPackage_2.uasset	
		EObjectFlags ObjectDefaultFlag = RF_Public | RF_Standalone; // ObjectFlag 는 Package 저장시, UObject 생성시에 쓰인다.
		FSavePackageArgs SaveArgs;
		SaveArgs.TopLevelFlags = ObjectDefaultFlag;

		// 패키지 이미 있다면 
		UPackage* SavePack = LoadPackage(nullptr, *FilePath, LOAD_None); //LoadPackage()
		
		// 기존 패키지 로드
		if (SavePack)
		{
			SavePack->FullyLoad();
		}
		
		// 패키지 생성
		SavePack = CreatePackage(*PackageName);

		// 오브젝트 설정
		{
			UMyObject* ObjForSave = NewObject<UMyObject>(SavePack, *AssetName, ObjectDefaultFlag);

			ObjForSave->intParam = 25;
			ObjForSave->StrParam = TEXT("#he231lldoekdll3142lde21jnkskk");
		}

		// 패키지 저장
		UPackage::SavePackage(SavePack, nullptr, *FilePath, SaveArgs);

		UE_LOG(LogTemp, Warning, TEXT("FilePath : %s"), *FilePath);


		// 패키지 로드 ( FilePath 를 사용해도 된다. )
		UPackage* LoadPack = LoadPackage(nullptr, *FilePath, LOAD_None);
		if (LoadPack)
		{
			LoadPack->FullyLoad();

			// 오브젝트 찾기
			{
				UMyObject* FoundObj = FindObject<UMyObject>(LoadPack, *AssetName);

				if (FoundObj)
				{
					UE_LOG(LogTemp, Warning, TEXT("Obj : {%d , %s}"), FoundObj->intParam, *FoundObj->StrParam);
				}
				else {
					UE_LOG(LogTemp, Warning, TEXT("not Found Object in Package"));
				}
			}
		}
	}
                        
                    

원본

                        
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
 
/*
/Temp : Saved 폴더에 매핑
/Game : Content 폴더에 매핑
*/
const FString PackageName = TEXT("/Game/MyPackage"); // 패키지 이름 설정 (폴더에 저장될 패키지의 이름이 된다. /Game/MyPackage.uasset )
const FString PackageAssetName = TEXT("PackageTargetClass_AssetName"); // 패키지 대표 에셋 이름 설정 ( 에디터에 표시될 에셋의 이름이 된다 . /Script/UECPP_Tutorial.PackageTargetClass'/Game/MyPackage.PackageTargetClass_AssetName' )

void UMyGameInstance::SavePackage()
{
// 0. 패키지를 미리 로드하기 : 이미 존재한다면, 로드먼저 하고 추가해 주는것이 안전하다.
// 로드를 먼저 안해주면 에러발생 할 수 있다 : cannot be saved as it has only been partially loaded
UPackage* MyPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
if (MyPackage)
{
    MyPackage->FullyLoad();
}

// 1. Package 생성
MyPackage = ::CreatePackage(*PackageName);
EObjectFlags ObjectDefaultFlag = RF_Public | RF_Standalone; // UPackage 저장 기본 옵션

// 2. Package 의 대표 에셋 생성및 설정 ( Package 에 새로 성성한 UObject 를 Asset으로 넣어준다. )
UPackageTargetClass* UObject_ForMainAsset = NewObject<UPackageTargetClass>(/*패키지*/MyPackage, /*메타클래스*/UPackageTargetClass::StaticClass(), /*에셋이름*/*PackageAssetName, /*옵션플래그*/ObjectDefaultFlag);
UObject_ForMainAsset->SetStrParam(TEXT("Main Asset"));
UObject_ForMainAsset->SetNumParam(0);

// 3. Subobject 를 Asset 에다가 추가
for (int i = 0; i < 10; i++)
{
    // MainAsset 의 하위로 들어갈 Subobject 의 이름 생성, 추가
    FString SubobjectName = FString::Printf(TEXT("Subobject[%d]"), i);
    UPackageTargetClass* UObject_ForSubobject = NewObject<UPackageTargetClass>(/*상위에셋*/UObject_ForMainAsset, /*메타클래스*/UPackageTargetClass::StaticClass(), /*에셋이름*/*SubobjectName, /*옵션플래그*/ObjectDefaultFlag);
    UObject_ForSubobject->SetStrParam(SubobjectName);
    UObject_ForSubobject->SetNumParam(i);
}

// 4. Package 저장 : 저장하기 위해서는 패키지의 경로와, 패키지의 확장자를 지정해주어야 한다.
const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = ObjectDefaultFlag;

if (UPackage::SavePackage(MyPackage, nullptr, *PackageFileName, SaveArgs))
{
    UE_LOG(LogTemp, Log, TEXT("Package(%s) 가 성공적으로 저장되었습니다."), *PackageFileName);
}
}

void UMyGameInstance::LoadPackage()
{
// 1. Package 로드
UPackage* MyPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
if (nullptr == MyPackage)
{
    UE_LOG(LogTemp, Warning, TEXT("Package(%s) 를 읽는데 실패하였습니다."), *PackageName);
}

// 2. 내부 모든 Asset을 로드해줍니다.
MyPackage->FullyLoad(); 

// 3. 에셋 찾기
UPackageTargetClass* FoundAsset = FindObject<UPackageTargetClass>(MyPackage, *PackageAssetName);
if (nullptr == FoundAsset) {
    UE_LOG(LogTemp, Warning, TEXT("Package(%s) 에는 Asset (%s) 이 없습니다."), *PackageName, *PackageAssetName);
}
UE_LOG(LogTemp, Log, TEXT("Package(%s) 로부터 찾은 Asset. UPackageTargetClass { NumParam : %d, StrParam : %s}"),*PackageName, FoundAsset->GetNumParam(), *FoundAsset->GetStrParam());

// 4. 서브오브젝트 찾기
/*
    서브오브젝트는 
    Asset 에서 이름으로 FindObject 로 찾거나
    Asset 에서 GetObjectsWithOuter 로 모든 Subobject 를 찾아서, 걸러서 찾거나 해야한다.
    
    DefaultSubobject 로는 찾을수 없다.
*/
{
    // 4-1 : FindObject 로 찾기
    {
        for (int i = 0; i < 10; i++)
        {
    
            FString SubobjectNameForFind = FString::Printf(TEXT("Subobject[%d]"), i);
            UPackageTargetClass* FoundSubobject = FindObject<UPackageTargetClass>(/*상위오브젝트(에셋)*/FoundAsset, *SubobjectNameForFind); // 서브오브젝트는 Package 안이 아니라, Asset 안에서 찾아야 한다.
            if (nullptr != FoundSubobject)
            {
                UE_LOG(LogTemp, Log, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass : {NumParam : %d, StrParam : %s} 을 찾았습니다 "), *FoundAsset->GetName(), FoundSubobject->GetNumParam(), *FoundSubobject->GetStrParam());
            }
            else {
                UE_LOG(LogTemp, Warning, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass %s 을 찾을 수 없었습니다."), *FoundAsset->GetName(), *SubobjectNameForFind);
            }
        }
    }
    
    // 4-2 : Asset 에서 이름으로 찾기 -> 실패 : DefaultSubobject 로는 찾을수가 없다.
    {
        for (int i = 0; i < 10; i++)
        {
            FString SubobjectNameForFind = FString::Printf(TEXT("Subobject[%d]"), i);
            UObject* FoundSubobject = FoundAsset->GetDefaultSubobjectByName(*SubobjectNameForFind);
            if (nullptr != FoundSubobject)
            {
                UPackageTargetClass* FoundSubobject_PackageTarget = Cast<UPackageTargetClass>(FoundSubobject);
                UE_LOG(LogTemp, Log, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass : {NumParam : %d, StrParam : %s} 을 찾았습니다 "), *FoundAsset->GetName(), FoundSubobject_PackageTarget->GetNumParam(), *FoundSubobject_PackageTarget->GetStrParam());
            }
            else {
                UE_LOG(LogTemp, Warning, TEXT("DefaultSubobject 를 MainAsset 에서 Asset (%s) 를 찾지 못했습니다."), *SubobjectNameForFind);
            }
        }
    }

    // 4-3 : GetDefaultSubobjects 로 모든 DefaultSubobject 찾기 -> 실패 : DefaultSubobject 로는 찾을수가 없다.
    {
        TArray<UObject*> FoundSubobjects;
        FoundAsset->GetDefaultSubobjects(FoundSubobjects); // 이게 개수가 0 이라면 , Subobject 는 DefaultSubobject 와는 다르다는 이야기 이다.
        if (FoundSubobjects.Num() > 0)
        {
            UE_LOG(LogTemp, Log, TEXT("찾은 서브오브젝트 개수 : %d, FoundSubobjects : %s"), FoundSubobjects.Num(), *FoundSubobjects[0]->GetName());
        }
        else {
            UE_LOG(LogTemp, Warning, TEXT("Asset 안에는 DefaultSubobject 가 없습니다."));
        }
    }
    // 4-4 : GetObjectsWithOuter 로 모든 Subobject 찾기 
    {
        TArray<UObject*> FoundSubobjects;
        GetObjectsWithOuter(FoundAsset, FoundSubobjects);
        UE_LOG(LogTemp, Log, TEXT("찾은 서브오브젝트 개수 : %d"), FoundSubobjects.Num());

        for (int i = 0; i < 10; i++)
        {
            UPackageTargetClass* FoundSubobject_Casted = Cast<UPackageTargetClass>(FoundSubobjects[i]);
            if (nullptr != FoundSubobject_Casted)
            {
                UE_LOG(LogTemp, Log, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass : {NumParam : %d, StrParam : %s} 을 찾았습니다 "), *FoundAsset->GetName(), FoundSubobject_Casted->GetNumParam(), *FoundSubobject_Casted->GetStrParam());
            }
        }
    }
}
}
                        
                    

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

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

Object Path (오브젝트 경로)

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

Asset Reference ( 오브젝트 참조 )

Asset 참조에는 Strong Reference 와, Soft Reference 가 있다. Strong Reference 는 Object A 가 Object B 를 참조하여 Object A 로드시 Object B 를 로드되는경우. Soft Reerence 는 경로 같은 문자열 형태의 간접 매커니즘을 통해, Object A 가 Object B 를 참조하게 만드는 경우.

StrongReference 1 : 직접 프로퍼티 참조

헤더파일에서 직접 멤버변수로 참조하고 있는 경우

                            
class UCLASS_B;

UCLASS()
class PROJECT_API UCLASS_A{
    GENERATED_BODY()
public:

    UPROPERTY()
    UCLASS_B* Object_B; // 강참조 :: 직접 프로퍼티 참조
}
                            
                        

StrongReference 2 : 생성자에서 참조

생성자 코드는 엔진이 초기화 될때 실행된다. 즉 게임이 실행되기 전에 해당 에셋이 로딩이 된다. ConstructorHelpers 라는 특수 클래스가 사용되는데, 생성 단계 도중 오브젝트와 , 오브젝트의 클래스를 찾을 수 있다.

                            
class UCLASS_B;

UCLASS()
class PROJECT_API UCLASS_A{
    GENERATED_BODY()
public:
    
    // 생성자 참조
    CLASS_A()
    {
        static ConstructorHelpers::FObjectFinder<UCLASS_B> Found_Object_B(TEXT("/Game/PackageName/AssetName"));

        UCLASS_B* Object_B = Found_Object_B.Object;
    }
}                       
                            
                        

SoftReference 1 : 간접 프로퍼티 참조

TSoftObjectPtr 을 사용하는 경우. 직접 프로퍼티 레퍼런스 인 것처럼 작업할 수 있다. 이때는 유효한지 검사를 통해 사용해야 합니다. TSoftObjectPtr 을 사용하려면, 에셋을 수동으로 로드 해야 합니다. 템플림함수 LoadObject<>() StaticLoadObject() FStreamingManager 를 사용해서 오브젝트를 로드할 수 있습니다.

                            
class UCLASS_B;

UCLASS()
class PROJECT_API UCLASS_A{
    GENERATED_BODY()
public:
    UPROPERTY()
    TSoftObjectPtr<UCLASS_B> Object_B; // 약참조 :: 오브젝트 경로를 사용한 참조

    // 오브젝트를 Load 하고 가져옵니다.
    UCLASS_B* GetObjectWithLoad();
}

// in cpp
void UCLASS_A::GetObjectWithLoad()
{
    if(Object_B.IsPending())
    {
        const TSoftObjectPath& AssetRef = Object_B.ToStringReference(); // Object Path 가져오기
        FStreamableManager& Streamable = UGameGlobals::Get().StreamableManager;
        Object_B = Cast<UCLASS_B>(Streamable.SynchronousLoad(AssetRef));
    }
    return Object_B.Get();
}

                            
                        

TObjectPtr

  • TObjectPtr : (Strong Reference) Header 파일에서는 UObject 에 대한 참조는 * 가 아닌 TObjectPtr 을 사용하도록 권장하고 있다.
  • TSoftObjectPtr : (Soft Reference) : UObject 에 대한 약한 참조
    • TSoftObjectPtr :: Get() : 참조된 에셋이 메모리에 존재한다면, Valid 한 값을 반환합니다.
    • TSoftObjectPtr :: IsValid()
    • TSoftObjectPtr :: IsNull()
    • TSoftObjectPtr :: IsPending()
    • TSoftObjectPtr :: ToStringReference() : 오브젝트의 경로를 반환합니다.
    • TSoftObjectPtr :: GetPackageName()
    • TSoftObjectPtr :: GetAssetName()
  • FSoftObjectPtr : TSoftObjectPtr 이 변수로 가지고 있다.
  • FSoftObjectPaths : 에셋의 전체 경로로 된 스트링이 있는 단순한 구조체, 이 Type 의 PROPERTY 를 만들면 에디터에는 마치 UObject* PROEPRTY 처럼 표시합니다.
    • FSoftObjectPaths :: IsValid()
    • FSoftObjectPaths :: IsNull()
                        
/* Persistent : 지속성 있는 */
template<class TObjectID>
struct TPersistentObjectPtr
{

	/**
	 * Dereference the pointer, which may cause it to become valid again. Will not try to load pending outside of game thread
	 * @return nullptr if this object is gone or the pointer was null, otherwise a valid UObject pointer
	 */
	FORCEINLINE UObject* Get() const
	{
		UObject* Object = WeakPtr.Get();
		
		// Do a full resolve if the returned object is null and either we think we've loaded new objects, or the weak ptr may be stale
		if (!Object && ObjectID.IsValid() && (TObjectID::GetCurrentTag() != TagAtLastTest || !WeakPtr.IsExplicitlyNull()))
		{
			Object = ObjectID.ResolveObject();
			WeakPtr = Object;

			// Not safe to update tag during save as ResolveObject may have failed accidentally
			if (Object || !GIsSavingPackage)
			{
				TagAtLastTest = TObjectID::GetCurrentTag();
			}

			// If this object is pending kill or otherwise invalid, this will return nullptr as expected
			Object = WeakPtr.Get();
		}
		return Object;
	}
    
    /**  
	 * Test if this does not point to a live UObject, but may in the future
	 * @return true if this does not point to a real object, but could possibly
	 */
	FORCEINLINE bool IsPending() const
	{
		return Get() == nullptr && ObjectID.IsValid();
	}

	/**  
	 * Test if this points to a live UObject
	 * @return true if Get() would return a valid non-null pointer
	 */
	FORCEINLINE bool IsValid() const
	{
		return !!Get();
	}
    
    /**  
	 * Test if this can never point to a live UObject
	 * @return true if this is explicitly pointing to no object
	 */
	FORCEINLINE bool IsNull() const
	{
		return !ObjectID.IsValid();
	}
    
private:
	/** Once the object has been noticed to be loaded, this is set to the object weak pointer **/
	mutable FWeakObjectPtr	WeakPtr;
	/** Compared to CurrentAnnotationTag and if they are not equal, a guid search will be performed **/
	mutable int32			TagAtLastTest;
	/** Guid for the object this pointer points to or will point to. **/
	TObjectID				ObjectID;
}


struct FSoftObjectPtr : public TPersistentObjectPtr<FSoftObjectPath>
{
    // .... 생략
}


struct TSoftObjectPtr
{
	template <class U>
	friend struct TSoftObjectPtr;

public:

    // .... 생략
    
    FORCEINLINE bool IsValid() const
	{
		// This does the runtime type check
		return Get() != nullptr;
	}

	/**
	 * Dereference the soft pointer.
	 * @return nullptr if this object is gone or the lazy pointer was null, otherwise a valid UObject pointer
	 */
	FORCEINLINE T* Get() const
	{
		return dynamic_cast<T*>(SoftObjectPtr.Get());
	}

	/**  
	 * Test if this does not point to a live UObject, but may in the future
	 * @return true if this does not point to a real object, but could possibly
	 */
	FORCEINLINE bool IsPending() const
	{
		return SoftObjectPtr.IsPending();
	}

	/**  
	 * Test if this can never point to a live UObject
	 * @return true if this is explicitly pointing to no object
	 */
	FORCEINLINE bool IsNull() const
	{
		return SoftObjectPtr.IsNull();
	}
    
    // .... 생략
    
private:
	FSoftObjectPtr SoftObjectPtr;
};
                                          
                    

StreamableManager

UAsset 에 대한 Synchronous Load (동기 로드)와 ASynchronous Load (비동기 로드)기능을 제공하는 관리자 객체 다수의 오브젝트 경로를 입력해 다수의 에셋을 로딩하는것도 과능하다. 콘텐츠 제작과 무관한 싱글톤 클래스에 FStreamableManager 를 선언해 두면 좋다. (예 - GameInstance)

에셋 비동기 로드는 어떻게 할까? 가장 쉬운 방법은 FStreamableManager를 사용하는 것입니다. 우선 FStreamableManager 를 만들어 줘야 하는데, Singleton Object, 이를테면 DefaultEngine.ini 에서 GameSingletonClassName에 지정된 오브젝트에 넣는 것이 좋습니다. 그리고 StreamableManager에 FSoftObjectPath 를 전달한 다음 로드를 시작합니다. SynchronousLoad는 메인 스레드를 너무 오래 붙잡아 둘 가능성이 있습니다. RequestAsyncLoad를 사용하면 애셋을 비동기 로드한 다음 완료되면 델리게이트를 호출합니다. 방법은 아래와 같습니다.

                        
void UGameCheatManager::GrantItems()
{
       TArray<FSoftObjectPath> ItemsToStream;
       FStreamableManager& Streamable = UGameGlobals::Get().StreamableManager;
       for(int32 i = 0; i < ItemList.Num(); ++i)
       {
       		  // ItemList 는 TArray<TSoftObjectPtr<UGameItem>> 이며, 에디터에서 디자이너에 의해 수정된 것.
              ItemsToStream.AddUnique(ItemList[i].ToStringReference());
       }
       Streamable.RequestAsyncLoad(ItemsToStream, FStreamableDelegate::CreateUObject(this, &UGameCheatManager::GrantItemsDeferred));
}

void UGameCheatManager::GrantItemsDeferred()
{
       for(int32 i = 0; i < ItemList.Num(); ++i)
       {
              UGameItemData* ItemData = ItemList[i].Get();
              if(ItemData)
              {
                     MyPC->GrantItem(ItemData);
              }
       }
}
                        
                    

StreamableManager 는 델리게이트가 호출될 때까지 로드하는 애셋에 대한 하드 레퍼런스를 유지시켜, 비동기 로드 요청한 오브젝트의 델리게이트가 호출되기도 전에 가비지 컬렉팅되는 일이 없도록 합니다. 델리게이트가 호출된 이후에는 그 레퍼런스가 해제되므로, 계속해서 남아있도록 하려면 어딘가에 하드 레퍼런스를 해 줘야 합니다. 같은 메서드를 사용해서 FAssetData 를 비동기 로드할 수도 있는데, 그냥 ToStringReference() 를 호출한 다음 배열에 추가시키고 델리게이트를 붙여 RequestAsyncLoad 를 호출해 주면 됩니다. 위에 언급한 메서드를 조합하면 게임 내 어느 애셋에 대해서도 효율적인 로드가 가능한 시스템을 구축할 수 있을 것입니다. 메모리에 직접 접근하는 게임플레이 코드가 비동기 로드를 처리하도록 변환해 주는 작업에 시간이 조금 걸리겠지만, 그 이후에는 게임에서 발생하는 멈춤 현상이나 차지하는 메모리 양이 훨씬 줄어들 것입니다.