06/02/2019

Добавляем экспорт данных из ассета

Недавно мне понадобилось экcпортировать данные из BlendSpace ассета в свой программный сервер — задача, скорее, ради фана и эксперимента. Возможно, такое больше никому не понадобится, но данный метод можно применить иначе — например, написать свой тулз для UE по работе с BlendSpace. UE4 имеет встроенную функцию экспорта данных ассета, но встроенный экспорт сохраняет все данные и в своем формате, что излишне, и нужно писать поддержку этого формата. Весь функционал мы реализуем в виде плагина.

Создание плагина

UE4 имеет "из коробки" встроенную возможность создавать шаблон кода плагина. Для этого необходимо выбрать Settings/Plugins и нажать кнопку New Plugin. В появившемся окне выбираем тип плагина Blank и вводим имя (например — BlendSpaceDataExporter) в поле name, жмем кнопку CreatePlugin. Будет создан каталог Plugins в директории проекта. На данном этапе плагин создан, его базовый шаблон кода будет виден в Visual Studio.

Добавление нового пункта меню

Так как мы создаем новый пункт меню для BlendSpace ассета, было бы хорошо сделать наш функционал на базе класса  AssetTypeActions_BlendSpace.h , который уже реализует необходимые пункты меню для BlendSpace ассета. Но данный класс находится в каталоге Private, из-за чего сторонние модули не смогут его подключить. Можно, конечно, сделать изменения в самом коде движка, в том же файле AssetTypeActions_BlendSpace.h или скопировать весь функционал в свой новый класс, но мы пойдем через создание функционала через плагин и добавим только пункт, который нас интересует.

Итак, создадим класс BlendSpaceDataActions, унаследованный от FAssetTypeActions_Base. Нам также нужно включить заголовочный файл AssetTypeActions_Base.h.

Теперь нужно переопределить виртуальные методы базового класса:

virtual bool HasActions(const TArray<UObject*>& InObjects) const override;

Движок вызывает HasActions, чтобы проверить, есть ли у класса функционал, который может быть выполнен для выделенного объекта.

virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;

GetActions будет вызвана, если функция HasActions возвращает true. В этой функции мы получаем ссылку на FMenuBuilder, чтобы создать свои пункты в контекстном меню.

virtual FText GetName() const override; — Возвращает имя типа объекта. Используется для подписи группы пункта в контекстом меню, а также имя ассета, которое будет отображаться в контекстном меню при создании ассета.

virtual UClass* GetSupportedClass() const override; — Возвращает инстанс UClass класса, для которого создается пункт меню.

virtual FColor GetTypeColor() const override; — Возвращает цвет, который будет применяться для данного типа ассета в контент браузере.

virtual uint32 GetCategories() override; — Возвращает категорию, которая соответствует ассету, то есть в какой категории контекстного меню будет размещаться ассет при создании.

Реализация методов в cpp файле

В нашем случае метод HasActions должен возвращать true, так как у нас есть функционал по добавлению пунктов меню в GetActions. Как говорилось выше, если метод HasActions будет возвращать false, выполнение кода не дойдет до функции GetActions.

bool FBlendSpaceDataActions::HasActions(const TArray<UObject*>& InObjects) const
{
        return true;
}

Мы расширяем функционал для BlendSpace ассетов, значит, нужно вернуть инстанс класса UBlendSpace; если разрабатывать класс для своего ассета, тогда, соответственно, нужно возвращать инстанс класса для своего ассета.

UClass* FBlendSpaceDataActions::GetSupportedClass() const
{
        return UBlendSpace::StaticClass();
}

В методе GetTypeColor вернем стандартный цвет для BlendSpace ассета.

FColor FBlendSpaceDataActions::GetTypeColor() const
{
        return FColor(255, 168, 111);
}

Так как BlendSpace ассет находится в контекстном меню в категории Animation, функция GetCategory должна возвращать Animation группу.

uint32 FBlendSpaceDataActions::GetCategories()
{
        return EAssetTypeCategories::Animation;
}

