Привет, Хабр! В этой статье я поделюсь своим опытом создания утилит в 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. Если у вас есть вопросы или свои решения подобных задач — пишите в комментариях!
MMadmer
Давай, братан, хорош, пили еще, контент в кайф!