mcpField
U
ue-editor-tools

ue-editor-tools

github.com/quodsoler开发4191.3k 次安装4 周前更新
代码仓库

关于 ue-editor-tools

面向 Unreal Engine 5.8 的编辑器扩展技能指南,适用于开发自定义工具和工作流的开发者。涵盖详情面板、编辑器工具 Widget、菜单工具栏、子系统、资产操作和数据验证。

主要功能

  1. 01编辑器模块配置
  2. 02详情面板自定义
  3. 03编辑器工具 Widget 与脚本操作
  4. 04菜单和工具栏扩展
  5. 05编辑器子系统与资产工具

适用场景

  1. 01添加自定义属性编辑界面
  2. 02制作编辑器内工具面板
  3. 03扩展 Content Browser 右键操作
  4. 04创建菜单命令或编辑器子系统

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/quodsoler/unreal-engine-skills --skill ue-editor-tools

安装后在 Claude Code 中输入/ue-editor-tools确认已加载。

附带文件references/detail-customization-patterns.mdreferences/editor-module-setup.md

详细介绍

UE 编辑器工具

目标引擎:UE 5.8。以下 API 已根据 5.8 头文件验证;较旧的形式列在“已弃用 — 请勿使用”下。

编辑器扩展位于 "Type": "Editor" 的模块中,并依赖 UnrealEd、Slate、SlateCore、PropertyEditor(详情面板)、ToolMenus(菜单和工具栏)、EditorSubsystem(UEditorSubsystem)、Blutility(编辑器实用工具控件和脚本化操作)、AssetTools 和 AssetDefinition(内容浏览器资源类型)。这些模块都不能从 Runtime 模块中无条件引用(引擎 Runtime 模块仅在 if (Target.bBuildEditor) 和 #if WITH_EDITOR 下添加它们,例如 AIModule.Build.cs:31-34)。

上下文

如果 .agents/ue-project-context.md 存在,请阅读它(模块名称、约定、已启用的插件、GAS/网络设置)。如果文件不存在,也请继续。

根据请求和代码库确定所属领域。只有在两种合理解读会产生不同代码时才提问。

请求涉及……前往
创建编辑器模块、Build.cs、注册生命周期编辑器模块设置
为类或结构体自定义属性面板详情自定义
在编辑器中运行的 UMG 面板、右键资源/Actor 操作编辑器实用工具控件和脚本化操作
主菜单、工具栏、上下文菜单项、命令菜单和工具栏
编辑器生命周期单例、GEditor->GetEditorSubsystem<T>()编辑器子系统
撤销/重做、标记对象为脏、响应编辑器事件事务和编辑器委托
带有自定义交互或工具的视口模式编辑器模式
内容浏览器类型颜色、类别、工厂、缩略图资源类型、工厂和缩略图
带标签页的资源编辑器窗口资源编辑器工具包
在保存时或 CI 中验证资源数据验证

编辑器模块设置

.uproject(或 .uplugin)模块条目——详情自定义和 UToolMenus::RegisterStartupCallback 菜单使用 Default 即可(CommonUIEditor 在 Default 阶段注册其详情自定义);当 StartupModule 本身需要访问 GEditor 或其他已初始化的引擎状态时,使用 PostEngineInit:

{ "Name": "MyGameEditor", "Type": "Editor", "LoadingPhase": "PostEngineInit" }
// MyGameEditor.Build.cs
PrivateDependencyModuleNames.AddRange(new string[] {
    "Core", "CoreUObject", "Engine", "UnrealEd",
    "Slate", "SlateCore", "InputCore",
    "PropertyEditor",   // IDetailCustomization, IPropertyTypeCustomization
    "EditorSubsystem",  // UEditorSubsystem
    "Blutility",        // UEditorUtilityWidget, UAssetActionUtility
    "UMG", "UMGEditor",  // EditorUtilityWidget(Blueprint).h include UMG headers; Blutility links them privately
    "ToolMenus",        // UToolMenus
    "AssetTools",       // IAssetTools
    "AssetDefinition",  // UAssetDefinition
    "MyGame"            // your runtime module
});

声明模块类,定义两个生命周期函数,然后将 IMPLEMENT_MODULE 放在最后:

// MyGameEditor.h
#pragma once
#include "Modules/ModuleManager.h"

class FMyGameEditorModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;

private:
    void RegisterMenus();
    static void OpenMyPanel();
};

