관리 메뉴

기억을 위한 기록들

[UE4-CPP] [애셋로딩에 관하여-2] 비동기식 애셋로딩 본문

UnrealEngine/UE4 - CPP

[UE4-CPP] [애셋로딩에 관하여-2] 비동기식 애셋로딩

에드윈H 2021. 12. 12. 14:20

 

*언리얼엔진4 문서 참조하여 작성

https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/Assets/AsyncLoading/

 

비동기 애셋 로딩

실행시간에 애셋을 로드/언로드하는 메서드입니다.

docs.unrealengine.com

 

애셋 데이터를 참조하고 요청 시 로드하는 데 사용할 수 있는 일반적인 방법에는 두 가지가 있습니다.

 

 

FSoftObjectPath와 TSoftObjectPtr

아티스트나 디자이너가 애셋을 참조하도록 하는 가장 쉬운 방법은 하드 포인터의 UPROPERTY를 만들고 카테고리를 지정하는 것이다. UE4에서 애셋을 참조하는 하드 UObject 포인터 UPROPERTY을 가지고 있다면, 그 애셋은 그 속성을 포함하는 객체를 로드 할 때 로드될 것이다.
조심하지 않으면 게임 시작 시 모든 애셋을  100%를 로드하게 된다.

아티스트 혹은 디자이너가 항상 참조된 애셋을 로드하지 않고 동일한 UI를 하드 포인터와 함께 사용하여 특정 애셋에 대한 참조를 수행할 수 있도록 하려면 FSoftObjectPath 또는 TSoftObjectPtr을 사용해야한다.

 

 

FSoftObjectPath 란?

애셋의 전체 이름을 가진 문자열을 포함하는 단순 구조체입니다.

/** 
 * A struct that contains a string reference to an object, either a top level asset or a subobject.
 * @note The full C++ class is located here: Engine\Source\Runtime\CoreUObject\Public\UObject\SoftObjectPath.h
 */
USTRUCT(noexport, BlueprintType, meta=(HasNativeMake="Engine.KismetSystemLibrary.MakeSoftObjectPath", HasNativeBreak="Engine.KismetSystemLibrary.BreakSoftObjectPath"))
struct FSoftObjectPath
{
	/** Asset path, patch to a top level object in a package */
	UPROPERTY()
	FName AssetPathName;

	/** Optional FString for subobject within an asset */
	UPROPERTY()
	FString SubPathString;
};

TSoftObjectPtr 은 기본적으로 FSoftObjectPath 를 감싸는 TWeakObjectPtr 이며, 에디터 UI 에서 특정 클래스만 선택되게끔 제한시킬 수 있도록 하기 위해서 특정 클래스로 고정됩니다.

참조된 애셋이 메모리에 있는 경우 TSoftObjectPtr.Get()가 해당 포인터를 반환한다. 존재하지 않는다면, ToSoftObjectPath() 을 호출하여 참조하는 애셋을 찾아내어 아래 설명된 메서드를 사용해서 로드한 다음 TSoftObjectPtr.Get() 을 다시 호출하여 역참조합니다.

 

TSoftObjectPtr 과 SoftObjectPath 는 아티스트나 디자이너가 레퍼런스를 수동으로 설정하는 경우에는 좋지만,

특정 요구 사항을 충족하는 자산을 찾기 위해 쿼리 등의 작업을 수행하여 해당 자산을 모두 로드하지 않고 '애셋 레지스트리' 및  '오브젝트 라이브러리'를 사용하는 것이 좋습니다.

 

애셋 레지스트리와 오브젝트 라이브러리

애셋 레지스트리?

애셋에 대한 메타데이터를 저장하여 해당 애셋에 대한 검색 및 질의를 가능케 해주는 시스템입니다.

 

에디터에서는 콘텐츠 브라우저에 정보를 표시하기 위해 사용되나, 게임플레이 코드에서 현재 로드되지 않은 게임플레이 애셋에 대한 메타데이터 질의를 하는 데도 사용할 수 있습니다.

 

애셋에 대한 데이터를 검색 가능하게 만들려면, 프로퍼티에 "AssetRegistrySearchable" 태그를 추가해 줘야 합니다.

애셋 레지스트리에 대한 질의는 FAssetData 유형 오브젝트를 반환하는데, 여기에는 오브젝트에 대한 정보는 물론 검색가능한 것으로 마킹된 프로퍼티가 들어있는 키->값 짝의 맵도 포함됩니다.

 

오브젝트 라이브러리?

로드되지 않은 애셋 그룹을 가지고 작업하는 가장 쉬운 방법은 오브젝트 라이브러리입니다.

오브젝트 라이브러리

-  로드된 오브젝트

-  공유 기본 클래스에서 상속된 언로드된 오브젝트에 대한 FAssetData목록을 포함하는 오브젝트

검색할 경로를 지정하여 오브젝트 라이브러리를 로드하면 해당 경로에 모든 애셋이 추가됩니다.

 

이는 매우 유용할 수 있는데, 콘텐츠 폴더 일부분을 각기 다른 유형으로 지정하고, 아티스트/디자이너는 마스터 목록을 수동 편집할 필요 없이 새 애셋을 추가할 수 있기 때문입니다. 오브젝트 라이브러리를 사용해서 AssetData 를 디스크에서 로드하는 방법 예제는 이렇습니다:

