August 10, 2020

Ассинхронная загрузка ассетов.

Оригинал: https://docs.unrealengine.com/en-US/Programming/Assets/AsyncLoading/index.html

Unreal Engine 4.9

В UE4 появилось несколько новых систем, сильно упрощающих ассинхронную загрузку ассетных данных. Они заменяют большую часть функционала "seekfree" пакетов в UE3. Новые методы работают одинаково, как при разработке так и с подготовленными данными уже на устройстве пользователя. Такми образом, вам не нужно поддерживать две ветви кода. Существуют два основных метода для загрузки и ссылания на данные.

FSofObjectPaths и TSoftObjectPtr

Самый простой способ сослаться на ассет для дизайнера или художника — это создать строгий указатель на UProperty и присвоить ему категорию. В UE4 если у вас есть строгий указатель на UObject, который верно ссылается на ассет, то этот ассет будет загружен вместе с объектом, содержащим этот ассет путём размещения объекта на карте или ссылание на ассет из чего-то наподобие gameinfo. Если вы не будете соблюдать осторожность, то можете загрузить сразу все 100% ваших ассетов во время загрузки вашей игры. Если вы хотите, чтобы дизайнеры могли создавать ссылки на конкретные ассеты, используя тот же интерфейс, что и в случае со строгим указателем, используйте FSofObjectPaths и TSoftObjectPtr.

FSofObjectPaths — это простая структура, содержащая строку с полным именем ассета. Если вы создадите поле этого типа в классе, то оно появится в редакторе, как если бы это было поле типа UObject*. Также, она корректно поддерживает кукинг и редиректы, так что если у вас есть SoftObjectPath, то она гарантировано будет работать на устройстве пользователя. TSoftObjectPath — это практически TWeakObjectPtr, который является враппером FSoftObjectPath и будет шаблоном конкретного класса, чтобы можно было разрешить выбирать в редакторе только конкретные классы. Если ассет на который ссылаются существует в памяти, TSoftObjectPtr.Get() вернёт его. Если нет, то вы можете вызвать TSoftObjectPath(), чтобы найти ассет на который он ссылается, загрузить его, используя метод ниже, а затем снова вызвать TSoftObjectPtr.Get(), чтобы разыменовать его.

TSoftObjectPtrs и нестрогие пути к объекту прекрасны, если художник или дизайнер настраивают ссылки вручную, но если вы хотите сделать что-то наподобие запроса, чтобы найти ассет, удовлетворяющий определённым требованиям, без загрузки всех ассетов, вам придётся использовать регистр ассетов или библиотеки объектов.

Реестр ассетов и Библиотеки объектов

Реестр ассетов — это система, которая хранит матаданные об ассетах и позволяет создавать запросы об этих ассетах. Она используется в редакторе для вывода информации в браузере контента, но ещё её можно использовать из геймплейного кода для создания запросов метаданных о незагруженных ассетах. Чтобы метаданные ассета отображались при поиске, нужно дабавить тег AssetRegistrySearchable в свойства. Запросы к реестру ассетов возвращают объекты типа FAssetData, содержащие информацию об объекте и карту с парами формата ключ->значение, содержащими свойства, помеченные как доступные для поиска.

Самый простой способ работы с группами незагруженных ассетов — это использование Object Library. Object Library — это объект, содержащий список загруженных объектов или FAssetData для незагруженных объектов, который наследует от общего класса. Для загрузки библиотеки объектов нужно дать ей путь, по которому она будет искать, что в свою очередь добавит все ассеты по этому пути. Это может быть очень полезно, потому что так можно назначать отдельные части вашей контентной папки для разных видов контента, и ходожники и дизайнеры смогут доавлять ассеты без необходимости редактировать список ассетов. Ниже приведён пример загрузки данных ассетов с диска, используя библиотеку объектов.

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

В этом примере создаётся новая библиотека объектов, связывается с базовым классом и загружает все данные ассетов, найденные по указаннному пути. Затем опционально загружаются сами ассеты. Следует полностью загружать ассеты либо если они лёгкие, либо если вы занимаетесь кукингом и вам нужно убедиться, что все ассеты действительно "кукнулись". Пока вы выполняете запрос реестру ассетов во время кукинга и загружаете возвращённые ассеты, библиотека объектов будет работать с "кукнутыми" данными на устройстве пользователя в точности, как при разработке. С момента, как у вас есть данные ассетов в ObjectLibrary, вы можете создавать запросы и выборочно загружать определённые ассеты. Ниже приведён пример создания запроса.