// MyGameEditorModule.cpp — includes MyDataAsset.h, MyRange.h, the two customization
// headers, PropertyEditorModule.h and ToolMenus.h
void FMyGameEditorModule::StartupModule()
{
    FPropertyEditorModule& PropertyModule =
        FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

    PropertyModule.RegisterCustomClassLayout(
        UMyDataAsset::StaticClass()->GetFName(),
        FOnGetDetailCustomizationInstance::CreateStatic(&FMyDataAssetCustomization::MakeInstance),
        FRegisterCustomClassLayoutParams());

    PropertyModule.RegisterCustomPropertyTypeLayout(
        FMyRange::StaticStruct()->GetFName(),
        FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FMyRangeCustomization::MakeInstance));

    PropertyModule.NotifyCustomizationModuleChanged();

    UToolMenus::RegisterStartupCallback(
        FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FMyGameEditorModule::RegisterMenus));
}

void FMyGameEditorModule::ShutdownModule()
{
    UToolMenus::UnRegisterStartupCallback(this);
    UToolMenus::UnregisterOwner(this);

    if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
    {
        FPropertyEditorModule& PropertyModule =
            FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
        PropertyModule.UnregisterCustomClassLayout(UMyDataAsset::StaticClass()->GetFName());
        PropertyModule.UnregisterCustomPropertyTypeLayout(FMyRange::StaticStruct()->GetFName());
        PropertyModule.NotifyCustomizationModuleChanged();
    }
}

IMPLEMENT_MODULE(FMyGameEditorModule, MyGameEditor)

RegisterMenus 和 OpenMyPanel 的定义见菜单和工具栏。FRegisterCustomClassLayoutParams 包含 TOptional<int32> OptionalOrder;当两个模块自定义同一个类且顺序很重要时设置该值,否则传入默认构造的结构体。

目录布局、WITH_EDITOR 保护、工厂、缩略图、工具包和完整的模式样板代码:editor-module-setup.md。

详细信息自定义

IDetailCustomization 会替换 UClass 的整个详细信息面板布局。IPropertyTypeCustomization 会替换 USTRUCT 或实例化对象属性所在的行。两者都在 StartupModule 中注册,如上所示。

// MyDataAssetCustomization.h
#pragma once
#include "IDetailCustomization.h"
#include "Templates/SharedPointer.h"

class FMyDataAssetCustomization : public IDetailCustomization
{
public:
    static TSharedRef<IDetailCustomization> MakeInstance();

    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
    virtual void CustomizeDetails(const TSharedPtr<IDetailLayoutBuilder>& DetailBuilder) override;

private:
    TWeakPtr<IDetailLayoutBuilder> WeakBuilder;
};

// MyDataAssetCustomization.cpp
void FMyDataAssetCustomization::CustomizeDetails(const TSharedPtr<IDetailLayoutBuilder>& DetailBuilder)
{
    WeakBuilder = DetailBuilder;   // store it before calling ForceRefreshDetails() later
    CustomizeDetails(*DetailBuilder);
}

单参数重载负责布局工作:EditCategory、GetProperty、AddProperty、AddCustomRow、HideProperty、GetObjectsBeingCustomized——完整函数体见下方参考资料。

USTRUCT 对应的实现 FMyRangeCustomization : public IPropertyTypeCustomization,会重载 CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) 和 CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)。

关键接口(均位于 Engine/Source/Editor/PropertyEditor/Public):

类型头文件调用方法
IDetailLayoutBuilderDetailLayoutBuilder.hEditCategory、HideCategory、GetProperty、HideProperty、AddPropertyToCategory、GetObjectsBeingCustomized、GetSelectedObjects、ForceRefreshDetails、RegisterInstancedCustomPropertyTypeLayout
IDetailCategoryBuilderDetailCategoryBuilder.hAddProperty、AddCustomRow、AddCustomBuilder、AddGroup、AddExternalObjects、SetSortOrder、InitiallyCollapsed(IDetailChildrenBuilder.h 中的 IDetailChildrenBuilder 提供相同的五种添加方法,以及 CustomizeChildren 中的 AddExternalStructure)
FDetailWidgetRowDetailWidgetRow.hNameContent()、ValueContent()、WholeRowContent()、ExtensionContent()、FilterString、Visibility、IsEnabled
IPropertyHandlePropertyHandle.hGetValue/SetValue 重载、GetNumChildren、GetChildHandle、SetOnPropertyValueChanged、SetOnChildPropertyValueChanged、CreatePropertyNameWidget、CreatePropertyValueWidget、EnumerateRawData、NotifyPreChange、NotifyPostChange
IDetailPropertyRowIDetailPropertyRow.hDisplayName、ToolTip、EditCondition、IsEnabled、Visibility、GetDefaultWidgets、CustomWidget
PropertyCustomizationHelpersPropertyCustomizationHelpers.hMakeAddButton、MakeRemoveButton、MakeClearButton、MakeBrowseButton、MakeUseSelectedButton、MakePropertyComboBox、SObjectPropertyEntryBox、SClassPropertyEntryBox

