언리얼 C++ 프로그래밍

[언리얼 C++] 비동기 애셋 로딩

언린이 2021. 6. 13. 20:21

UE4에서 비동기 애셋 로딩이란 실행시간에 애셋을 로드/언로드하는 메서드를 의미합니다.

 

비동기 애셋 로딩은 개발 도중이나 디바이스상에서 쿠킹된 데이터로 실행할 때나 동일하게 작동하므로, 필요에 따라 데이터를 로딩하기 위한 별도의 코드 패스를 유지할 필요가 없습니다.

이렇게 필요에 따라 데이터를 로드 및 참조에 사용되는 방법은 일반적으로 두가지가 있습니다.

 

 

1. FSoftObjectPath와 TSoftObjectPtr

FSoftObjectPath는 애셋의 전체 이름으로 된 스트링이 들어있는 단순한 구조체입니다. 클래스에 이 유형의 프로퍼티를 만들면, 에디터에는 마치 UObject* 프로퍼티인 양 나타납니다. 쿠킹과 리디렉터도 제대로 처리되므로, FSoftObjectPath가 있다면 디바이스에서의 정상 작동도 보장됩니다.

 

TSoftObjectPtr은 기본적으로 FSoftObjectPath를 감싸는 TWeakObjectPtr 입니다.

참조된 애셋이 메모리에 존재한다면 TSoftObjectPtr.Get()은 애셋을 반환합니다. 

 

FSoftObjectPath와 TSoftObjectPtr은 아티스트나 디자이너가 레퍼런스를 수동 셋업하는 경우에는 좋지만, 특정 조건을 만족하는 애셋 질의 작업을 모든 애셋 로드 없이 하려는 경우, 애셋 레지스트리와 오브젝트 라이브러리를 사용하는 것이 좋습니다.

 

 

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

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

애셋에 대한 데이터를 검색 가능하게 만들려면, 프로퍼티에 "AssetRegistrySearchable" 태그를 추가해 줘야 합니다. 애셋 레지스트리에 대한 질의는 FAssetData 유형의 오브젝트를 반환하는데, 여기에는 오브젝트에 대한 정보는 물론 검색 가능한 것으로 마킹된 프로퍼티가 들어있는 키->값 짝의 Map도 포합됩니다.

 

로드되지 않은 애셋 그룹을 가지고 작업하는 가장 쉬운 방법은 ObjectLibrary 입니다. ObjectLibrary는 로드된 오브젝트와 로드되지 않은 오브젝트의 경우 FAssetData를 합친 목록이 들어있는 오브젝트로, 공유 베이스 클래스를 상속합니다. ObjectLibrary에 검색할 경로를 주는 것으로 로드하면, 그 경로에 있는 모든 애셋이 추가됩니다. 이는 매우 유용할 수 있는데, 컨텐츠 폴더 일부분을 각기 다른 유형으로 지정하고, 아티스트/디자이너는 마스터 목록을 수동 편집할 필요 없이 새 애셋을 추가할 수 있기 때문입니다. 

 

ObjectLibrary를 사용해서 AssetData를 디스크에서 로드하는 방법은 아래와 같습니다.

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

위 코드에서는 새 오브젝트 라이브러리를 생성하고, 베이스 클래스를 할당하면, 주어진 경로의 모든 애셋 데이터를 로드합니다. 그리고 나서 옵션을 통해 실제 애셋을 로드할 수도 있습니다. 애셋이 작은 경우 애셋을 전체 로드할 수도 있고, 쿠킹 중인 경우 모두 쿠킹되도록 할 수도 있습니다. 쿠킹 도중 애셋 레지스트리 질의를 하고서 반환된 애셋을 로드하는 한, ObjectLibrary는 개발 중인 데이터든 디바이스에서 쿠킹된 데이터든 똑같이 작동합니다.

 

ObjectLibrary에 애셋 데이터가 들어있는 상태라면, 질의를 통해 특정 애셋만 선택적으로 로드할 수 있는데, 그 방법은 아래와 같습니다.

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("Type")))
       {
              return AssetData;
       }
}

위 코드에서는, ObjectLibrary에서 TypeName칸에 "Type"이 들어있는 것들을 검색하여 처음 찾은 것을 반환합니다.

 

반환된 AssetData를 가지고 ToStringReference()를 호출하여 FSoftObjectPath로 변환하고 나면 StreamableManager를 사용하여 비동기 로드를 할 수 있습니다.

 

 

3. StreamableManager와 비동기 로딩

비동기 로딩을 하기 위해서 먼저 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는 델리게이트가 호출될 때까지 로드하는 애셋에 대한 하드 레퍼런스를 유지시켜, 비동기 로드하려 했던 오브젝트의 델리게이트가 호출되기도 전에 가비지 컬렉팅되는 일이 없도록 합니다. 델리게이트가 호출된 이후에는 그 레퍼런스가 해제되므로, 계속해서 남아있도록 하려면 어딘가에 하드 레퍼런스를 해줘야 합니다.