В GetName вернем имя ассета

FText FBlendSpaceDataActions::GetName() const
{
        return FText::FromString(TEXT("Blend Space"));
}

Теперь переопределим метод GetActions, в котором, собственно, будем добавлять наш новый пункт меню и добавим обработку нажатия на него.

void FBlendSpaceDataActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
        auto AnimAssets = GetTypedWeakObjectPtrs<UBlendSpace>(InObjects);

        MenuBuilder.AddMenuEntry(
                FText::FromString("Export BlendSpace Data"),
                FText::FromString("Export BlendSpace Data to file"),
                FSlateIcon(FEditorStyle::GetStyleSetName(),
                        "LevelEditor.ViewOptions"),
                FUIAction(
                        FExecuteAction::CreateSP(this, &FBlendSpaceDataActions::CustomAssetContext_Clicked, AnimAssets),
                        FCanExecuteAction()
                ));
}

Что конкретно происходит в этом методе:
Для начала мы конвертируем входной параметр InObjects, при помощи GetTypedWeakObjectPtrs<UBlendSpace>, в массив необходимых нам типов данных, а именно — TArray<TWeakObjectPtr<UBlendSpace>>. Этот массив мы передадим в наш метод обработки нажатия по пункту меню, чтобы в дальнейшем получить необходимые данные из BlendSpace ассета и записать в файл.

Дальше добавляем сам пункт меню через AddMenuEntry, используя MenuBuilder, переданный в GetActions.

FText::FromString("Export BlendSpace Data") — название пункта меню;
FText::FromString("Export BlendSpace Data to file") – текст тултипа;
FSlateIcon(FEditorStyle::GetStyleSetName(),"LevelEditor.ViewOptions") – устанавливаем иконку. Я использовал стандартный стиль эдитора и его иконку ViewOptions.
FUIAction(FExecuteAction::CreateSP(this,&FBlendSpaceDataActions::CustomAssetContext_Clicked, AnimAssets), FCanExecuteAction()) – обработчик нажатия на пункт меню. FExecuteAction::CreateSP хранит указатель на метод и данные для параметра. Этот метод будет выполняться каждый раз, когда пункт меню будет нажат.

Также необходимо добавить заголовочные файлы, в которых есть описание FMenuBuilder и FEditorStyle:

#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "EditorStyleSet.h"
#include <AssetTypeActions_Base.h>

Теперь создадим пустой метод CustomAssetContext_Clicked, который мы передали в CreateSP. Немного позже мы заполним его функционалом.

void FBlendSpaceDataActions::CustomAssetContext_Clicked(TArray<TWeakObjectPtr<UBlendSpace>> Objects)
{
}

Основная часть для добавления пункта меню готова. Осталось зарегистрировать наш класс во время загрузки UE4. Если проект был назван как в начале статьи, необходимо открыть базовый файл плагина BlendSpaceDataExporter.cpp и в методе StartupModule добавить код для регистрации нашего класса.

void FBlendSpaceDataExporterModule::StartupModule()
{
        IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
        AssetTools.RegisterAssetTypeActions(MakeShareable(new FBlendSpaceDataActions));
}

Добавление зависимостей

Наш класс зарегистрирован. Если сейчас скомпилировать проект, появятся ошибки по типу:

unresolved external symbol "__declspec(dllimport) private: static class UClass * __cdecl UEditorLoadingSavingSettings::GetPrivateStaticClass(void)

unresolved external symbol "__declspec(dllimport) public: static class FAssetEditorManager & __cdecl FAssetEditorManager::Get(void)

unresolved external symbol "__declspec(dllimport) public: bool __cdecl FAssetEditorManager::OpenEditorForAsset

unresolved external symbol "__declspec(dllimport) public: bool __cdecl FAssetEditorManager::OpenEditorForAssets

unresolved external symbol "__declspec(dllimport) public: static class TSharedRef<class FSimpleAssetEditor,0> __cdecl FSimpleAssetEditor::CreateEditor