GetValue/SetValue 返回 FPropertyAccess::Result(Success、Fail、MultipleValues)。NotifyPostChange 接受 EPropertyChangeType::Type,例如 EPropertyChangeType::ValueSet。ECategoryPriority::Type 按从上到下的顺序排序:Variable、Transform、Important、TypeSpecific、Default、Uncommon。

完整的行布局模式、CustomizeHeader/CustomizeChildren 函数体、SObjectPropertyEntryBox、逐对象值枚举和字体说明:detail-customization-patterns.md。

编辑器实用控件和脚本化操作

UEditorUtilityWidget(EditorUtilityWidget.h,模块 Blutility)是一个在编辑器中运行的 UUserWidget。在 C++ 中创建子类以公开辅助函数,然后在 Blueprint 子类中构建界面。

// MyEditorUtilityWidget.h
#pragma once
#include "EditorUtilityWidget.h"
#include "MyEditorUtilityWidget.generated.h"

UCLASS()
class MYGAMEEDITOR_API UMyEditorUtilityWidget : public UEditorUtilityWidget
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable, Category = "My Tools")
    void PrefixSelectedAssets(const FString& Prefix);
};

// MyEditorUtilityWidget.cpp
void UMyEditorUtilityWidget::PrefixSelectedAssets(const FString& Prefix)
{
    UEditorAssetSubsystem* AssetSubsystem = GEditor->GetEditorSubsystem<UEditorAssetSubsystem>();
    for (UObject* Asset : UEditorUtilityLibrary::GetSelectedAssets())
    {
        const FString Source = Asset->GetOutermost()->GetName();
        AssetSubsystem->RenameAsset(
            Source, FPaths::GetPath(Source) / (Prefix + TEXT("_") + Asset->GetName()));
    }
}

UEditorUtilityLibrary(EditorUtilityLibrary.h,Blutility)还提供 GetSelectedAssetData()、GetSelectedAssetsOfClass(UClass*)、GetSelectionSet()(关卡 Actor)和 SyncBrowserToFolders(const TArray<FString>&)。

通过 UEditorUtilitySubsystem(EditorUtilitySubsystem.h)从代码中打开控件 Blueprint:

UEditorUtilitySubsystem 调用效果
SpawnAndRegisterTab(UEditorUtilityWidgetBlueprint*)注册并打开选项卡;返回 UEditorUtilityWidget*
SpawnAndRegisterTabAndGetID(UEditorUtilityWidgetBlueprint*, FName& OutTabID) / SpawnAndRegisterTabWithId(UEditorUtilityWidgetBlueprint*, FName InTabID)同上,写回或提供选项卡 ID
RegisterTabAndGetID(UEditorUtilityWidgetBlueprint*, FName& OutTabID)注册选项卡生成器,但不打开选项卡
TryRun(UObject* Asset)运行派生自 UEditorUtilityObject 的资源(调用其 Run)

脚本化操作:UAssetActionUtility 子类中的任何 UFUNCTION(CallInEditor) 都会显示在 Content Browser 右键菜单中;UActorActionUtility 对关卡 Actor 执行相同操作。两者都派生自 UEditorUtilityObject。使用 SupportedClasses 数组(TArray<TSoftClassPtr<UObject>>,可在 Class Defaults 中编辑)进行筛选;使用 GetSupportedClasses() 读取。

// MyAssetActionUtility.h
#pragma once
#include "AssetActionUtility.h"
#include "MyAssetActionUtility.generated.h"

UCLASS()
class MYGAMEEDITOR_API UMyAssetActionUtility : public UAssetActionUtility
{
    GENERATED_BODY()

public:
    UFUNCTION(CallInEditor, Category = "My Tools")
    void SetTextureCompressionToUI();
};

