移动/游戏
扩展 Unreal Editor 自定义工具、面板…
在 UE 编辑器中添加 Editor Utility Widget、Blutility、详情定制、编辑器模式、资产类型操作、编辑器子系统、菜单/工具栏、资产验证等编辑器扩展时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
扩展 Unreal Editor 自定义特定
解决思路
- 阅读 `.agents/ue-project-context.md`,获取编辑器模块结构、引擎版本、团队工作流和项目约定后再给指导。
- 若需求不明确,先询问用户要做哪个区域:Editor Utility Widget、Blutility、Detail Customization、Custom Editor Mode、Asset Type Actions、Editor Subsystem、Menu/Toolbar Extension。
- 所有扩展编辑器的代码放在 `Type` 为 `Editor` 的模块中;不要在 Runtime 模块中直接包含 `UnrealEd` 或 `PropertyEditor` 头文件,除非用 `#if WITH_EDITOR` 保护。
- 编辑器模块描述使用 `{ Name: MyGameEditor, Type: Editor, LoadingPhase: PostEngineInit }` 这类配置。
- 在 `MyGameEditor.Build.cs` 的 `PrivateDependencyModuleNames.AddRange` 中加入 `Core`、`CoreUObject`、`Engine`、`UnrealEd`、`Slate`、`SlateCore`、`EditorStyle`、`PropertyEditor`、`EditorSubsystem`、`Blutility`、`ToolMenus`、`AssetTools`、`MyGame`。
- 用 `IMPLEMENT_MODULE(FMyGameEditorModule, MyGameEditor)` 定义编辑器模块,并保证 `StartupModule` 中的每个注册都在 `ShutdownModule` 中镜像注销。
- 在 `StartupModule` 中用 `FModuleManager::LoadModuleChecked<FPropertyEditorModule>(PropertyEditor)` 获取 `FPropertyEditorModule& PropMod`,调用 `PropMod.RegisterCustomClassLayout(UMyDataAsset::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FMyDataAssetCustomization::MakeInstance))`,再调用 `PropMod.NotifyCustomizationModuleChanged()`。
- 在 `StartupModule` 中调用 `UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FMyGameEditorModule::RegisterMenus))`。
- 在 `ShutdownModule` 中若 `FModuleManager::Get().IsModuleLoaded(PropertyEditor)`,调用 `FModuleManager::GetModuleChecked<FPropertyEditorModule>(PropertyEditor).UnregisterCustomClassLayout(UMyDataAsset::StaticClass()->GetFName())`。
- 在 `ShutdownModule` 中调用 `UToolMenus::UnRegisterStartupCallback(this)`,并在 `UToolMenus* TM = UToolMenus::TryGet()` 非空时调用 `TM->UnregisterOwner(this)`。
- 需要 asset type actions、editor modes、factory 的完整样板时,查阅 `references/editor-module-setup.md`。
- 创建 Editor Utility Widget:使用 `EditorUtilityWidget.h` 中的 `UEditorUtilityWidget`(继承 `UUserWidget`)做仅编辑器 UMG 面板;可在 Content Browser 右键 > Editor Utilities > Editor Utility Widget 创建 Blueprint 子类。
- 运行 Editor Utility Widget:右键 widget asset > Run Editor Utility Widget;也可在 C++ 中继承 `UEditorUtilityWidget`。
- 在 C++ Editor Utility Widget 中实现操作函数,例如遍历 `UEditorUtilityLibrary::GetSelectedAssets()`,取 `Asset->GetOutermost()->GetName()` 作为源路径,用 `UEditorAssetLibrary::RenameAsset(Src, FPaths::GetPath(Src) / Prefix + TEXT(_) + Asset->GetName())` 重命名。
- 使用 `UEditorUtilityLibrary` 函数:`GetSelectedAssets()` 获取当前选中的 Content Browser 资产;`GetSelectedAssetsOfClass(UClass*)` 按类过滤;`UEditorAssetLibrary::DeleteAsset(Path)` 删除资产;`RenameAsset` 移动/重命名;`GetSelectionSet()` 获取选中的关卡 Actor;`SyncBrowserToFolders(TArray<FString>)` 同步 Content Browser 视图。
- 程序化查询资产时使用 `FARFilter` 配合 `IAssetRegistry::GetAssets`,按类、路径或标签筛选。
- 程序化打开 widget:`GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>()->SpawnAndRegisterTab(LoadObject<UEditorUtilityWidgetBlueprint>(nullptr, TEXT("/Game/EditorWidgets/BP_MyTool")))`。
- Blutility 资产右键动作:让 `UAssetActionUtility` 子类上的任意 `UFUNCTION(CallInEditor)` 出现在 Content Browser 上下文菜单;在 Class Defaults 设置 `SupportedClasses` 按资产类型过滤。
- 实现 `UAssetActionUtility` 示例动作:继承 `AssetActionUtility.h`,在函数中遍历 `UEditorUtilityLibrary::GetSelectedAssets()`,对 `UTexture2D` 设置 `CompressionSettings = TC_EditorIcon`,调用 `MarkPackageDirty()` 和 `PostEditChange()`。
- 关卡 Actor 右键动作使用 `UActorActionUtility`,模式与 `UAssetActionUtility` 相同;两者都继承 `UEditorUtilityObject`。
- 类详情定制:实现 `IDetailCustomization` 子类,提供 `MakeInstance()`,重写 `CustomizeDetails(IDetailLayoutBuilder&)`;同时重写 `TSharedPtr<IDetailLayoutBuilder>&` 重载并保存 `WeakBuilder`,供异步 `ForceRefreshDetails` 使用。
- 在类详情定制的 `CustomizeDetails` 中使用 `EditCategory`、`AddProperty`、`AddCustomRow`、`HideCategory`;注册用 `PropMod.RegisterCustomClassLayout(UMyClass::StaticClass()->GetFName(), ...MakeInstance)`,注销用 `PropMod.UnregisterCustomClassLayout(...)`。
- 结构体详情定制:实现 `IPropertyTypeCustomization`,提供 `MakeInstance()`、`CustomizeHeader(TSharedRef<IPropertyHandle> Handle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& Utils)`、`CustomizeChildren(TSharedRef<IPropertyHandle> Handle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& Utils)`;注册用 `PropMod.RegisterCustomPropertyTypeLayout(FMyStruct::StaticStruct()->GetFName(), ...MakeInstance)`。
- 需要完整详情定制实现、Slate row 模式、`IPropertyHandle` 读写、NameContent/ValueContent 时,查阅 `references/detail-customization-patterns.md`。
- 自定义编辑器模式:继承 `EdMode.h` 中的 `FEdMode` 提供专门 viewport 交互;全局注册,且每个 viewport 同时只能有一个激活。
- 实现 `FMyEditorMode`:定义 `static const FEditorModeID EM_MyMode`,重写 `Enter()`、`Exit()`、`HandleClick(FEditorViewportClient*, HHitProxy*, const FViewportClick&)`、`MouseMove(FEditorViewportClient*, FViewport*, int32 X, int32 Y)`、`Render(const FSceneView*, FViewport*, FPrimitiveDrawInterface*)`、`UsesToolkits() const { return true; }`;在鼠标处理中用 `View->DeprojectFVector2D`(`FSceneView`)从像素坐标生成世界空间射线。
- 在 `MyEditorMode.cpp` 定义 `const FEditorModeID FMyEditorMode::EM_MyMode = TEXT(EM_MyEditorMode)`;构造函数设置 `Info = FEditorModeInfo(EM_MyMode, FText::FromString(My Editor Mode), FSlateIcon(), true)`。
- 在 `Enter()` 中调用 `FEdMode::Enter()`;若 `!Toolkit.IsValid()`,创建 `FMyEditorModeToolkit` 并调用 `Toolkit->Init(Owner->GetToolkitHost())`。
- 在 `Exit()` 中若 `Toolkit.IsValid()`,调用 `FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef())`,再调用 `FEdMode::Exit()`。
- 在 `Render()` 中先调用 `FEdMode::Render(View, Viewport, PDI)`,再用 `DrawWireSphere(PDI, FVector::ZeroVector, FLinearColor::Green, 50.f, 16, SDPG_World)` 绘制。
- 在模块生命周期注册/注销编辑器模式:`StartupModule` 用 `FEditorModeRegistry::Get().RegisterMode<FMyEditorMode>(FMyEditorMode::EM_MyMode, FText::FromString(My Mode), FSlateIcon(), true)`;`ShutdownModule` 用 `FEditorModeRegistry::Get().UnregisterMode(FMyEditorMode::EM_MyMode)`。
- 资产类型操作:继承 `AssetTypeActions_Base.h` 中的 `FAssetTypeActions_Base`,控制自定义资产类型的 Content Browser 外观和上下文菜单。
- 实现资产类型操作:`GetName()` 返回 `FText::FromString(My Asset)`;`GetTypeColor()` 返回 `FColor(200, 100, 50)`;`GetSupportedClass()` 返回 `UMyAsset::StaticClass()`;`GetCategories()` 返回 `EAssetTypeCategories::Misc`。
- 重写 `OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor)`,遍历对象,对 `UMyAsset* Asset` 调用 `MakeShareable(new FMyAssetEditor)->InitMyEditor(EToolkitMode::Standalone, EditWithinLevelEditor, Asset)`。
- 在 `StartupModule` 注册资产类型操作并保留引用,在 `ShutdownModule` 注销。
- 基于标签的资产编辑器窗口:实现 `FAssetEditorToolkit` 子类 `FMyAssetEditor`,提供 `InitMyEditor(EToolkitMode::Type Mode, TSharedPtr<IToolkitHost> Host, UMyAsset* Asset)`,实现 `GetToolkitFName()` 返回 `MyAssetEditor`、`GetBaseToolkitName()` 返回 `INVTEXT(My Asset Editor)`、`RegisterTabSpawners(const TSharedRef<FTabManager>& TabMgr)`、`UnregisterTabSpawners(const TSharedRef<FTabManager>& TabMgr)`;在 `InitMyEditor` 内调用 `InitAssetEditor()`,用 `FTabManager::NewLayout` 设置标签布局。
- 查询已打开资产编辑器:用 `GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(MyAsset, false)` 获取 `IAssetEditorInstance*`;若存在调用 `Editor->FocusWindow(MyAsset)`;关闭窗口用 `CloseWindow(EAssetEditorCloseReason::AssetEditorHostClosed)`,无参形式在 5.3 已弃用。
- 资产工厂:继承 `UFactory`,构造函数设置 `SupportedClass = UMyDataAsset::StaticClass()`、`bCreateNew = true` 使资产出现在 Add 菜单、`bEditAfterNew = true` 使创建后打开编辑器;重写 `FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)` 并返回 `NewObject<UMyDataAsset>(InParent, InClass, InName, Flags)`。
- 自定义缩略图:实现 `UMyThumbnailRenderer : UThumbnailRenderer`,重写 `Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* Target, FCanvas* Canvas, bool bAdditionalViewFamily)`;模块启动时用 `UThumbnailManager::Get().RegisterCustomRenderer(UMyDataAsset::StaticClass(), UMyThumbnailRenderer::StaticClass())` 注册。
- 编辑器子系统:继承 `EditorSubsystem.h` 中的 `UEditorSubsystem` 作为编辑器生命周期单例,自动发现无需注册;通过 `GEditor->GetEditorSubsystem<T>()` 访问。
- 实现编辑器子系统:重写 `Initialize(FSubsystemCollectionBase& Collection)`,调用 `Super::Initialize(Collection)`,用 `FEditorDelegates::PostUndoRedo.AddUObject(this, &UMyEditorSubsystem::OnPostUndoRedo)` 绑定;重写 `Deinitialize()`,用 `FEditorDelegates::PostUndoRedo.RemoveAll(this)`,再调用 `Super::Deinitialize()`。
- 使用内置子系统:`UEditorActorSubsystem` 选择/生成/删除关卡 Actor;`UEditorAssetSubsystem` 加载/保存/复制资产;`ULevelEditorSubsystem` 打开/保存/管理关卡;`UEditorUtilitySubsystem` 生成编辑器工具 widget 标签。
- 菜单/工具栏扩展用 UToolMenus(UE5+,替代 `FExtender`);在 startup callback 内注册,确保 Slate 已就绪。
- 在 `RegisterMenus()` 中用 `FToolMenuOwnerScoped OwnerScoped(this)`;主菜单用 `UToolMenus::Get()->ExtendMenu(LevelEditor.MainMenu.Window)`,再 `Menu->FindOrAddSection(MyGame).AddMenuEntry(OpenMyPanel, FText::FromString(My Panel), FText::GetEmpty(), FSlateIcon(FAppStyle::GetAppStyleSetName(), Icons.Settings), FUIAction(FExecuteAction::CreateLambda([]() { GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>()->SpawnAndRegisterTab(nullptr); })))`。
- 添加工具栏按钮:用 `UToolMenus::Get()->ExtendMenu(LevelEditor.LevelEditorToolBar.PlayToolBar)`,再 `TB->FindOrAddSection(MyTools).AddEntry(FToolMenuEntry::InitToolBarButton(MyBtn, FUIAction(FExecuteAction::CreateLambda([]() {})), FText::FromString(My Tool), FText::GetEmpty(), FSlateIcon(FAppStyle::GetAppStyleSetName(), Icons.Toolbar.Settings)))`。
- 菜单/工具栏扩展始终注销:调用 `UToolMenus::UnRegisterStartupCallback(this)`,再调用 `TryGet()->UnregisterOwner(this)`。
- 命令注册用 `TCommands` 模式:定义 `FMyCommands : public TCommands<FMyCommands>`,构造函数传 `TCommands(MyEditor, INVTEXT(My Editor), NAME_None, FAppStyle::GetAppStyleSetName())`;声明 `TSharedPtr<FUICommandInfo> OpenPanel`;重写 `RegisterCommands()` 并调用 `UI_COMMAND(OpenPanel, My Panel, Opens the panel, EUserInterfaceActionType::Button, FInputChord())`;在 `StartupModule` 调用 `FMyCommands::Register()` 并通过 `CommandList->MapAction(...)` 绑定;在 `ShutdownModule` 调用 `FMyCommands::Unregister()`。
- 数据验证:继承 `UEditorValidatorBase`,它会被 Editor > Tools > Validate Assets 自动发现;实现 `CanValidateAsset_Implementation(const FAssetData& InAssetData, UObject* InObject, FDataValidationContext& InContext) const` 返回 `InObject && InObject->IsA<UMyDataAsset>()`,并实现 `ValidateLoadedAsset_Implementation(const FAssetData& InAssetData, UObject* InAsset, FDataValidationContext& Context)` 返回 `EDataValidationResult`。
- 数据验证也可通过命令行运行:`UnrealEditor-Cmd -run=DataValidation`。
- 常见错误修复:编辑器头文件出现在 Runtime 模块时,移到 Editor 模块;运行时侧编辑器钩子用 `#if WITH_EDITOR`。
- 常见错误修复:`ShutdownModule` 缺少 `UnregisterCustomClassLayout` 时,始终成对注册/注销;否则 Live Coding reload 会崩溃。
- 常见错误修复:Slate lambda 中捕获原始指针时,改为捕获 `TWeakPtr` 并在使用前 pin。
- 常见错误修复:编辑器扩展模块使用 `LoadingPhase: Default` 时,对注册菜单或自定义的模块改用 `PostEngineInit`。
- 常见错误修复:Runtime 引用 `UEditorUtilityWidget` 时,记住它仅编辑器存在,打包构建中不存在。
- 常见错误修复:每次值变化都调用 `ForceRefreshDetails()` 时,只在布局结构变化时使用;值变化用 handles/attributes。
- 常见错误修复:`RegisterMode` 未配 `UnregisterMode` 时,始终成对,避免插件重载崩溃。
- 相关技能:Slate 基础看 `ue-ui-umg-slate`;编辑器模块 `.Build.cs`、LoadingPhase、`WITH_EDITOR` 看 `ue-module-build-system`;需要资产编辑器/类型操作的自定义 `UDataAsset` 看 `ue-data-assets-tables`;C++ 基础和反射看 `ue-cpp-foundations`。
适用边界 · 注意事项
- 如果需要的是 Slate/UMG 控件基础(SNew、TAttribute、FReply、SBox、SHorizontalBox 等),不要用本经验,改看 `ue-ui-umg-slate`。
- 如果需要的是编辑器模块 `.Build.cs`、LoadingPhase、`WITH_EDITOR` 保护等模块构建配置细节,改看 `ue-module-build-system`。
- 编辑器专有 API 不适用于运行时/打包构建;不要在没有 Editor 模块和 `#if WITH_EDITOR` 保护时在 Runtime 中使用。