Ссылки на ассеты.
UE4 позволяет работать с ассетами в двух режимах:
- Жесткие ссылки, когда объект А содержит ссылку на объект Б, и в случае загрузки объекта А, тут же загружается объект Б.
- Мягкие ссылки (ленивые), когда объект А содержит косвенную ссылку на объект Б, в виде, например, строки до файла модели.
Разберемся с
жесткими ссылками.
Мы можем создавать их двумя путями:
1. Ссылка из редактора.Это обычная переменная, отмеченная UPROPERTY() макросом. Вы создаете такую переменную, отдаете класс дизайнеру, который создает на его основе BluePrint и присваивает переменной значение, выбирая ассет в редакторе. Теперь при создании BluePrint'а дизайнера всегда будет загружаться меш, звук или материал по прямой жесткой ссылке.
2. Ссылка из конструктора.Вы используете хелпер конструктора класса, например
Код:
static ConstructorHelpers::FObjectFinder<UTexture2D> BarFillObj(TEXT("/Game/UI/HUD/BarFill"));
Теперь вы можете присвоить
BarFillObj.Object своей переменной-текстуре и теперь при создании вашего класса в игре (или BP, наследованного от этого класса) всегда будет загружаться текстура
BarFill из папки
Content\UI\HUDПо сути тоже самое, как и в пункте 1, только ассет выбирается программистом, а не дизайнером. Если по такому пути ничего нету, то вернется
NULL, поэтому перед вызовом
.Object, следует убедиться, что объект найден.
В чем самая главная соль жестких ссылок? На этапе загрузки уровня, ваши акторы начинают жестко подгружать свои модели, текстуры, звуки и прочее. В этот момент вы наблюдаете, что сцена тупо висит, потому что пока все не проинициализируются, нет возможности продолжить работу дальше. Не очень приятно.
Поэтому есть механизм
мягких ссылок.
Одна из возможностей их использования - TAssetPtr.
Например
Код:
TAssetPtr<UStaticMesh> BaseMesh;
В данной переменной содержится не ссылка на ассет, а строка-путь до него в наших папках. Вы можете проверить, загружен ли объект (
.IsPending()) и загрузить его при необходимости в любой момент игры, как в примере ниже.
Код:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Building)
TAssetPtr<UStaticMesh> BaseMesh;
UStaticMesh* GetLazyLoadedMesh()
{
if (BaseMesh.IsPending())
{
const FStringAssetReference& AssetRef = BaseMesh.ToStringReference();
BaseMesh = Cast< UStaticMesh>(Streamable.SynchronousLoad(AssetRef));
}
return BaseMesh.Get();
}
Для того что бы использовать механизм применимо к классам, нужно использовать
TAssetSubclassOf, вместо
TAssetPtr.
Все хорошо, но тут путь задается дизайнером в редакторе, а что если мы сами хотим создать строку-путь и попробовать загрузить что-то по этому пути?
Есть два метода
FindObject и
LoadObject. Первый ищет ассет среди загруженных, второй ассет грузит. По уму сначала нужно попробовать найти ассет среди загруженных и если его там нет, то загрузить. Пример использования:
Код:
AFunctionalTest* TestToRun = FindObject<AFunctionalTest>(TestsOuter, *TestName);
GridTexture = LoadObject<UTexture2D>(NULL, TEXT("/Engine/EngineMaterials/DefaultWhiteGrid.DefaultWhiteGrid"), NULL, LOAD_None, NULL);
Для классов есть соотвественно своя функция:
Код:
DefaultPreviewPawnClass = LoadClass<APawn>(NULL, *PreviewPawnName, NULL, LOAD_None, NULL);
Которая по сути эквивалентна действиям:
Код:
DefaultPreviewPawnClass = LoadObject<UClass>(NULL, *PreviewPawnName, NULL, LOAD_None, NULL);
if (!DefaultPreviewPawnClass->IsA(APawn::StaticClass()))
{
DefaultPreviewPawnClass = nullptr;
}
Итак подведем промежуточный итог.- Каждый раз, когда мы устанавливаем UPROPERTY() макрос, то мы даем задание движку подготовиться и загрузить наши ассеты в память при появлении объекта нашего родительского класса в памяти. (Можно случайно загрузить вообще все ассеты сцены при старте оной, даже те, которые игрок увидит только на десятый час игры, что не совсем хорошо).
- Если мы хотим оставить дизайнерам возможность удобно выбирать ассеты через UI редактора, но оставить за собой контроль за их подгрузкой, то мы используем TAssetPtr или FStringAssetReference.
Стоп. Что за FStringAssetReference? Это простая структура, которая содержит путь до нашего ассета. В редакторе никто не заметит разницу по сравнению с жесткой ссылкой. Можно будет перетягивать ассеты, все будет запекаться (cooking), будут работать редиректоры.
Что же тогда такое TAssetPtr? А это обертка над FStringAssetReference, которая позволяет ограничивать пользователя редактора каким-то конкретным классом (например он сможет выбирать ассет в редакторе только из материалов или мешей). Если ассет в памяти, то TAssetPtr.Get() вернет нам его, если нет, то ToStringReference() вернет нам строку, по которой его можно загрузить и снова вызвать TAssetPtr.Get().
Хорошо, если мы однозначно знаем, что за ассет нам нужен. А если мы хотим искать по каким-то признакам не загружая все ассеты в память?
Используем систему регистров ассетов. В ней хранятся метаданные об ассетах, которую нам отображает наш контент-браузер редактора. Что бы сделать возможность поиска нужно добавить в
UPROPERTY() тег
AssetRegistrySearchable. Поиск по регистру вернет объекты FAssetData, которые содержат информацию в виде карты ключ->значение.
Легче всего работать с неподгруженными ассетами через
ObjectLibrary. Это объект, который содержит список уже загруженных объектов и
FAssetData для незагруженных объектов. Вы загружаете эту волшебную библиотеку, передавая ей путь для поиска, то она вернет вам все найденные по пути объекты. Это очень удобно, потому что обычно контент группируется иерархически по папкам, поэтому наши артисты могут спокойно рассовывать свои новые поделки по правильным папкам, а движок сам будет решать что делать с новым контентом. Небольшой примерчик:
Код:
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;
}
}
Тут мы ищем первый ассет, который содержит в поле
TypeName строчку "FooType". Теперь имея
FAssetData, можно вызвать
ToStringReference() и получить уже знакомый нам
FStringAssetReference.
Вау! Это очень круто!
А теперь мы загрузим наш
FStringAssetReference в память. Давайте подробнее разберемся с
StreamableManager, который вскользь проскочил в нашем коде в начале статьи.
Этот класс позволяет загружать наши объекты в память. Лучше всего сделать его синглтоном вашей игры и вынести куда-нибудь в глобальные дали. Умеет он это делать синхронно (как в начале статьи
Streamable.SynchronousLoad(AssetRef)) и асинхронно. Мы же крутые перцы? Зачем нам синхронно тормозить всю игру? Давайте сделаем все еще круче:
Код:
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);
}
}
}
Ну вроде все понятно? Берем массив путей, скармливаем нашему
StreamableManager вместе с каллбеком и ждем, потирая руки, пока тот не выполнит свои темные дела. Только не забудьте, что все несохраненные ссылки сожрет GC сразу после выполнения каллбека.