// MyAssetActionUtility.cpp
void UMyAssetActionUtility::SetTextureCompressionToUI()
{
    for (UObject* Asset : UEditorUtilityLibrary::GetSelectedAssets())
    {
        if (UTexture2D* Texture = Cast<UTexture2D>(Asset))
        {
            Texture->CompressionSettings = TC_EditorIcon;
            Texture->MarkPackageDirty();
            Texture->PostEditChange();
        }
    }
}

编辑器通过资产注册表标签构建这些菜单,不会加载实用工具资源;这些标签由 FAssetActionUtilityPrototype(EditorUtilityAssetPrototype.h)封装。请将筛选条件保存在 CDO 的 SupportedClasses 中,而不是在运行时计算。

菜单和工具栏

UToolMenus(模块 ToolMenus)负责管理所有编辑器菜单和工具栏。请从启动回调中注册,以确保 Slate 和要扩展的菜单已就绪。

// MyGameEditorModule.cpp (continued) — ToolMenuSection.h, ToolMenuEntry.h,
// Styling/AppStyle.h, EditorUtilitySubsystem.h, EditorUtilityWidgetBlueprint.h
void FMyGameEditorModule::OpenMyPanel()
{
    if (UEditorUtilityWidgetBlueprint* WidgetBP = LoadObject<UEditorUtilityWidgetBlueprint>(
            nullptr, TEXT("/Game/EditorWidgets/BP_MyTool.BP_MyTool")))
    {
        GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>()->SpawnAndRegisterTab(WidgetBP);
    }
}

void FMyGameEditorModule::RegisterMenus()
{
    FToolMenuOwnerScoped OwnerScoped(this);
    UToolMenu* WindowMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
    FToolMenuSection& Section = WindowMenu->FindOrAddSection("MyGame");
    Section.Label = FText::FromString("My Game");
    Section.AddMenuEntry(
        "OpenMyPanel",
        FText::FromString("My Panel"),
        FText::FromString("Open the My Game tool panel"),
        FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"),
        FUIAction(FExecuteAction::CreateStatic(&FMyGameEditorModule::OpenMyPanel)));

    UToolMenu* PlayToolBar =
        UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar");
    PlayToolBar->FindOrAddSection("MyTools").AddEntry(
        FToolMenuEntry::InitToolBarButton(
            "MyToolButton",
            FUIAction(FExecuteAction::CreateStatic(&FMyGameEditorModule::OpenMyPanel)),
            FText::FromString("My Tool"),
            FText::FromString("Run the My Game tool"),
            FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Toolbar.Settings")));
}
调用用途
UToolMenus::Get() / UToolMenus::TryGet()单例;TryGet 不会强制加载模块
RegisterMenu(FName Name, FName Parent = NAME_None, EMultiBoxType Type = EMultiBoxType::Menu, bool bWarnIfAlreadyRegistered = true)声明由你拥有的菜单
ExtendMenu(FName)向其他人拥有的菜单添加内容;如果菜单尚未注册,则会排队等待扩展
FindMenu(FName)查找已注册的菜单;不存在时返回 nullptr
UToolMenu::FindOrAddSection(FName) / AddSection(FName, Label, FToolMenuInsert)获取或创建区段
FToolMenuSection::AddMenuEntry(FName, Label, ToolTip, FSlateIcon, FToolUIActionChoice, EUserInterfaceActionType, FName TutorialHighlight, TOptional<FText> InputBindingOverride)菜单项
FToolMenuSection::AddSubMenu / AddSeparator / AddEntry子菜单、分隔符、预构建条目
FToolMenuEntry::InitToolBarButton / InitComboButton / InitWidget / InitMenuEntry构建要传给 AddEntry 的条目
UToolMenus::RegisterStartupCallback(const FSimpleMulticastDelegate::FDelegate&)延迟注册,直到菜单就绪
UToolMenus::UnRegisterStartupCallback(this) + UToolMenus::UnregisterOwner(this)必须在 ShutdownModule 中调用

FToolMenuOwnerScoped 会将其作用域内创建的所有内容标记为归属该所有者,以便 UnregisterOwner 将其全部移除。常用菜单名称:LevelEditor.MainMenu.Window、LevelEditor.MainMenu.Tools、LevelEditor.LevelEditorToolBar.PlayToolBar、LevelEditor.LevelEditorToolBar.User、LevelEditor.ActorContextMenu、ContentBrowser.AssetContextMenu。图标使用 FSlateIcon(FAppStyle::GetAppStyleSetName(), "StyleName")。

键盘快捷键和可复用命令使用 TCommands 和 UI_COMMAND;参见 editor-module-setup.md。

编辑器子系统

所属模块加载后,会自动实例化一次 UEditorSubsystem(模块 EditorSubsystem,头文件 EditorSubsystem.h),无需调用注册函数。使用 GEditor->GetEditorSubsystem<T>() 获取它。

// MyEditorSubsystem.h
#pragma once
#include "EditorSubsystem.h"
#include "MyEditorSubsystem.generated.h"

UCLASS()
class MYGAMEEDITOR_API UMyEditorSubsystem : public UEditorSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

private:
    void OnMapOpened(const FString& Filename, bool bAsTemplate);
};

// MyEditorSubsystem.cpp
void UMyEditorSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    FEditorDelegates::OnMapOpened.AddUObject(this, &UMyEditorSubsystem::OnMapOpened);
}

