Привет, Хабр! В этой статье я поделюсь своим опытом создания утилит в Unreal Engine, которые автоматизируют процесс генерации Actor Blueprint и Data Asset. Эти утилиты значительно упрощают работу дизайнерам уровней, помогая сократить время на рутинные задачи и минимизировать ошибки, а также могут быть полезны в широком спектре задач, связанных с разработкой.

Мы рассмотрим, как использовать Editor Utility Widgets на практике, чтобы упростить работу в редакторе. Основная часть будет выполнена в Blueprint, но для решения отдельных задач нам также понадобятся функции на C++. Помимо этого, я расскажу о фабриках ассетов и Subobject в UE.


Задача: Генератор префабов

Во время работы над проектом (на этапе DesignTime, то есть при работе непосредственно с редактором) появилась задача — часто и быстро создавать префабы для ускорения проектировки блокаута. Ручное создание и настройка таких объектов занимали слишком много времени, что замедляло процесс разработки и повышало вероятность ошибок.

Ранее я уже разрабатывал утилиту, которая позволяла заменять выбранные Static Mesh и все их копии на уровне на сгенерированные Actor-классы. Это решение оказалось полезным, так как позволило сделать объекты интерактивными и минимизировало ручную работу на уровне. На основе этого опыта я решил создать расширенный инструмент, который автоматизирует сборку префабов;

  1. Выбираем объекты на уровне

  2. Создаем кастомный Actor-класс, сохраняя все ключевые свойства:

    • Позицию, поворот и масштаб (Transform относительно "центра" выделенной области).

    • Привязанные меши и материалы.

  3. Сохраняем созданный Blueprint Class в Content Browser для дальнейшего использования.

В этой статье мы начнем с базового функционала — генерации Actor Blueprint и создадим Data Asset; Во второй части статьи (по результатам опроса в конце статьи) я разберу несколько практических примеров Editor Utility виджетов.


Создаем Editor Utility

Подготовка движка

Для Editor Scripting в UE предварительно нужно включить плагин Editor Scripting Utilities, и, если он до этого не был включен, перезапустите движок.

Edit -> Plugins

Создаем Editor Utility Widget Blueprint

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

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

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

По умолчанию Single Property View выглядит так. Во вкладе PropertyName нужно указать название переменной (Property) объекта, из которого будет браться значение.
По умолчанию Single Property View выглядит так. Во вкладе PropertyName нужно указать название переменной (Property) объекта, из которого будет браться значение.
Для того, чтобы Single Property View начала отображать переменную виджета, на Event PreConstruct необходимо вызвать SetObject(Self)
Для того, чтобы Single Property View начала отображать переменную виджета, на Event PreConstruct необходимо вызвать SetObject(Self)

Создание 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

Мне показалось удобным по кнопке автоматически получать путь текущей папки в Content Browser
Мне показалось удобным по кнопке автоматически получать путь текущей папки в Content Browser

Итак, мы создали актор, но мы так же можем и создать Child от уже существующего класса.
Для этого нам нужно:

  1. Получить Blueprint Asset из созданного объекта

  2. Вызвать Reparent Blueprint

Pure getter функция для Blueprint Asset
Pure getter функция для Blueprint Asset

Работа с Subobject Data Subsystem

Для добавления в Actor Subobject (подобъектов) необходимо использовать Subobject Data Subsystem. Это подсистема Unreal Engine, которая позволяет управлять компонентами объекта, их данными и привязками.

Справка: разница между Component и Subobject

Основные шаги работы:

  1. Сбор данных subobject для нашего созданного Actor

  2. Создание subobject

  3. Добавление и регистрация subobject в Actor

Я решил добавить Static Mesh Component к моему созданному актору
Я решил добавить Static Mesh Component к моему созданному актору
Добавим зеленый кубик
Добавим зеленый кубик
Результат выполнения утилиты
Результат выполнения утилиты

Конвертация 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. Если у вас есть вопросы или свои решения подобных задач — пишите в комментариях!


Ссылки


Комментарии (1)


  1. MMadmer
    21.01.2025 15:03

    Давай, братан, хорош, пили еще, контент в кайф!