Полезное

Мы Вконтакте

Discord канал

#
Аватара пользователя
Пользователь
Сообщения: 40
Добрый день. Копаясь по документации наткнулся на возможность, с которой хочу поделиться в этой статье.

Рассмотрим ситуацию, когда у вас есть гоночный симулятор. Есть машины, есть запчасти и вам очень хочется дать возможность игроку покупать разные колеса и двигателя, которые не только отличаются визуально, но и меняют параметры всей машины.

Вы создали класс своей машины, сделали в ней переменные для всех запчастей и встали перед вопросом: а как же хранить характеристики всех запчастей. Причем хранить удобно, что бы в любой момент можно было что-то поменять, что-то подкрутить.
А потом задумались, как же грамотно все это менять на своей машине, заведомо не зная, есть ли модели и материалы, описанные в вашей "базе данных" нерадивым дизайнером в вашем проекте.

Начнем с конца.
Аватара пользователя
Пользователь
Сообщения: 40
Ссылки на ассеты.

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;
}


Итак подведем промежуточный итог.
  1. Каждый раз, когда мы устанавливаем UPROPERTY() макрос, то мы даем задание движку подготовиться и загрузить наши ассеты в память при появлении объекта нашего родительского класса в памяти. (Можно случайно загрузить вообще все ассеты сцены при старте оной, даже те, которые игрок увидит только на десятый час игры, что не совсем хорошо).
  2. Если мы хотим оставить дизайнерам возможность удобно выбирать ассеты через 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 сразу после выполнения каллбека.
Аватара пользователя
Пользователь
Сообщения: 40
Хорошо. Мы научились грузить все и вся. Теперь нужно разобраться, как хранить конкретные вещи. Например модель двигателя, его материал, цвет-параметр материала, мощность двигателя, крутящий момент, расход топлива и т.п.

Используем механизм DataTables.

Дизайнер берет старый-добрый эксель и заполняет его всеми нашими параметрами. Например:
Код:
Name,Power,Torque,Asset
"CartoonFerrariEngine",10,20,"Texture2d'/Game/Engines/CartoonFerrari'"
"CartoonMANEngine",10,20,"Texture2d'/Game/Engines/CartoonMAN'"


В тоже время программист делает возможность чтения данных из этого файла:
Код:
USTRUCT(BlueprintType)
struct FEngineData : public FTableRowBase
{
        GENERATED_USTRUCT_BODY()

public:

        FEngineData ()
                        : Power(0)
                        , Torque(0)
        {}

        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Engine)
        FString Name;

        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Engine)
        int32 Power;

        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Engine)
        int32 Torque;

        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Engine)
        TAssetPtr<UTexture> Asset;
};


О! Привет, знакомый нам TAssetPtr<UTexture>, теперь мы знаем, что с тобой делать.

Ладно, а как связать мух с котлетами?

Просто импортируйте ваш excel файл в контент-папку и движок предложит выбрать структуру, с которой будет ассоциировать данные.

Теперь немного о втором типе файлов-данных - CurveTable. Точно такая же таблица, но с одним "но". В ней задаются не просто дискретные точки "состояний", а некоторые опорные точки кривой "состояния".

Чаво?

Ну вот, например:
Код:
   "Пусто"   0   1   2   3
Melee_Damage   15   20   25   30
Melee_StunTime   0   0   0   0


Теперь вы можете не просто брать состояние в точке "1", состояние в точке "2", а попросить движок интерполировать состояние "1.5". И тогда движок вернет Melee_Damage = 22.5f
А еще можно настраивать виды интерполяции: линейная, кубическая... Но это уже отдельная тема, если интересно, покопайтесь.

Итак. Есть таблица, есть структура, все связано, но теперь нам бы как-то получить наши данные.

Берем компонент UDataTable, связываем его (вы же теперь умеете динамически подгружать ассеты) с нашим ассетом-таблицей, а теперь используем магию UDataTable::FindRow. Или аналогичные классы и методы в CurveTable.

И будьте внимательны. Если вы в своей структуре используете не TAssetPtr<UTexture>, а просто UTexture, то при загрузке вашей таблицы в память выгрузятся все ассеты, указанные в таблице. Вы ведь помните разницу между жестким и ленивым связыванием?

Всем спасибо. Удачи в ваших проектах.
Аватара пользователя
Пользователь
Сообщения: 40
Ах да, если вас не волнует ленивое связывание, то просто создайте структуру в редакторе. Ассоциируйте ее с файлом-данных и читайте данные в Blueprints, через аналогичные методы. Все для народа :)
Аватара пользователя
Пользователь
Сообщения: 21
Познавательно и полезно. Спасибо!


Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 10

UEngine.ru © 2017
Все права защищены. При копировании материалов с сайта, ссылка на первоисточник обязательна.
Яндекс.Метрика
Главная страница