void UMyEditorSubsystem::Deinitialize()
{
    FEditorDelegates::OnMapOpened.RemoveAll(this);
    Super::Deinitialize();
}

void UMyEditorSubsystem::OnMapOpened(const FString& Filename, bool bAsTemplate)
{
    UE_LOG(LogMyGame, Display, TEXT("Opened map %s"), *Filename);
}

UEditorSubsystem 派生自 UDynamicSubsystem;Initialize(FSubsystemCollectionBase&)、Deinitialize() 和 ShouldCreateSubsystem(UObject* Outer) const 来自 USubsystem,因此务必调用 Super::。编写自己的子系统之前,值得先了解以下引擎编辑器子系统:

子系统模块用途
UEditorActorSubsystemUnrealEdGetAllLevelActors、GetSelectedLevelActors、SetSelectedLevelActors、SpawnActorFromClass、DestroyActor
UEditorAssetSubsystemUnrealEdLoadAsset、RenameAsset、DuplicateAsset、DeleteAsset、SaveLoadedAsset、ListAssets
UAssetEditorSubsystemUnrealEdOpenEditorForAsset、FindEditorForAsset、CloseAllEditorsForAsset、GetEditorModeInfoOrderedByPriority
UEditorUtilitySubsystemBlutilitySpawnAndRegisterTab、RegisterTabAndGetID、TryRun
ULevelEditorSubsystemLevelEditor打开、保存和新建关卡的操作

规范的子系统类型表(游戏实例、世界、本地玩家、引擎、编辑器)位于 ue-cpp-foundations。

事务与编辑器委托

对 UObject 的每项编辑器修改都必须用事务包裹,并在修改前调用 Modify(),否则撤销时会恢复过期数据:

#include "ScopedTransaction.h"
#include "Subsystems/EditorActorSubsystem.h"

void UMyEditorSubsystem::OffsetSelection(const FVector& Delta)
{
    const FScopedTransaction Transaction(
        NSLOCTEXT("MyGameEditor", "OffsetSelection", "Offset Selection"));
    UEditorActorSubsystem* ActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
    for (AActor* Actor : ActorSubsystem->GetSelectedLevelActors())
    {
        Actor->Modify();
        Actor->SetActorLocation(Actor->GetActorLocation() + Delta);
    }
}

FScopedTransaction(ScopedTransaction.h,UnrealEd)还提供 Cancel() 和 IsOutstanding()。直接写入资产属性后,调用 MarkPackageDirty() 和 PostEditChange()。

委托签名
FEditorDelegates::PostUndoRedoFSimpleMulticastDelegate
FEditorDelegates::OnMapOpened(const FString& Filename, bool bAsTemplate)
FEditorDelegates::PreBeginPIE / PostPIEStarted / EndPIE(const bool bIsSimulating)
FEditorDelegates::OnAssetPostImport(UFactory*, UObject*)
FEditorDelegates::OnNewActorsPlaced(UObject*, const TArray<AActor*>&)
FEditorDelegates::OnPackageDeleted / OnAssetsPreDelete(UPackage*) / (const TArray<UObject*>&)
FCoreUObjectDelegates::OnObjectPropertyChanged / OnPreObjectPropertyChanged(UObject*, FPropertyChangedEvent&) / (UObject*, const FEditPropertyChain&)
FCoreUObjectDelegates::OnObjectModified(UObject*)