unresolved external symbol "__declspec(dllimport) public: static class FName const & __cdecl FEditorStyle::GetStyleSetName(void)

Это ошибки линковки, то есть компилятор не видит библиотек/модулей с реализаций этих классов: UEditorLoadingSavingSettings, FAssetEditorManager, FEditorStyle. Чтобы узнать, какие модули нужно подключить к проекту, необходимо открыть документацию и в поиске ввести имя класса. На странице описания класса можно найти название модуля, в котором реализован этот класс.

Добавим модули к нашему проекту. Откроем файл BlendSpaceDataExporter.Build.cs и в PublicDependencyModuleNames добавим модули UnrealEd, EditorStyle. Если сейчас скомпилировать проект, все должно пройти без ошибок. Давайте сразу же добавим остальные зависимости, которые необходимы нам для проекта — DesktopPlatform, Json, JsonUtilities.

Сохранение данных из BlendSpace в json файл

Запустив UE4, создайте BlendSpace ассет и нажмите на нем правой кнопкой. Мы увидим новый добавленный пункт меню.

Теперь добавим функционал для нашего меню и будем добавлять его в созданный ранее метод CustomAssetContext_Clicked:

void FBlendSpaceDataActions::CustomAssetContext_Clicked(TArray<TWeakObjectPtr<UBlendSpace>> Objects)
{
        for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt)
        {
                auto BlendSpace = (*ObjIt).Get();
                if (BlendSpace)
                {
                        TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
                        TSharedPtr<FJsonObject> JsonObjectAxes = MakeShareable(new FJsonObject);
                        for (int32 AxisIndex = 0; AxisIndex < 2; ++AxisIndex)
                        {
                               TSharedPtr<FJsonObject> JsonObjectAxle = MakeShareable(new FJsonObject);
                               JsonObjectAxle->SetNumberField("min", BlendSpace->GetBlendParameter(AxisIndex).Min);
                               JsonObjectAxle->SetNumberField("max", BlendSpace->GetBlendParameter(AxisIndex).Max);
                               JsonObjectAxes->SetObjectField(BlendSpace->GetBlendParameter(AxisIndex).DisplayName, JsonObjectAxle);
                        }
                        JsonObject->SetObjectField("axes", JsonObjectAxes);


                        TSharedPtr<FJsonObject> JsonObjectAnimation = MakeShareable(new FJsonObject);
                        for (const FBlendSample& BlendSample : BlendSpace->GetBlendSamples())
                        {
                               FString position = FString::SanitizeFloat(BlendSample.SampleValue.X) + " " + FString::SanitizeFloat(BlendSample.SampleValue.Y);
                               JsonObjectAnimation->SetStringField(BlendSample.Animation->GetName(), position);
                        }
                        JsonObject->SetObjectField("animations", JsonObjectAnimation);

                        FString content;
                        TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&content);
                        FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);

                        TArray<FString> SaveFilenames;
                        FString FileTypes = TEXT("files|*.txt|All files|*.*");
                        FString DefaultFilename = BlendSpace->GetName() + ".txt";
                        FDesktopPlatformModule::Get()->SaveFileDialog(FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), "Save as ...",
                               FPaths::ProjectDir(), DefaultFilename, FileTypes, EFileDialogFlags::None, SaveFilenames);
                        FFileHelper::SaveStringToFile(content, *SaveFilenames.Last());
                }
        }
}

В наш метод передается массив выделенных BlendSpace ассетов. Нужно пройтись по массиву и получить данные из BlendSpace. Сперва получим данные о блэнд сетке, где заданы позиции анимации. У нас две оси. Через BlendSpace->GetBlendParameter, передав индекс оси, можно получить параметры этой оси (имя, максимальное значение и минимальное значение). Считав эти данные, запишем их в объект JSON.

