Привет, Хабр! В этой статье я поделюсь своим опытом создания утилит в Unreal Engine, которые автоматизируют процесс генерации Actor Blueprint и Data Asset. Эти утилиты значительно упрощают р��боту дизайнерам уровней, помогая сократить время на рутинные задачи и минимизировать ошибки, а также могут быть полезны в широком спектре задач, связанных с разработкой.
Мы рассмотрим, как использовать Editor Utility Widgets на практике, чтобы упростить работу в редакторе. Основная часть будет выполнена в Blueprint, но для решения отдельных задач нам также понадобятся функции на C++. Помимо этого, я расскажу о фабриках ассетов и Subobject в UE.
Задача: Генератор префабов
Во время работы над проектом (на этапе DesignTime, то есть при работе непосредственно с редактором) появилась задача — часто и быстро создавать префабы для ускорения проектировки блокаута. Ручное создание и настройка таких объектов занимали слишком много времени, что замедляло процесс разработки и повышало вероятность ошибок.
Ранее я уже разрабатывал утилиту, которая позволяла заменять выбранные Static Mesh и все их копии на уровне на сгенерированные Actor-классы. Это решение оказалось полезным, так как позволило сделать объекты интерактивными и минимизировало ручную работу на уровне. На основе этого опыта я решил создать расширенный инструмент, который автоматизирует сборку префабов;
Выбираем объекты на уровне
Создаем кастомный Actor-класс, сохраняя все ключевые свойства:
Позицию, поворот и масштаб (Transform относительно "центра" выделенной области).
Привязанные меши и материалы.
Сохраняем созданный Blueprint Class в Content Browser для дальнейшего использования.
В этой статье мы начнем с базового функционала — генерации Actor Blueprint и создадим Data Asset; Во второй части статьи (по результатам опроса в конце статьи) я разберу несколько практических примеров Editor Utility виджетов.
Создаем Editor Utility
Подготовка движка
Для Editor Scripting в UE предварительно нужно включить плагин Editor Scripting Utilities, и, если он до этого не был включен, перезапустите движок.

Создаем Editor Utility Widget Blueprint

Добавляем переменные, которые понадобятся в будущем:

Для данной утилиты, я сделал такой дизайн:

Обратите внимание: поля AssetName, Reparent Class, Save Path и Status являются Single Property View; Этот виджет является частью Editor Scripting движка и позволяет автоматически просматривать и изменять значение Property у объекта, в нашем случае этого же виджета.


Создание Actor через фабрики и Subobject
Теперь приступим к сложной части статьи - Генерация Actor. Для этого мы используем Asset Tools и метод CreateAsset.

Однако CreateAsset требует фабрику класса, который мы создаем (Factory class), которая недоступна в Blueprint. Для обхода этой проблемы я написал функцию CreateObject на C++:
.h:
UFUNCTION(BlueprintPure, Category = "ServiceFunctions", Meta = (DisplayName = "Create Object", Keywords = "make instance", DeterminesOutputType = "Class", DynamicOutputParam = "Object")) static void CreateObject(TSubclassOf<UObject> Class, UObject*& Object);
.cpp:
void UHabrArticleBPLibrary::CreateObject(TSubclassOf<UObject> Class, UObject*& Object) { if (!Class) { UE_LOG(LogTemp, Warning, TEXT("CreateObject: Invalid class provided.")); Object = nullptr; return; } Object = NewObject<UObject>(GetTransientPackage(), Class); if (!Object) { UE_LOG(LogTemp, Error, TEXT("CreateObject: Failed to create an instance of %s."), *Class->GetName()); } }
Эта функция позволяет создавать объекты любого класса, передавая его как аргумент. После этого, используя Supported Class из фабрики, я смог создать Actor и применить метод Reparent для изменения его родительского класса.
Ниже вы можете ознакомиться с Blueprint функцией, которая создает, сохраняет и записывает созданный ассет в Uobject переменную.
Теперь, подключив простой код кнопок Refresh и Create Actor мы уже можем протестировать Editor Utility Widget


Итак, мы создали актор, но мы так же можем и создать Child от уже существующего класса.
Для этого нам нужно:
Получить Blueprint Asset из созданного объекта
Вызвать Reparent Blueprint


Работа с Subobject Data Subsystem
Для добавления в Actor Subobject (подобъектов) необходимо использовать Subobject Data Subsystem. Это подсистема Unreal Engine, которая позволяет управлять компонентами объекта, их данными и привязками.
Справка: разница между Component и Subobject
Основные шаги работы:
Сбор данных subobject для нашего созданного Actor
Создание subobject
Добавление и регистрация subobject в Actor