编辑器模式

UEdMode(Tools/UEdMode.h,UnrealEd)是模式基类。所有 UEdMode 子类都会在所有模块加载阶段完成后,由 UAssetEditorSubsystem::RegisterEditorModes 自动发现——在构造函数中填写 Info,无需调用注册表。

// MyEditorMode.h
#pragma once
#include "Tools/UEdMode.h"
#include "MyEditorMode.generated.h"

UCLASS()
class MYGAMEEDITOR_API UMyEditorMode : public UEdMode
{
    GENERATED_BODY()

public:
    static const FEditorModeID EM_MyEditorMode;
    UMyEditorMode();

    virtual void Enter() override;
    virtual void Exit() override;
    virtual void ModeTick(float DeltaTime) override;
    virtual void CreateToolkit() override;
    virtual bool UsesToolkits() const override;
};
  • 在构造函数中设置 Info = FEditorModeInfo(EM_MyEditorMode, DisplayName, FSlateIcon(...), /*InVisibility=*/true);Info 是 UEdMode 的受保护成员。Enter() 和 Exit() 必须调用 Super::。
  • 交互工具:在 Enter() 中调用 RegisterTool(TSharedPtr<FUICommandInfo> UICommand, FString ToolIdentifier, UInteractiveToolBuilder* Builder, EToolsContextScope ToolScope = EToolsContextScope::Default)。GetInteractiveToolsContext(EToolsContextScope) 返回 UEditorInteractiveToolsContext;模式自身的上下文对象是 UEdModeInteractiveToolsContext。
  • 视口输入(MouseMove、InputKey、HandleClick、InputDelta、Tick)通过实现 ILegacyEdModeViewportInterface(Tools/LegacyEdModeInterfaces.h)提供。Render 和 DrawHUD 通过 ILegacyEdModeWidgetInterface 提供;UBaseLegacyWidgetEdMode 实现了这两个接口,并将调用转发给 CreateWidgetHelper() 返回的 FLegacyEdModeWidgetHelper。
  • 使用 GLevelEditorModeTools().ActivateMode(UMyEditorMode::EM_MyEditorMode) 激活;使用 IsModeActive / GetActiveScriptableMode 查询;使用 DeactivateMode 退出。
  • FEdMode(EdMode.h)是旧版共享指针模式;它继承自 FLegacyEdModeWidgetHelper,并通过 FEditorModeRegistry::Get().RegisterMode<T>(...) / UnregisterMode(...) 注册。新模式请使用 UEdMode 编写。

带工具包的完整模式实现:editor-module-setup.md。

资产类型、工厂和缩略图

UAssetDefinition(模块 AssetDefinition,头文件 AssetDefinition.h)是 5.8 中与内容浏览器集成的入口点。继承 UAssetDefinitionDefault(AssetDefinitionDefault.h,UnrealEd),这样 OpenAssets 就会默认打开通用的 FSimpleAssetEditor(AssetDefinitionDefault.cpp:16-25)。系统会通过定义类的 CDO 自动发现定义,无需注册调用。

重写 FText GetAssetDisplayName() const、FLinearColor GetAssetColor() const、TSoftClassPtr<UObject> GetAssetClass() const 和 TConstArrayView<FAssetCategoryPath> GetAssetCategories() const;仅当默认工具包路由不符合需求时,才重写 EAssetCommandResult OpenAssets(const FAssetOpenArgs& OpenArgs) const。

EAssetCategoryPaths 提供 Basic、Animation、Audio、Blueprint、Cinematics、Data、Foliage、FX、Gameplay、AI 等分类;使用 FAssetCategoryPath(EAssetCategoryPaths::Gameplay, INVTEXT("My Game")) 嵌套分类。此类型的上下文菜单项通过 UToolMenus 对 ContentBrowser.AssetContextMenu 的扩展添加,而不是通过定义类添加。

