Добавляем экспорт данных из ассета
Недавно мне понадобилось эк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
Текст: Антон Малицкий