插件设置
由于是用在编辑器的插件,Type 需要设置为 Editor。插件可能会改写、注入和拦截一些底层类和配置,因此加载阶段 LoadingPhase 建议提前到 PreDefault。这样加载时机就在引擎的核心系统(如 CoreUObject)加载之后,但在引擎的大部分普通模块和项目本身的代码加载之前。
"Modules": [
{
"Name": "EditorHelper",
"Type": "Editor",
"LoadingPhase": "PreDefault"
}
]
Asset 和 Actor
资产 Asset 和 Actor 区别大概就是前者主要面对是 Content Browser 里,后者主要应用在游戏的 Level 里。
- Asset 对应的
AssetActionUtility - Actor 对应的
ActorActionUtility
在编辑器中创建 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。
有个曲线修复的方法:
- 去插件的
Content Browswer文件夹(不是项目的); - 右键
EditorUtilities中选择EditorUtilityBlueprint; - 选择创建
AssetActionUtility,就有一个对应的蓝图; - 打开,将父类设置为:
UAssetActionHelper(前面的创建的 C++ 类);
现在,应该就可以看到 ScriptedAssetAction。

点击就可以在关卡上看到红色的 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 提供了一些操作库:
UEditorUtilityLibraryUEditorAssetLibrary
#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;
}
可以根据需要,拓展,包括更加细致的子类处理。