FAssetTypeActions_Base(AssetTypeActions_Base.h,模块 AssetTools)以及 IAssetTools::RegisterAssetTypeActions / UnregisterAssetTypeActions 仍可编译,但 IAssetTypeActions 的成员正逐步弃用,转而采用定义系统。新类型请使用 UAssetDefinition。

  • 工厂:编辑器模块中的 UFactory 子类在构造函数中设置 SupportedClass、bCreateNew、bEditAfterNew,并重写 FactoryCreateNew(UClass*, UObject*, FName, EObjectFlags, UObject*, FFeedbackContext*)。系统会自动发现,无需注册。
  • 缩略图:继承 UThumbnailRenderer,重写 Draw(UObject*, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget*, FCanvas*, bool bAdditionalViewFamily),然后在 StartupModule 中调用 UThumbnailManager::Get().RegisterCustomRenderer(UMyDataAsset::StaticClass(), UMyThumbnailRenderer::StaticClass()),并在 ShutdownModule 中调用 UnregisterCustomRenderer(...)。
  • 查询:构建 FARFilter(AssetRegistry/ARFilter.h),设置 ClassPaths、PackagePaths、bRecursiveClasses、bRecursivePaths,然后将其传递给 IAssetRegistry::GetAssets(const FARFilter&, TArray<FAssetData>&, bool bSkipARFilteredAssets = true)。

定义、工厂和缩略图的完整实现:editor-module-setup.md。

资产编辑器工具包

FAssetEditorToolkit(Toolkits/AssetEditorToolkit.h)用于承载带标签页的编辑器窗口。GetToolkitFName、GetBaseToolkitName 和 GetWorldCentricTabPrefix 是纯虚函数;你的初始化函数需使用 FTabManager::NewLayout 构建布局,并调用 InitAssetEditor(Mode, InitToolkitHost, AppIdentifier, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectToEdit)。

使用 IAssetEditorInstance* Instance = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(Asset, /*bFocusIfOpen=*/false); 查找或聚焦已打开的编辑器,然后调用 Instance->FocusWindow(Asset) 或 Instance->CloseWindow(EAssetEditorCloseReason::AssetEditorHostClosed)。

数据验证

DataValidation 插件中包含 UEditorValidatorBase(默认启用;模块 DataValidation,LoadingPhase: PreDefault)。系统会通过验证器的 CDO 自动发现验证器;重写 CanValidateAsset_Implementation(const FAssetData& InAssetData, UObject* InObject, FDataValidationContext& InContext) const 和 ValidateLoadedAsset_Implementation(const FAssetData& InAssetData, UObject* InAsset, FDataValidationContext& Context)。使用 UnrealEditor-Cmd <Project>.uproject -run=DataValidation 无头运行。

已弃用 — 请勿使用

不要使用在 5.8 中使用来源
FEditorStyle::Get()、FEditorStyle::GetBrush/GetFontStyleFAppStyle::Get()、FAppStyle::GetBrush/GetFontStyleEditor/EditorStyle/Public/EditorStyleSet.h 中的 UE_DEPRECATED(5.1)
IPropertyHandle::CreatePropertyNameWidget(Name, ToolTip, bDisplayResetToDefault, ...)CreatePropertyNameWidget(NameOverride, ToolTipOverride)PropertyEditor/Public/PropertyHandle.h:641 中的 UE_DEPRECATED(5.0)
UAssetActionUtility::GetSupportedClass() / UActorActionUtility::GetSupportedClass()SupportedClasses 数组 + GetSupportedClasses()Blutility/Classes/AssetActionUtility.h:69、ActorActionUtility.h:24 中的 UE_DEPRECATED(5.2)
IAssetEditorInstance::CloseWindow()(无参数)CloseWindow(EAssetEditorCloseReason)UnrealEd/Public/Subsystems/AssetEditorSubsystem.h:62 中的 UE_DEPRECATED(5.3)
FAssetEditorToolkit::OnRequestClose()(无参数)OnRequestClose(EAssetEditorCloseReason)UnrealEd/Public/Toolkits/AssetEditorToolkit.h:435 中的 UE_DEPRECATED(5.3)
UEditorValidatorBase::CanValidateAsset_Implementation(UObject*)、ValidateLoadedAsset_Implementation(UObject*, TArray<FText>&)接受 (const FAssetData&, UObject*, FDataValidationContext&) 的重载DataValidation/Public/EditorValidatorBase.h:125,130 中的 UE_DEPRECATED("5.4")
IDetailLayoutBuilder::GetDetailsView()GetDetailsViewSharedPtr()PropertyEditor/Public/DetailLayoutBuilder.h:63,74 中的 UE_DEPRECATED(5.5)
IDetailTreeNode::GetNodeDetailsView()GetNodeDetailsViewSharedPtr()PropertyEditor/Public/IDetailTreeNode.h:128 中的 UE_DEPRECATED(5.5)
IAssetTypeActions::GetThumbnailOverlayUAssetDefinition + GetThumbnailActionOverlayDeveloper/AssetTools/Public/IAssetTypeActions.h:122 中的 UE_DEPRECATED(5.5)
FEditorModeRegistry::GetModeInfo 迭代辅助函数、OnRegisteredModesChangedUAssetEditorSubsystem::GetEditorModeInfoOrderedByPriority、FindEditorModeInfo、OnEditorModesChangedUnrealEd/Public/EditorModeRegistry.h:98,104,141 中的 UE_DEPRECATED(4.26)
UEdModeInteractiveToolsContext::UsesDragTools() / OnRegisterViewportInteractions()UsesViewportInteractions() / OnBuildViewportInteractions()(实验性)UnrealEd/Public/Tools/EdModeInteractiveToolsContext.h:427,441 中的 UE_DEPRECATED(5.8)
FEditorModeTools::GetOverrideCursorVisibility(bool&, bool&, bool)(按值传递的 bSoftwareCursorVisible)GetCursorVisibilityOverride(bool&, bool&, bool&)UnrealEd/Public/EditorModeManager.h:321 中的 UE_DEPRECATED(5.8)