TArray<FAssetData> AssetDatas;
ObjectLibrary->GetAssetDataList(AssetDatas);

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

        const FString* FoundTypeNameString = AssetData.TagsAndValues.Find(GET_MEMBER_NAME_CHECKED(uaSSEToBJECT, tYPEnAME));

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

В этом примере происходит поиск по библиотеке объектов чего либо, где поле TypeName содержит "FooType". Возвращается первый найденный экземпляр. После получения AssetData, можно вызвать ToStringReferece() чтобы конвертировать её в FSoftObjectPath, который можно будет загружать ассинхронно.

StreamableManager и Асинхронная загрузка

Теперь когда у вас есть FSoftObjectPath, который ссылается на ассет на диске, как его можно загрузить асинхронно? Использование FStreamableManager — простейший способ сделать это. Для начала вам нужно будет создать FStreamableManager. Я бы советовал поместить его во что-то на подобие глобального для игры синглтон объекта, например определённого с помощью GameSingletonClassName в DefaultEngine.ini. Затем ему можно передать FSoftObjectPath и начать загрузку. SynchronousLoad произведёт обычную блокирующую загрузку (игра остановиться до завершения загрузки) и вернёт ассет. Этот метод может подойти для легковесных объектов, однако в некоторых случаях может остановить главный поток слишком надолго. В таком случае нужно будет использовать ReqestAsynchLoad, который асинхронно загрузит группу ассетов и вызовет делегата по завершению. Ниже приведён пример.

void UGameCheatManager::GrantItems()
{
        TArray<FSoftObjectpath> ItemsToStream;
        FStreamManager& Streamable = UGameGlobals::Get().StreamManager;
        for (int32 i = 0; i < ItemList.Num(); ++i)
        {
                ItemsToStream.AddUnique(ItemList[i].ToStringReference());
        }
        Streamable.RequestAsynchLoad(ItemsToStream, FStreamableDelegate::CreateUObject(this, &UGameCheatManager::GrantItemsDeffered));
}

void UGameCheatManager::GrantItemsDeffered()
{
        for (int32 i = 0; i < ItemList.Num(); ++i)
        {
                UGameData* ItemData = ItemList[i].Get()
                if (ItemData)
                {
                        MyPc->GrantItems(ItemData);
                }
}

В этом примере ItemList является TArray&lt, который содержит TSoftObjectPath<UGameItem>, который был модифицирован дизайнерами в редакторе. Код итерирует список и конвертирует TSoftObjectPath<UGameItem> в StringReference и ставит их в очередь на загрузку. Когда все эти предметы загружены (или отсутствуют и не могли быть загружены), вызывается переданный ранее делегат. Этот делегат затем итерирует тот же список предметов, разыменовывает их и передаёт игроку. StreamableManager хранит строгие ссылки на любой загруженный им ассет, пока не будет вызван делегат, чтобы вы могли быть уверены, что ни один из объектов, которых вы хотели асинхронно загрузить не будут удалены сборщиком мусора до того как будет вызван делегат. StreamableManager перестаёт хранить ссылки после вызова делегата, так что вам нужно строго ссылаться на объекты где-нибудь в другом месте, чтобы удостовериться, что они остануться под рукой.

Вы можете использовать тот же самый метод для загрузки FAssetData, просто вызовите для них (FAssetData) ToStringReference и поместите их в массив, затем вызовите RequestAsynchLoad вместе с делегатом. Делегат может быть чем угодно, так что вы можете передать информацию о пейлоэде, если это необходимо. Путём комбинирования выше описанных методов вы можете создать систему, позволяющую эффективно загружать любые ассеты в вашу игру. Переписывание геймплейного кода, использующего прямые обращения к памяти под использование асинхронной загрузки займёт время, но в итоге ваша игра будет иметь меньше простоев и будет использовать намного меньше памяти.