if (!ObjectLibrary)
{
       ObjectLibrary = UObjectLibrary::CreateLibrary(BaseClass, false, GIsEditor);
       ObjectLibrary->AddToRoot();
}
ObjectLibrary->LoadAssetDataFromPath(TEXT("/Game/PathWithAllObjectsOfSameType");
if (bFullyLoad)
{
       ObjectLibrary->LoadAssetsFromAssetData();
}
TArray<FAssetData> AssetDatas;
ObjectLibrary->GetAssetDataList(AssetDatas);

for (int32 i = 0; i < AssetDatas.Num(); ++i)
{
       FAssetData& AssetData = AssetDatas[i];

       const FString* FoundTypeNameString = AssetData.TagsAndValues.Find(GET_MEMBER_NAME_CHECKED(UAssetObject,TypeName));

       if (FoundTypeNameString && FoundTypeNameString->Contains(TEXT("FooType")))
       {
              return AssetData;
       }
}

위 예시에서는 오브젝트라이브러리에서 TEXT("FooType")을 포함하는 TypeName 필드가 있는 모든 항목을 검색한 다음 가장 먼저 찾은 항목을 반환합니다.

FAssetData& AssetData가 생겨서 있으면 ToStringReference()를 호출하여 FSoftObjectPath로 변환한 후 다음 시스템을 사용하여 비동기식으로 로드할 수 있습니다.

 

StreamableManager 와 비동기 로딩

이제 디스크의 애셋을 참조하는 FSoftObjectPath 가 생겼으니, 실제 비동기 로드는 어떻게 할까요?

가장 쉬운 방법은 FStreamableManager 입니다. 우선, FStreamableManager 를 만들어 줘야 하는데, 일종의 게임 전역 싱글턴 오브젝트, 이를테면 DefaultEngine.ini 에서 GameSingletonClassName 에 지정된 오브젝트에 넣는 것이 좋습니다. 그 후 거기에 FSoftObjectPath 를 전달한 다음 로드를 시작합니다. 

 

SynchronousLoad 는 단순한 로드 블록 후 오브젝트를 반환할 것입니다. 작은 오브젝트의 경우 이 메서드로 충분할 테지만, 메인 스레드를 너무 오래 붙잡아 둘 가능성이 있습니다.

그러한 경우에는 RequestAsyncLoad 를 사용해 줘야 하는데, 애셋 그룹을 비동기 로드한 다음 완료되면 델리게이트를 호출하는 것입니다. 예제입니다

void UGameCheatManager::GrantItems()
{
       TArray<FSoftObjectPath> ItemsToStream;
       FStreamableManager& Streamable = UGameGlobals::Get().StreamableManager;
       for(int32 i = 0; i < ItemList.Num(); ++i)
       {
              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);
              }
       }
}

이 예제에서 ItemList 는 TArray< TSoftObjectPtr<UGameItem> > 이며, 에디터에서 디자이너에 의해 수정된 것입니다. 코드는 그 리스트에 대해 반복하여 StringReferences 로 변환시킨 다음 로드를 위한 대기열에 등록시킵니다.

 

그 아이템 전부가 로드되(거나 없어서 실패하)면 전달된 델리게이트를 호출합니다. 그러면 그 델리게이트는 같은 아이템 리스트에 대해 반복하여 그 역참조를 구한 다음 플레이어에게 전해줍니다. 

 

StreamableManager 는 델리게이트가 호출될 때까지 로드하는 애셋에 대한 하드 레퍼런스를 유지시켜, 비동기 로드하려 했던 오브젝트의 델리게이트가 호출되기도 전에 가비지 컬렉팅되는 일이 없도록 합니다.

 

델리게이트가 호출된 이후에는 그 레퍼런스가 해제되므로, 계속해서 남아있도록 하려면 어딘가에 하드 레퍼런스를 해 줘야 합니다.

 

같은 메서드를 사용해서 FAssetData 를 비동기 로드할 수도 있는데, 그냥 ToStringReference 를 호출한 다음 배열에 추가시키고 델리게이트를 붙여 RequestAsyncLoad 를 호출해 주면 됩니다.

 

델리게이트는 원하는 무엇이든 될 수 있으므로, 원한다면 페이로드 정보와 함께 전달해 줄 수 있습니다. 위에 언급한 메서드를 조합하면 게임 내 어느 애셋에 대해서도 효율적인 로드가 가능한 시스템을 구축할 수 있을 것입니다.

 

메모리에 직접 접근하는 게임플레이 코드가 비동기 로드를 처리하도록 변환해 주는 작업에 시간이 조금 걸리겠지만, 그 이후에는 게임에서 발생하는 멈춤 현상이나 차지하는 메모리 양이 훨씬 줄어들 것입니다.

 

void MyComponent::RoadAnimAsset(const FString& path )
{
FSoftObjectPath asset( path );

FStreamableManager assetLoader;
TAssetPtr<UAnimMontage> animMotage = assetLoader.LoadSynchronous( asset );

auto pCharacter = Cast<ACharacter>( GetOwner() );

if( pCharacter )
{
pCharacter->PlayAnimMontage( animMotage.Get() );
}

}