for (int32 AxisIndex = 0; AxisIndex < 2; ++AxisIndex)
{
        TSharedPtr<FJsonObject> JsonObjectAxle = MakeShareable(new FJsonObject);
        JsonObjectAxle->SetNumberField("min", BlendSpace->GetBlendParameter(AxisIndex).Min);
        JsonObjectAxle->SetNumberField("max", BlendSpace->GetBlendParameter(AxisIndex).Max);
        JsonObjectAxes->SetObjectField(BlendSpace->GetBlendParameter(AxisIndex).DisplayName, JsonObjectAxle);
}

Теперь считаем данные о самих анимациях — какая анимация и в каких координатах расположена. Необходимые данные находятся в BlendSpace->GetBlendSamples()

for (const FBlendSample& BlendSample : BlendSpace->GetBlendSamples())
{
        FString position = FString::SanitizeFloat(BlendSample.SampleValue.X) + " " + FString::SanitizeFloat(BlendSample.SampleValue.Y);
        JsonObjectAnimation->SetStringField(BlendSample.Animation->GetName(), position);
}

После создания JSON структуры необходимо сериализовать эти данные в строку, чтобы сохранить в файл.

FString content;
TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&content);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);

После того как все данные готовы для записи, необходимо вызвать диалоговое окно для сохранения. Вызов стандартных диалоговых окон реализованы в модуле FDesktopPlatformModule. Нам необходима функция SaveFileDialog.

FDesktopPlatformModule::Get()->SaveFileDialog( FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), "Save as ...",

                FPaths::ProjectDir(), DefaultFilename, FileTypes, EFileDialogFlags::None, SaveFilenames);

Описание параметров метода:

FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr) — задает родителя окна, если в метод FindBestParentWindowHandleForDialogs передать nullptr, тогда установится главное окно MainFrame. Вторым параметром указывает текст заголовка окна.

FPaths::ProjectDir() — устанавливаем директорию по умолчанию.

DefaultFilename — имя файла по умолчанию. Мы передаем сюда имя ассета.

FileTypes — типы файлов, которые могут отображаться в диалоговом окне.

EFileDialogFlags::None — устанавливаем флаг, что может выделить только один файл в диалоговом окне.

SaveFilenames — в эту переменную сохраниться путь и имя файла, куда нужно будет сохранить наши данные.

FFileHelper::SaveStringToFile(content, *SaveFilenames.Last()); — Сохранение наших данных в файл, который мы указали в диалоговом окне.

Не забудьте добавить заголовочные файлы:

#include "Misc/FileHelper.h"
#include "DesktopPlatformModule.h"

На этом все, можно скомпилировать код. Если все сделали верно, ошибок быть не должно. Можно запустить UE4 и протестировать функционал. Создайте BlendSpace, настройте анимации в нем и попробуйте экспортировать данные. Должен получиться файл в таком формате:

{
        "axes":
        {
                "horizontal":
                {
                        "min": 0,
                        "max": 100
                },
                "vertical":
                {
                        "min": 0,
                        "max": 100
                }
        },
        "animations":
        {
                "Idle_Rifle_Hip": "50.0 50.0",
                "Walk_Bwd_Rifle_Ironsights": "50.0 100.0",
                "Walk_Fwd_Rifle_Ironsights": "50.0 0.0",
                "Walk_Lt_Rifle_Ironsights": "100.0 50.0",
                "Walk_Rt_Rifle_Ironsights": "0.0 50.0"
        }
}

Код плагина можно найти на github: https://github.com/MafiyCGD/BlendSpaceDataExporter

Текст: Антон Малицкий

Recent Posts

10 инсайдерских советов для художников, которые хотят попасть в игровую индустрию

13/05/2019
Суть этой статьи — дать знания, которые будут актуальны независимо от того, прошли ли вы курсы, выучились сами, либо уже работаете в индустрии.

3D рендеринг: отрисовка сцены

08/05/2019
Рассмотрим на практике, как отрисовывается сцена на GPU, используя знания, полученные в первой части статьи. Внутри — интерактивное демо, в котором можно “пощупать” кадр прямо в браузере.

Этапы создания окружения во время разработки игры

06/05/2019
Рассказываем о ключевых этапах создания окружения для игры ААА-класса и о том, какие ассеты обычно для этого используются.