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