常见错误

在 Runtime 模块中包含编辑器头文件: UnrealEd、PropertyEditor、Blutility 和 ToolMenus 仅供编辑器使用,不会链接到打包后的构建中,因此,无条件依赖它们的 Runtime 模块无法打包(UBT 会抛出 BuildException,UnrealEd.Build.cs:12-14)。将代码放在 MyGameEditor 中;仅对 PostEditChangeProperty 等运行时类钩子使用 #if WITH_EDITOR。

在类定义之前使用 IMPLEMENT_MODULE: 该宏必须放在 FMyGameEditorModule 声明和两个函数定义之后——将它放在 .cpp 文件末尾。

注册后未取消注册: 每个 RegisterCustomClassLayout、RegisterCustomPropertyTypeLayout、RegisterCustomRenderer 和 RegisterStartupCallback 都需要在 ShutdownModule 中有对应的取消注册操作,并使用 FModuleManager::Get().IsModuleLoaded(...) 进行保护。缺少对应操作会导致 Live Coding 重新加载时崩溃。

修改 UObject 时未使用事务: 写入前始终使用 FScopedTransaction 和 Object->Modify()。

对 UEdMode 调用 FEditorModeRegistry::RegisterMode: UEdMode 子类会根据其 CDO 自动注册;该注册表仅用于 FEdMode。

在 Slate lambda 中捕获原始 this: 捕获 TWeakPtr/TWeakObjectPtr,并在使用前调用 pin——细节面板小组件的生命周期会超过定制对象。

每次值变化都调用 ForceRefreshDetails(): 这会销毁并重建布局。仅在行结构变化时调用;使用 TAttribute 和 _Lambda 绑定来驱动值。

在 LoadingPhase: Default 时于 StartupModule 中访问 GEditor: Default 阶段加载的项目模块会早于编辑器引擎加载;请通过 UToolMenus::RegisterStartupCallback / FCoreDelegates::GetOnPostEngineInit() 延迟执行(OnPostEngineInit 成员已标记为 UE_DEPRECATED(5.8),CoreDelegates.h:240),或在 PostEngineInit 阶段加载。

相关技能

  • ue-module-build-system — .Build.cs 语法、模块类型、公共与私有依赖、插件描述符
  • ue-cpp-foundations — UCLASS/UPROPERTY/UFUNCTION说明符、规范的子系统类型表、UObject 生命周期
  • ue-ui-umg-slate — Slate 小组件基础知识(SNew、TAttribute、FReply、复合小组件),每个自定义行都会用到
  • ue-data-assets-tables — 此类编辑器和定义所封装的 UDataAsset、UPrimaryDataAsset 和 UDataTable 类型
  • ue-testing-debugging — UE_LOG 分类和详细程度、编辑器代码自动化测试、Insights 性能分析
  • ue-actor-component-architecture — 需要定制其细节面板的 Actor 和组件类
  • ue-project-context — 编写和使用 .agents/ue-project-context.md
  • ue-blueprint-cpp-interop — 将 C++ 暴露给 Blueprint:UFUNCTION/UPROPERTY 元键、潜伏操作和异步节点

评论

支持 Markdown