异步资源加载

概述

虚幻引擎4中有几个新系统,使得可以更加轻松地异步加载资源数据,这些系统代替了虚幻引擎3中的免搜索内容包中存在的很多功能。这些新方法既可以在开发数据应用也可以在设备上的烘焙数据上进行应用,所以您不必保留两个根据需要加载数据的代码路径。有两种通用的方法可以供您根据需要来引用及加载数据。

FStringAssetReferences 和TAssetPtr

美术师或设计师引用资源的最简单的方法是,创建一个UProperty 强指针,并赋予其一个类目。在虚幻引擎4中,如果您使用强指针UObject 属性引用一个资源,那么当加载包含该属性的对象时将会加载那个资源(通过把对象放置在地图中,或者通过从类似于游戏信息这样的东西中进行引用该对象)。如果您不关心是否在游戏启动时能够100%地加载您的资源。如果您想让 美术师/设计师 使用和强指针一样的用户界面来引用特定的资源,而不是总是加载引用的资源,请使用 FStringAssetReferenceTAssetPtr

FStringAssetReference 是一个简单的结构体,包含了一个具有资源完整名称的字符串。如果您在类中创建一个那种类型的属性,那么它将会显示在编辑器中,就像是个 UObject * 属性一样。它还可以正确地处理烘焙和重定向,所以如果您有一个StringAssetReference,那么它一定可以在设备上正常工作。TAssetPtr基本上就是一个封装了 FStringAssetReferenceTWeakObjectPtr ,它使用一个特定的类作为模板,以便您能限制编辑器用户界面,使其仅允许选择特定的类。如果所引用的资源存在于内存中,那么 TAssetPtr.Get() 将返回该资源。如果该资源不在内存中,那么您那可以调用 ToStringReference() 来查找它引用的资源,并使用下面介绍的方法加载该资源,然后再次调用 TAssetPtr.Get() 解除对该资源的引用。

如果美术师或设计师正在手动地设置引用,那么使用 TAssetPtrs 和 StringAssetReferences是非常适合的,但如果您想进行类似于查询这样的处理,查找满足特定要求的资源,而不是加载所有资源,那么您应该使用资源注册表及对象库。

资源注册表和对象库

资源注册表是一个存储资源的元数据的系统,允许您搜索及查询这些资源。编辑器使用此资源注册表来显示内容浏览器中的信息,但是您也可以从游戏性代码使用该注册表,来查找当前没有加载的游戏资源的相关元数据。要想使得一个资源的数据是可搜索的,您需要给该属性添加"AssetRegistrySearchable"标签。查询资源注册表会返回FAssetData类型的对象,它包含了关于该对象的信息和一个 键-》值 对映射表,该映射表包含了标记为可搜索的属性。

处理成组的未加载的资源的最简单方法是使用 ObjectLibraryObjectLibrary 是一个对象,包含了一系列继承共享基类的未加载对象或者未加载对象的FAssetData 。您可以通过提供一个搜索路径来加载一个对象库,它将加载那个路径中的所有资源。这是非常有用的,因为您可以为不同类型指定您的部分内容文件夹,且 美术师/设计师 不需要手动编辑清单表就可以添加新的资源。这里是一个示例,展示了如何使用一个对象库来从磁盘加载AssetData:

if (!ObjectLibrary)
{
       ObjectLibrary = ConstructObject<UObjectLibrary>(UObjectLibrary::StaticClass());
       ObjectLibrary->ObjectBaseClass = BaseClass;
       ObjectLibrary->UseWeakReferences(GIsEditor);
       ObjectLibrary->AddToRoot();
}
ObjectLibrary->LoadAssetDataFromPath(TEXT("/Game/PathWithAllObjectsOfSameType");
if (bFullyLoad)
{
       ObjectLibrary->LoadAssetsFromAssetData();
}

在这个示例中,它创建了一个新的对象库,关联了一个基类,然后加载了给定路径中的所有资源数据。接着选择性地加载真正的资源。如果资源很小,或者如果您正在进行烘焙且需要确保烘焙所有内容,那么您则需要完全地加载这些资源。只要您在烘焙过程中执行一次资源注册表查询,并加载返回的资源,您的对象库就可以正常地在烘焙数据中发挥作用,就像在开发过程中的处理一样。一旦把资源数据放入到了一个 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("FooType")))
       {
              return AssetData;
       }
}

在这个示例中,它搜索对象库并查找任何具有 TypeName 文本域且该文本域中包含 "FooType" 的对象,然后返回第一次查找到的对象。一旦有了此 AssetData ,您可以调用 ToStringReference() 来把它转换成一个 FStringAssetReference ,然后您可以使用下一个系统进行异步加载。

StreamableManager(动态加载管理器)和异步加载

现在,您具有一个引用了磁盘上一个资源的 FStringAssetReference ,那么怎样真正地异步加载它哪? FStreamableManager是完成这个处理的最简单方法。首先,您需要创建一个 FStreamableManager ,我建议你把它放到某个全局游戏单例对象中,比如在 DefaultEngine.ini 中使用 GameSingletonClassName 指定的对象。然后,您可以把 StringAssetReference 传给该对象,并启动进行加载。SynchronousLoad 将会进行简单的、阻碍加载,并返回该对象。这种方法对于较小的对象可能很好,但是它可能会使您的主线程停顿时间过长。出现那种情况,您将需要使用 RequestAsyncLoad ,它将会异步地加载一组资源,并在完成后调用一个代理。这里是一个示例:

void UGameCheatManager::GrantItems()
{
       TArray<FStringAssetReference> 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< TAssetPtr<UGameItem> > ,由设计人员在编辑器中进行修改。这段代码迭代那个列表,将列表项转换为 StringReferences ,并将它们进行排队。当所有列表项都加载后(或者由于缺失列表项而不能加载),它将在代理中调用已通过的相关处理。该代理然后迭代同样的列表项,区分它们,并把它们提供给一个玩家。StreamableManager 在调用该代理之前,将保持到任何它所加载的对象的强引用,所以在调用该代理之前,您可以确保您想异步加载的任何对象都没有被垃圾回收。当调用该代理后, StreamableManager 释放这些引用,所以您想确保它们一直存在,您需要在其他地方对它们进行强引用。

您可以使用同样的方法来异步加载 FAssetDatas ,仅需在它们上面调用 ToStringReference ,把它们添加到一个数组中,然后使用一个代理调用RequestAsyncLoad 。该代理可以是您想操作的任何内容,所以如果需要,你可以传入负载信息。通过把结合使用上述的方法,您应该可以设置一个高效加载游戏中的任何资源的系统。转换直接访问内存的游戏代码来处理异步加载将会花费一些时间,但是之后您的游戏停顿将变得非常少,并且内存使用量会更低。