Конвертация Data Asset в подходящий формат
Следующей задачей было создание и сохранение Data Asset. С помощью вышеописанного метода (CreateObject) и CreateAsset я смог создать объект типа UDataAsset. Однако возникла проблема: абстрактный класс UDataAsset при сохранении обнуляется и вызывает ошибку.
Наверное все, кто работал с Data Asset знают, что в Unreal Engine для работы с Data Asset используется базовый класс UPrimaryDataAsset. В Content Browser есть кнопка ConvertDataAssetToDifferentType, которая выполняет конвертацию. Покопавшись в исходном коде этой функции (AssetTypeActions_DataAsset.cpp) выяснилось, что под капотом создается новый объект, а старый удаляется.
На основе этого я написал следующую библиотеку функций:
.h:
#pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "UObject/ObjectMacros.h" #include "UObject/Object.h" #if WITH_EDITOR #include "Editor.h" #include "ObjectTools.h" #include "Engine/Engine.h" #endif #include "DataAssetActionsFunctionLibrary.generated.h" UCLASS() class ARTICLE_PROJECT_API UDataAssetActionsFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "DataAsset|Editor", meta = (CallInEditor = "true", DisplayName = "Convert DataAsset To Different Class (Editor Only)")) static UDataAsset* ConvertDataAsset(UObject* SourceObject, TSubclassOf<UDataAsset> TargetClass); };
.cpp:
#include "DataAssetActionsFunctionLibrary.h" #include "Engine/DataAsset.h" #if WITH_EDITOR #include "UObject/UObjectGlobals.h" #include "UObject/Package.h" #include "Misc/PackageName.h" #include "Editor.h" #include "Engine/Engine.h" #include "ObjectTools.h" #endif UDataAsset* UDataAssetActionsFunctionLibrary::ConvertDataAsset(UObject* SourceObject, TSubclassOf<UDataAsset> TargetClass) { #if WITH_EDITOR if (!SourceObject) { UE_LOG(LogTemp, Warning, TEXT("ConvertDataAsset: SourceObject is null.")); return nullptr; } if (!TargetClass) { UE_LOG(LogTemp, Warning, TEXT("ConvertDataAsset: TargetClass is null.")); return nullptr; } // Попробуем привести SourceObject к UDataAsset UDataAsset* OldDataAsset = Cast<UDataAsset>(SourceObject); if (!OldDataAsset) { UE_LOG(LogTemp, Warning, TEXT("ConvertDataAsset: SourceObject is not a UDataAsset.")); return nullptr; } if (!OldDataAsset->IsValidLowLevel()) { UE_LOG(LogTemp, Warning, TEXT("ConvertDataAsset: OldDataAsset is not valid.")); return nullptr; } // Сохраняем оригинальные имя и Outer FName OldName = OldDataAsset->GetFName(); UObject* Outer = OldDataAsset->GetOuter(); // Переименовываем старый объект во временный пакет OldDataAsset->Rename(nullptr, GetTransientPackage(), REN_DoNotDirty | REN_DontCreateRedirectors); // Создаём новый объект типа UDataAsset (или наследника), // передавая в шаблон UDataAsset, а вторым параметром - TargetClass (UClass*) UDataAsset* NewDataAsset = NewObject<UDataAsset>( Outer, TargetClass, // <-- ВАЖНО: передаём TargetClass, а не *TargetClass OldName, OldDataAsset->GetFlags() ); if (!NewDataAsset) { UE_LOG(LogTemp, Warning, TEXT("ConvertDataAsset: Failed to create new data asset of class %s."), *TargetClass->GetName()); return nullptr; } // Копируем свойства со старого объекта на новый UEngine::FCopyPropertiesForUnrelatedObjectsParams CopyParams; CopyParams.bNotifyObjectReplacement = true; UEngine::CopyPropertiesForUnrelatedObjects(OldDataAsset, NewDataAsset, CopyParams); // Помечаем пакет "грязным" для сохранения NewDataAsset->MarkPackageDirty(); // Перенаправляем все ссылки со старого объекта на новый (Editor-only) { bool bShowDeleteConfirmation = false; TArray<UObject*> OldObjects; OldObjects.Add(OldDataAsset); ObjectTools::ConsolidateObjects(NewDataAsset, OldObjects, bShowDeleteConfirmation); } return NewDataAsset; #else // !WITH_EDITOR UE_LOG(LogTemp, Warning, TEXT("ConvertDataAsset can only be used in Editor builds.")); return nullptr; #endif }
Эта функция копирует данные из исходного объекта в новый, а также заменяет все ссылки на старый объект.

Задержка в данном случае дает Asset Registry время на
Регистрацию нового ассета.
Синхронизацию данных с Content Browser.
Конечно в C++ можно вызвать
FAssetRegistryModule::AssetCreated(NewAsset); NewAsset->MarkPackageDirty(); UPackage::SavePackage(NewAsset->GetOutermost(), nullptr, RF_Standalone, *PackageFileName);
Но мне не хотелось добавлять Asset Registry в Build.cs, а Async Edittor Delay никак не влияет на производительность виджета, в отличие от обычного Latent Delay.
Заключение
В статье мы рассмотрели:
Создание акторов через фабрики и Subobject Data Subsystem.
Конвертацию Data Asset, подсмотрев исходный код движка.
Эти инструменты и подходы позволили значительно ускорить создание контента и добавить гибкости в разработку.
Надеюсь, мой опыт будет полезен другим разработчикам Unreal Engine. Если у вас есть вопросы или свои решения подобных задач — пишите в комментариях!
