Skip to content

UE5 编辑器插件开发:操作 Assets

发布于  at 09:56 PM

插件设置

由于是用在编辑器的插件,Type 需要设置为 Editor。插件可能会改写、注入和拦截一些底层类和配置,因此加载阶段 LoadingPhase 建议提前到 PreDefault。这样加载时机就在引擎的核心系统(如 CoreUObject)加载之后,但在引擎的大部分普通模块和项目本身的代码加载之前。

"Modules": [
	{
		"Name": "EditorHelper",
		"Type": "Editor",
		"LoadingPhase": "PreDefault"
	}
]

Asset 和 Actor

资产 Asset 和 Actor 区别大概就是前者主要面对是 Content Browser 里,后者主要应用在游戏的 Level 里。

在编辑器中创建 C++ 类,AssetActionUtility

添加对应的模块

Rider 会自动提示,添加 Blutility

PublicDependencyModuleNames.AddRange(
	new string[]
	{
		"Core", "Blutility",
	}
);

第一个消息

测试一下是否正常:

UCLASS()
class EDITORHELPER_API UAssetActionHelper : public UAssetActionUtility
{
	GENERATED_BODY()

public:
	UFUNCTION(CallInEditor)
	void TestFunc();
};

UFUNCTION(CallInEditor) 用于注册。

简单的打印一下消息:

void UAssetActionHelper::TestFunc()
{
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Red, "Hello World!");
	}
}

Bug

Bug 好像存在好久了,就是编辑器看不到 Scripted Asset Action

有个曲线修复的方法:

  1. 去插件的 Content Browswer 文件夹(不是项目的);
  2. 右键 EditorUtilities 中选择 EditorUtilityBlueprint
  3. 选择创建 AssetActionUtility,就有一个对应的蓝图;
  4. 打开,将父类设置为:UAssetActionHelper(前面的创建的 C++ 类);

现在,应该就可以看到 ScriptedAssetAction

UE5编辑器中的Scripted Asset Action 菜单

点击就可以在关卡上看到红色的 Hello World!。(过于明显,就不截图了)

封装打印函数

可以在 Public 中创建一个 DebugHelper.h

#pragma once

inline void Print(const FString& Message, const FColor& Color)
{
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(-1, 8.f, Color, Message);
	}
}

inline void PrintLog(const FString& Message)
{
	UE_LOG(LogTemp, Warning, TEXT("%s"), *Message);
}

inline 是有必要的,避免因为触犯了 C++ 的 ODR(One Definition Rule,唯一定义原则)导致链接错误。

因为,#pragma once 只能保证在同一个 .cpp 中不重复,不能保证在多个 .cpp 不重复。如果两个 .cpp 同时包含该头文件,编译成两个 .obj,在链接的阶段,就会因为出现两个定义而导致违背 ODR。

这样,TestFunc 就可以改成:

#include "AssetActions/AssetActionHelper.h"
#include "DebugHelper.h"

void UAssetActionHelper::TestFunc()
{
	Print("Hello World!", FColor::Red);
	PrintLog("Working...");
}

批量复制

UE 提供了一些操作库:


#include "AssetActions/AssetActionHelper.h"
#include "DebugHelper.h"
#include "EditorUtilityLibrary.h"
#include "Subsystems/EditorAssetSubsystem.h"

void UAssetActionHelper::DuplicateAssets(const int32 NumOfDuplicates)
{
	if (NumOfDuplicates <= 0)
	{
		Print(TEXT("Please enter a VALID number"), FColor::Red);
		return;
	}

	TArray<FAssetData> SelectedAssetsData = UEditorUtilityLibrary::GetSelectedAssetData();
	uint32 Counter = 0;

	UEditorAssetSubsystem* EditorAssetSubsystem = GEditor->GetEditorSubsystem<UEditorAssetSubsystem>();

	for (const FAssetData& AssetData : SelectedAssetsData)
	{
		for (int32 i = 0; i < NumOfDuplicates; i++)
		{
			const FString SourceAssetPath = AssetData.GetSoftObjectPath().ToString();
			const FString NewDuplicatedAssetName = AssetData.AssetName.ToString() + TEXT("_") + FString::FromInt(i + 1);
			const FString NewPathName = FPaths::Combine(AssetData.PackageName.ToString(), NewDuplicatedAssetName);

			if (EditorAssetSubsystem->DuplicateAsset(SourceAssetPath, NewPathName))
			{
				EditorAssetSubsystem->SaveAsset(NewPathName, false);
				++Counter;
			}
		}
	}

	if (Counter > 0)
	{
		Print(FString::Printf(TEXT("Successfully duplicated assets %d files."), Counter), FColor::Green);
	}
}

因为用到 GEditor,所以需要加载 UnrealEd 模块:

PrivateDependencyModuleNames.AddRange(
	new string[]
	{
		"CoreUObject",
		"Engine",
		"Slate",
		"SlateCore",
		"UnrealEd",
	}
);

脚本的效果

复制:

复制菜单

输入需要复制的数量:

复制的数量

会创建一个同名文件夹,放入复制的资产:

批量复制的资产

编辑器消息

现在的消息提示要么是在关卡编辑器里,要么是在 Log 里,显得很不正式,也有点奇怪。要获得一种更加原生的编辑器消息体验,我们可以使用 FMessageDialog

可以把第一个 Print 换成:

FMessageDialog::Open(EAppMsgType::Ok, FText::FromString("Please enter a VALID number"), FText::FromString("Warning"));

编辑器消息对话

当然,也可以封装一下:

inline EAppReturnType::Type ShowMsgDialog(EAppMsgType::Type MsgType, const FString& Message, bool bShowMsgAsWarning = true)
{
	if (bShowMsgAsWarning)
	{
		return FMessageDialog::Open(MsgType, FText::FromString(Message), FText::FromString("Warning"));
	}

	return FMessageDialog::Open(MsgType, FText::FromString(Message));
}

同样,定制一个新的提示:

#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"

inline void ShowNotifyInfo(const FString& Message)
{
	FNotificationInfo NotifyInfo(FText::FromString(Message));
	NotifyInfo.bUseLargeFont = true;
	NotifyInfo.FadeOutDuration = 7.f;

	FSlateNotificationManager::Get().AddNotification(NotifyInfo);
}

这样可以改成:

if (Counter > 0)
{
	ShowNotifyInfo(FString::Printf(TEXT("Successfully duplicated assets %d files."), Counter));
}

复制成功后的消息提示:

消息提示

资产重命名:添加前缀

UAssetUtilityLibrary 还提供了很多资产操作的工具方法。

例如,UE 资产命名,我们通常会使用前缀来区分不同类型的资产,官方也有推荐的前缀。像我们最熟悉的蓝图,通常就使用 BP_ 作为前缀。

就可以开发一个自动加前缀的脚本:

// ...
	UFUNCTION(CallInEditor)
	void AddPrefixes();

private:
	TMap<UClass*, FString> PrefixMap =
	{
		{UBlueprint::StaticClass(), TEXT("BP_")}, // 自行补充
	};
// ...

批量操作资产的逻辑是类似的:

void UAssetActionHelper::AddPrefixes()
{
	TArray<UObject*> SelectedObjects = UEditorUtilityLibrary::GetSelectedAssets();
	uint32 Counter = 0;

	for (UObject* Object : SelectedObjects)
	{
		if (!IsValid(Object)) continue;

		const FString* PrefixFound = PrefixMap.Find(Object->GetClass());
		if (!PrefixFound || PrefixFound->IsEmpty())
		{
			Print(FString::Printf(TEXT("Failed to find prefix for class %s"),
				*Object->GetClass()->GetName()), FColor::Red);
			continue;
		}

		FString OldName = Object->GetName();
		if (OldName.StartsWith(*PrefixFound))
		{
			Print(FString::Printf(TEXT("%s already has prefix added"), *OldName), FColor::Green);
			continue;
		}

		const FString NewNameWithPrefix = *PrefixFound + OldName;
		UEditorUtilityLibrary::RenameAsset(Object, NewNameWithPrefix);
		++Counter;
	}
	
	if (Counter > 0)
	{
		ShowNotifyInfo(FString::Printf(TEXT("Successfully renamed %s assets"), *FString::FromInt(Counter)));
	}
}

当然,这只是比较粗糙的添加前缀。蓝图也分很多类型,例如:AC_WBP_ABP_

为了让代码更加完善,可以进一步拓展。例如:将 TMap 更换为函数:

/** 根据资产的 Class 获取对应的命名规范前缀 */
static const FString* GetPrefixForClass(const UClass* InClass);

不过添加的类型越多,就意味着要引入更多的头文件(注意:以下只是一部分):

#include "Engine/VolumeTexture.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Blueprint/UserWidget.h"
#include "Engine/UserDefinedEnum.h"

const FString* UAssetActionHelper::GetPrefixForClass(const UClass* InClass)
{
	if (!InClass) return nullptr;
	
    static const TMap<UClass*, FString> PrefixMap =
    {
		// ----- 材质与渲染 (Materials & Rendering) -----
		{UTexture2D::StaticClass(), TEXT("T_")},
		{UMaterial::StaticClass(), TEXT("M_")},
		{UMaterialInstanceConstant::StaticClass(), TEXT("MI_")},
		{UMaterialInstanceDynamic::StaticClass(), TEXT("MID_")},
		{UVolumeTexture::StaticClass(), TEXT("HDRI_")},
    	
		// ----- 模型/网格体 (Meshes) -----
		{USkeletalMesh::StaticClass(), TEXT("SK_")},
		{UStaticMesh::StaticClass(), TEXT("SM_")},

		// ----- 蓝图与核心架构 (Blueprints & Core) -----
		{UBlueprint::StaticClass(), TEXT("BP_")},
		{UActorComponent::StaticClass(), TEXT("AC_")},
		{UUserWidget::StaticClass(), TEXT("WBP_")},
		{UUserDefinedEnum::StaticClass(), TEXT("E_")},

		// ----- 数据 (Data) -----
		{UCurveTable::StaticClass(), TEXT("CT_")},
		{UDataTable::StaticClass(), TEXT("DT_")},

		// ----- 骨骼动画 (Animation) -----
		{USkeleton::StaticClass(), TEXT("SKEL_")},
		{UAnimBlueprint::StaticClass(), TEXT("ABP_")},
		{UAnimMontage::StaticClass(), TEXT("AM_")},
		{UAnimSequence::StaticClass(), TEXT("AS_")},
		{UBlendSpace::StaticClass(), TEXT("BS_")},
	};
	
    if (const FString* FoundPrefix = PrefixMap.Find(InClass))
    {
        return FoundPrefix;
    }
	
    for (const auto& Pair : PrefixMap)
    {
        if (InClass->IsChildOf(Pair.Key))
        {
            return &Pair.Value;
        }
    }

    return nullptr;
}

可以根据需要,拓展,包括更加细致的子类处理。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

上一篇
UE5 编辑器插件开发:定制菜单
下一篇
相机投影矩阵与内外参计算