虚幻引擎编辑器开发基础(二)
一、前言
在上篇虚幻引擎编辑器开发基础(一)中,笔者介绍了虚幻引擎编辑器开发的一些基础内容,如:插件与模块、Slate、资源等。
这篇文章将整理一下对编辑器进行扩展的相关知识。
如:编辑器窗口扩展、自定义资源、自定义编辑器模式等。
由于笔者只自己实践过其中的一部分。一些内容可能会留空,标注出可能的参考文章,后续工作涉及到或做到将补充和修改。
如有错误,还望见谅。
下图展示了虚幻引擎中编辑器开发的主要内容。
名称 | 描述 |
---|---|
Developer Tools | 常用的开发工具,包括蓝图调试器、碰撞分析器、调试器等辅助开发工具 |
TooBar/Menu Extensions | 主编辑器窗口或单个Asset的编辑窗口的工具栏和菜单栏的扩展 |
Detail Customization | 细节面板的扩展 |
Graph Nodes and Pins | 蓝图节点和针脚的扩展 |
Custom Asset And Editor | 自定义Asset类型和编辑窗口 |
Custom Edit Mode | 自定义组件在编辑器窗口中的输入行为和可视化 |
二、编辑器窗口扩展
笔者将 TooBar/Menu Extensions 、Detail Customization 归纳到编辑器窗口的扩展。
划分为以下三个部分,分别是:
- 菜单栏(Menu)和工具栏(TooBar)扩展;
- 属性面板扩展;
- 视窗ViewPort扩展;
下面将逐一进行介绍说明。
2.1 菜单栏和工具栏扩展
这部分主要摘录自 【UE4】编辑器开发(一)关卡编辑器拓展
首先,先区别几个概念:MenuBar、Menu、Toolbar。
MenuBar
横向的可交互的菜单栏。
Menu
纵向菜单。
Toolbar
工具栏。
UI拓展点
通过开启Editor Preferences-General-Miscellaneous-Display UI Extension Points即可以在UI面板看到用绿色字符标识出的可拓展点。
打开显示扩展点后,工具栏变成了下图样子,绿色的字符标出了扩展点名称。
下文将基于StandaloneWindow插件进行介绍。
2.1.1 FExtender
扩展引擎编辑器的系统菜单栏,主要添加的控件有三种:
- FMenuBarBuilder
- FMenuBuilder
- FToolBarBuilder
这三者分别对应了上面提到的几个概念。
FExtend提供了AddMenuExtension来扩展Menu,AddMenuBarExtension扩展MenuBar, AddToolBarExtension扩展ToolBar。
如下所示,MultiBoxExtender.h
文件:
class FExtender
{
public:
/**
* Extends a menu bar at the specified extension point
*
* @param ExtensionHook Part of the menu to extend. You can extend the same point multiple times, and extensions will be applied in the order they were registered.
* @param HookPosition Where to apply hooks in relation to the extension hook
* @param CommandList The UI command list responsible for handling actions for the menu items you'll be extending the menu with
* @param MenuExtensionDelegate Called to populate the part of the menu you're extending
*
* @return Pointer to the new extension object. You can use this later to remove the extension.
*/
SLATE_API TSharedRef< const FExtensionBase > AddMenuBarExtension( FName ExtensionHook, EExtensionHook::Position HookPosition, const TSharedPtr< FUICommandList >& CommandList, const FMenuBarExtensionDelegate& MenuBarExtensionDelegate );
/**
* Extends a menu at the specified extension point
*
* @param ExtensionHook Part of the menu to extend. You can extend the same point multiple times, and extensions will be applied in the order they were registered.
* @param HookPosition Where to apply hooks in relation to the extension hook
* @param CommandList The UI command list responsible for handling actions for the menu items you'll be extending the menu with
* @param MenuExtensionDelegate Called to populate the part of the menu you're extending
*
* @return Pointer to the new extension object. You can use this later to remove the extension.
*/
SLATE_API TSharedRef< const FExtensionBase > AddMenuExtension( FName ExtensionHook, EExtensionHook::Position HookPosition, const TSharedPtr< FUICommandList >& CommandList, const FMenuExtensionDelegate& MenuExtensionDelegate );
/**
* Extends a tool bar at the specified extension point
*
* @param ExtensionHook Part of the menu to extend. You can extend the same point multiple times, and extensions will be applied in the order they were registered.
* @param HookPosition Where to apply hooks in relation to the extension hook
* @param CommandList The UI command list responsible for handling actions for the toolbar items you'll be extending the menu with
* @param ToolbarExtensionDelegate Called to populate the part of the toolbar you're extending
*
* @return Pointer to the new extension object. You can use this later to remove the extension.
*/
SLATE_API TSharedRef< const FExtensionBase > AddToolBarExtension( FName ExtensionHook, EExtensionHook::Position HookPosition, const TSharedPtr< FUICommandList >& CommandList, const FToolBarExtensionDelegate& ToolBarExtensionDelegate );
// ...省略部分源码
};
2.1.2 UToolMenu
主要用于扩展菜单(包括了工具栏的菜单,以及菜单栏中的纵向菜单)。
虚幻中相关的文件为:
ToolMenu.h/cpp
ToolMenus.h/cpp
ToolMenuSection.h/cpp
ToolMenuEntry.h/cpp
其中类之间的使用关系大概如下:
- 先找到Menu,再找到MenuSection,然后添加Entry(即一个菜单按钮)
2.1.3 菜单栏扩展
下面,我们将基于以上的知识来扩展菜单栏。
大概的内容如下图所示。
拓展新菜单栏
这里的新菜单栏只是是一个新的MenuBar。
通过FExtender实现
// 加载LevelEditor模块
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
// 创建菜单栏项
{
// 创建拓展
TSharedPtr<FExtender> MenuBarExtender = MakeShareable(new FExtender());
/**
@Param FName ExtensionHook. 项所在位置
@Param EExtensionHook::Position HookPosition. 更具体的表示在ExtensionHook的相对位置:Before、After、First
@Param const TSharedPtr< FUICommandList >& CommandList. 触发命令
@Param const FMenuBarExtensionDelegate& MenuBarExtensionDelegate. 单播委托。绑定一个方法,方法是关于返回拓展项的信息。
*/
MenuBarExtender->AddMenuBarExtension("Help", EExtensionHook::After, PluginCommands, FMenuBarExtensionDelegate::CreateRaw(this, &FStandaloneWindowModule::AddMenuBarExtension));
// 添加拓展项到关卡编辑器
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuBarExtender);
}
// 此方法是创建一个菜单栏拓展项的委托方法
void FStandaloneWindowModule::AddMenuBarExtension(FMenuBarBuilder& Builder)
{
// AddMenuEntry有多个重载方法。可以通过其设置显示文本、提示文本、图标等参数。
Builder.AddMenuEntry(FStandaloneWindowCommands::Get().OpenPluginWindow);
}
结果:
拓展已有菜单栏
通过FExtender实现
// 加载LevelEditor模块
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
{ // 创建菜单拓展项,就是菜单栏内的子项
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender());
// 增加菜单到 General UI扩展点之后
MenuExtender->AddMenuExtension("General", EExtensionHook::After, PluginCommands, FMenuExtensionDelegate::CreateRaw(this, &FStandaloneWindowModule::AddMenuExtension));
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
// 此方法用于
void FStandaloneWindowModule::AddMenuExtension(FMenuBuilder& Builder)
{
Builder.AddMenuEntry(FStandaloneWindowCommands::Get().OpenPluginWindow);
}
结果:
通过UToolMenus实现
// Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
FToolMenuOwnerScoped OwnerScoped(this);
{
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
{
// 在WindowLayout部分添加
FToolMenuSection& Section = Menu->FindOrAddSection("WindowLayout");
Section.AddMenuEntryWithCommandList(FStandaloneWindowCommands::Get().OpenPluginWindow, PluginCommands);
}
}
结果:
2.1.4 工具栏扩展
通过FExtender实现
// 加载LevelEditor模块
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
// 创建工具栏拓展项
{
TSharedPtr<FExtender> ToolBarExtender = MakeShareable(new FExtender());
ToolBarExtender->AddToolBarExtension("Settings", EExtensionHook::After, PluginCommands,FToolBarExtensionDelegate::CreateRaw(this, &FStandaloneWindowModule::AddToolBarExtension));
LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolBarExtender);
}
// AddToolbarExtension的回调
void FStandaloneWindowModule::AddToolbarExtension(FToolBarBuilder& Builder)
{
// 一个空的下拉菜单
auto ContextMenu = []()
{
FMenuBuilder MenuBuilder(true, nullptr);
return MenuBuilder.MakeWidget();
};
//添加一个工具栏按钮
Builder.AddToolBarButton(FStandaloneWindowCommands::Get().OpenPluginWindow);
//添加一个下拉菜单
Builder.AddComboButton(
FUIAction(),
FOnGetContent::CreateLambda(ContextMenu),
TAttribute<FText>(),
TAttribute<FText>());
}
通过UToolMenus实现
{
UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar");
{
FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("Settings");
{
FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FStandaloneWindowCommands::Get().OpenPluginWindow));
Entry.SetCommandList(PluginCommands);
}
}
}
结果:
2.1.5 小结
上面构建菜单栏、菜单和工具栏拓展项的显示参数分别使用FMenuBarBuilder、FMenuBuilder和FToolBarBuilder。他们的继承关系为:
下图采用了 mermaid 做图。
通过上述的方法即可就行扩展菜单栏或工具栏的菜单按钮。
当然以上仅是UI方面的工具。
更重要的还是FUICommandList中绑定的命令回调。
在StandaloneWindow插件中,其主要用的内容是:激活一个全局的Tab窗口。
void FStandaloneWindowModule::PluginButtonClicked()
{
FGlobalTabmanager::Get()->TryInvokeTab(StandaloneWindowTabName);
}
当然关于菜单按钮拓展项的扩展细节还有更多,比如:
-
创建下拉菜单;
-
创建分隔栏;
-
创建分割线;
-
创建子下拉菜单;
-
…等等
其它常用可拓展编辑器模块:
类型 | 菜单栏拓展 | 工具栏拓展 |
---|---|---|
AnimationBlueprintEditor | √ | √ |
AnimationEditor | √ | √ |
BehaviorTreeEditor | √ | √ |
Cascade | √ | √ |
CurveAssetEditor | √ | × |
CurveTableEditor | √ | × |
DataTableEditor | √ | × |
DestructibleMeshEditor | √ | √ |
EnvironmentQueryEditor | √ | √ |
FontEditor | √ | √ |
Kismet | √ | × |
LevelEditor | √ | √ |
MaterialEditor | √ | √ |
Matinee | √ | √ |
NiagaraEditor | √ | √ |
Persona | √ | × |
PhysicsAssetEditor | √ | √ |
SkeletalMeshEditor | √ | √ |
StaticMeshEditor | √ | √ |
StringTableEditor | √ | × |
TextureEditor | √ | √ |
TranslationEditor | √ | × |
UMGEditor | √ | √ |
加载模块时,注意有些模块的注册名和类名不一致,比如Kistmet和FBlueprintEditorModule等。
还要注意某些模块的加载顺序和时间。
2.2 属性细节面板扩展
属性细节面板(DetailsView)指的是展示UObject对象属性的面板。
UE提供了一套针对UObject的细节面板生成机制,能将一个UObject的各种属性(自然是帶了UPROPERTY宏标记),轻松地生成对应的细节面板。
实际上每种自定义属性的UI,在UE里都有相对应的实现,下面这张图可以明确看出对于每种UPROPERTY类型UE都实现了一个UI。
UE4所有的PROPERYTY宏能够发挥作用,其实都来自于一个叫IDetailsView的类。
具体原理来说也很简单,也就是解析(Parse)这个UObject中的所有UPROPERTY的类型,依次生成相应的Slate对象。
可以在PropertyEditorHelpers.cpp
,看到所有的UObject属性反射生成的SWidget类型。
#include "UserInterface/PropertyEditor/SPropertyEditor.h"
#include "UserInterface/PropertyEditor/SPropertyEditorNumeric.h"
#include "UserInterface/PropertyEditor/SPropertyEditorArray.h"
#include "UserInterface/PropertyEditor/SPropertyEditorCombo.h"
#include "UserInterface/PropertyEditor/SPropertyEditorEditInline.h"
#include "UserInterface/PropertyEditor/SPropertyEditorText.h"
#include "UserInterface/PropertyEditor/SPropertyEditorBool.h"
#include "UserInterface/PropertyEditor/SPropertyEditorArrayItem.h"
#include "UserInterface/PropertyEditor/SPropertyEditorTitle.h"
#include "UserInterface/PropertyEditor/SPropertyEditorDateTime.h"
#include "UserInterface/PropertyEditor/SResetToDefaultPropertyEditor.h"
#include "UserInterface/PropertyEditor/SPropertyEditorAsset.h"
#include "UserInterface/PropertyEditor/SPropertyEditorClass.h"
#include "UserInterface/PropertyEditor/SPropertyEditorStruct.h"
#include "UserInterface/PropertyEditor/SPropertyEditorSet.h"
#include "UserInterface/PropertyEditor/SPropertyEditorMap.h"
2.2.1 细节面板(DetailsView)的创建
用IDetailsView来创建细节面板
/**
* Interface class for all detail views
*/
class IDetailsView : public SCompoundWidget
{
// ... 省略部分源码
};
IDetailsView本身也是SCompoundWidget,所以可以用来创建Slate UI。
定义了一个类:
UCLASS()
class TESTDETAILVIEW_API UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Test")
float a;
UPROPERTY(EditAnywhere, Category = "Test")
UStaticMesh* Mesh;
UPROPERTY(EditAnywhere, Category = "Test")
int c;
};
用于创建UI:
- 修改FStandaloneWindowModule::OnSpawnPluginTab函数:
TSharedRef<SDockTab> FStandaloneWindowModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
TSharedPtr<IDetailsView> DetailsView;
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs(false, false, false, FDetailsViewArgs::HideNameArea);
DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
UMyObject* MyClass = NewObject<UMyObject>();
DetailsView->SetObject(MyClass);
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
// Put your tab content here!
DetailsView->AsShared()
];
}
结果如下:
2.2.2 细节面板的扩展定制
通过IDetailsView是可以对UObject的各种属性进行直接生成UI。但有时,我们想做一些自定义的功能,进一步定制DetailsView,比如在XXX类别下加个SButton之类。
想要进一步定制DetailsView,得继承IDetailCustomization,并在CustomizeDetails中增加控件(SWidget)。
IDetailCustomization
类如下:
- 可以看出需要对CustomizeDetails进行重写,从而实现自定义。
/**
* Interface for any class that lays out details for a specific class
*/
class IDetailCustomization : public TSharedFromThis<IDetailCustomization>
{
public:
// ...
/** Called when details should be customized */
// 当需要自定义细节时被调用
virtual void CustomizeDetails( IDetailLayoutBuilder& DetailBuilder ) = 0;
// ... 省略部分源码
};
创建任意UI
对UMyObject类,实现一个自定义的IDetailCustomization,代码如下:
.h文件:
class FMyObjectDetailCustom : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder);
};
.cpp文件:
TSharedRef<IDetailCustomization> FMyObjectDetailCustom::MakeInstance()
{
return MakeShareable(new FMyObjectDetailCustom);
}
void FMyObjectDetailCustom::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& aaa = DetailBuilder.EditCategory(FName("aaa"));
aaa.AddCustomRow(FText::GetEmpty())
.NameContent()
[
SNew(STextBlock)
.Text(FText::FromString("sss"))
]
.ValueContent()
[
SNew(SButton)
.Text(FText::FromString("abc"))
];
}
为了让CustomizeDetails起效果,还得在开始模块进行CustomizeDetails的注册
- 注意:类名前不含”U“。
// 注册自定义CustomizeDetails
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.RegisterCustomClassLayout(FName("MyObject"), FOnGetDetailCustomizationInstance::CreateStatic(&FMyObjectDetailCustom::MakeInstance));
再模块销毁的时候进行注销:
// 注销
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.UnregisterCustomClassLayout(FName("MyObject"));
结果如下:
IDetailCategoryBuilder
代表了 details view 中的一个类别(category)
/**
* Represents a category for the a details view
*/
class IDetailCategoryBuilder
- 例如上述的示例中,添加了一个
aaa
的类别。而原UObject中存在一个Test
的类别。因而最终的结果是两个。
其中,AddCustomRow函数可以用于添加一个自定义的行widget,包含了NameContent和ValueContent。
/**
* Adds a custom widget row to the category
*
* @param FilterString A string which is used to filter this custom row when a user types into the details panel search box
* @param bForAdvanced Whether the widget should appear in the advanced section
*/
virtual FDetailWidgetRow& AddCustomRow(const FText& FilterString, bool bForAdvanced = false) = 0;
隐藏成员变量UI
1、隐藏整个类别:
/**
* Hides an entire category
*
* @param CategoryName The name of the category to hide
*/
virtual void HideCategory( FName CategoryName ) = 0;
隐藏“Test“类别:
// 隐藏类别
DetailBuilder.HideCategory(FName("Test"));
2、隐藏某个属性的UI:
/**
* Hides a property from view
*
* @param PropertyHandle The handle of the property to hide from view
*/
virtual void HideProperty( const TSharedPtr<IPropertyHandle> PropertyHandle ) = 0;
下面以隐藏属性c为例:
TSharedRef<IPropertyHandle> cHandle = DetailBuilder.GetProperty("c");
DetailBuilder.HideProperty(cHandle);
示例结果:
自定义成员变量UI
通过AddCustomRow函数,让我们可以添加一些UI。但是如果我们要重新定义一些属性的UI形式呢?
和隐藏成员变量UI一样,首先需要通过GetProperty
函数获得成员变量的属性句柄(IPropertyHandle)。
再通过AddCustomRow进行创建UI。
// 重写属性a的UI
// 属性a的句柄
TSharedRef<IPropertyHandle> aHandle = DetailBuilder.GetProperty("a");
{
// 自定义一个类别
IDetailCategoryBuilder& CustCategory = DetailBuilder.EditCategory(FName("Custom Category"));
CustCategory.AddCustomRow(FText::FromName(TEXT("1")))
.NameContent()
[
SNew(STextBlock).Text(aHandle->GetPropertyDisplayName())
]
.ValueContent()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(SCheckBox)
]
+ SHorizontalBox::Slot()
[
SNew(SEditableTextBox)
]
];
}
结果:
- 出现了两个A属性的UI(一个为UE自动生成,一个为我们自定义的)。
- 若覆盖的话,应该在创建UI之前,将对应的先隐藏。
2.3 视窗ViewPort
此处,对ViewPort进行一个简单的介绍,扩展放在自定义资源当中。
如下图场景编辑器的ViewPort所示,其包含了两个部分:
- 窗口上的工具栏UI;
- 预览的场景;
ViewPort相关的核心类如下:
- FPreviewScene 、FEditorViewportClient、SViewportToolBar、SEditorViewport
FPreviewScene
- 封装用于预览或缩略图呈现的场景, 主要负责预览场景的渲染
/**
* Encapsulates a simple scene setup for preview or thumbnail rendering.
*/
class ENGINE_API FPreviewScene : public FGCObject
其提供了一堆借口用于添加或移除组件或调整光照:
/**
* Adds a component to the preview scene. This attaches the component to the scene, and takes ownership of it.
*/
virtual void AddComponent(class UActorComponent* Component,const FTransform& LocalToWorld, bool bAttachToRoot=false);
/**
* Removes a component from the preview scene. This detaches the component from the scene, and returns ownership of it.
*/
virtual void RemoveComponent(class UActorComponent* Component);
// ... 等等
FEditorViewportClient
- 视图窗口客户端,对摄像机移动,渲染调试,鼠标点击等一些操作的高级封装;
/** Viewport client for editor viewports. Contains common functionality for camera movement, rendering debug information, etc. */
class UNREALED_API FEditorViewportClient : public FCommonViewportClient, public FViewElementDrawer, public FGCObject
{
// ... 省略部分源码
// 持有预览场景的实例
/** The scene used for the viewport. Owned externally */
FPreviewScene* PreviewScene;
};
处理键鼠输入的函数:
- ProcessClick
- InputKey
- InputWidgetDelta
绘制相关的函数:
- Draw
- DrawCanvas
SViewportToolBar
- 放置在视口中的视口工具栏小部件
/**
* A level viewport toolbar widget that is placed in a viewport
*/
class UNREALED_API SViewportToolBar : public SCompoundWidget
SEditorViewport
- 编辑器视口类,它将创建 FEditorViewportClient 和 SViewportToolBar。
class UNREALED_API SEditorViewport : public SCompoundWidget
{
//... 省略部分源码
// 创建ViewPort客户端,虚函数由子类覆写
virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() = 0;
// 创建ViewPort工具栏,虚函数由子类覆写
// Implement this to add a viewport toolbar to the inside top of the viewport
virtual TSharedPtr<SWidget> MakeViewportToolbar() { return TSharedPtr<SWidget>(nullptr); }
// 客户端的实例
/** The client responsible for setting up the scene */
TSharedPtr<FEditorViewportClient> Client;
};
以LevelEditor(场景编辑器为)例,有以下的类:
-
SLevelViewport:继承自SEditorViewport
-
SLevelViewportToolBar:继承自SSViewportToolBar
-
FLevelEditorViewportClient:继承自FEditorViewportClient
具体的代码细节还需读者自行进行查看。
三、自定义资源
自定义资源包含两个部分:
- 资源类(NewAsset);
- 资源的编辑器模块(AssetEditor);
资源类即新建资源类型的UObject类,用于存储数据。
资源的编辑器模块,负责整个自定义资源可视化相关编辑修改等。
3.1 自定义资源的创建
自定义资源的创建涉及到以下几个类:
- UFactory;
- FAssetTypeActions_Base;
首先,让我们自定义资源类(继承自UObject),这里仍使用UMyObject。
注意需要标记为BlueprintType,且通过XXX_API将其导出。
UCLASS(BlueprintType)
class STANDALONEWINDOW_API UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Test")
float a;
UPROPERTY(EditAnywhere, Category = "Test")
UStaticMesh* Mesh;
UPROPERTY(EditAnywhere, Category = "Test")
int c;
};
其次,定义工厂类(UFactory)。
- 在其构造函数中,支持的类设置为UMyObject类型。
- 重载FactoryCreateNew函数,创建UMyObject的实例。
// 工厂类的定义
UCLASS()
class STANDALONEWINDOW_API UMyObjectFactory : public UFactory
{
GENERATED_UCLASS_BODY()
// UFactory interface
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
// End of UFactory interface
};
// 构造函数
UMyObjectFactory::UMyObjectFactory(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// 当bCreateNew或者ShouldShowInNewMenu方法返回true时,该资源可以通过资源菜单创建
bCreateNew = true;
// 源码注解是创建资源后,打开关联编辑器,但没有发现有什么实际作用,后期发现作用后再补充
bEditAfterNew = true;
// 该资源对应或支持的对象类!!!
SupportedClass = UMyObject::StaticClass();
//这里注意一下,就是UE4自己实现的一些工厂类中,SupportedClass的类是资源蓝图类,而ParentClass(的子类)才是资源实例类。一个是资源蓝图类,一个是真正游戏中实例化的类。注意区分。
}
UObject* UMyObjectFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
check(Class->IsChildOf(UMyObject::StaticClass()));
return NewObject<UMyObject>(InParent, Class, Name, Flags | RF_Transactional);
}
然后,定义AssetTypeAction类,并且将其注册到AssetTools模块。
定义如下:
- 类别在Gameplay下,名称为MyObject。
- 支持的类型也是为UMyObject。
// 需要注册AssetTypeAction到AssetToolsModule
class FAssetTypeActions_MyObject : public FAssetTypeActions_Base
{
public:
// IAssetTypeActions Implementation
virtual FText GetName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_MyObject", "MyObject"); }
virtual FColor GetTypeColor() const override { return FColor(139, 119, 101); }
virtual UClass* GetSupportedClass() const override { return UMyObject::StaticClass(); }
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override {}
virtual uint32 GetCategories() override { return EAssetTypeCategories::Gameplay; }
};
注册:
- 将以下代码放在StartupModule()函数中,模块进行加载的时候会注册。
// 将AssetTypeAction注册到AssetTools模块
IAssetTools& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
TSharedPtr<FAssetTypeActions_MyObject> AssetTypeAction = MakeShareable(new FAssetTypeActions_MyObject());
AssetToolsModule.RegisterAssetTypeActions(AssetTypeAction.ToSharedRef());
最后,进行编译运行,到Content Browser右键,可以得到下图:
小结以下:
- UFactory:创建资源的工厂类;
- FAssetTypeActions_Base:资源的相关操作类,引擎通过该类创建资源,需要注册到AssetToolsModule中。
3.2 自定义资源的编辑器
通过3.1中的操作,我们可以创建一个新的资源类型文件了,但是当我们兴冲冲地点击它,却没办法打开编辑器进行编辑!
这是因为:OpenAssetEditor函数的复写为空。
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override {}
那么接下来,我们就来通过自定义编辑器来解决这个问题。
一个编辑器界面通常可能包含了以下内容:
- 菜单栏;
- 工具栏;
- 细节面板;
- 预览窗口;
- 图表页签;
- …等;
3.2.1 创建编辑器类
资源的编辑器,需要一个继承自FWorkflowCentricApplication的编辑器类作为入口。
代码如下:
MyObjecetEditor.h
class STANDALONEWINDOW_API FMyObjectEditor : public FWorkflowCentricApplication, public FNotifyHook, public FEditorUndoClient
{
public:
//初始化编辑器函数
void InitMyObjectEditor(const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, UMyObject* InMyObject);
public:
//~ Begin FWorkflowCentricApplication Interface
virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;
//~ End FWorkflowCentricApplication Interface
//~ Begin IToolkit Interface
virtual FName GetToolkitFName() const override;
virtual FText GetBaseToolkitName() const override;
virtual FString GetWorldCentricTabPrefix() const override;
virtual FLinearColor GetWorldCentricTabColorScale() const override;
//~ End IToolkit Interface
virtual void RegisterToolbarTab(const TSharedRef<class FTabManager>& InTabManager);
virtual void InvokeMyObjectGraphTab(); //打开图表Tab,预留
UMyObject* GetStateMachine() { return MyObject.Get(); }
TSharedPtr<FDocumentTracker> GetDocumentManager() { return DocumentManager; }
private:
TWeakObjectPtr<UMyObject> MyObject;
TSharedPtr<FDocumentTracker> DocumentManager;
};
MyObjecetEditor.cpp
void FMyObjectEditor::InitMyObjectEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UMyObject* InMyObject)
{
MyObject = InMyObject;
// Initialize the asset editor and spawn nothing (dummy layout)
const TSharedRef<FTabManager::FLayout> DummyLayout = FTabManager::NewLayout("NullLayout")->AddArea(FTabManager::NewPrimaryArea());
InitAssetEditor(Mode, InitToolkitHost, FName("MyObjectEditorApp"), DummyLayout, true, true, InMyObject);
//Init DocumentManager
DocumentManager = MakeShareable(new FDocumentTracker);
DocumentManager->Initialize(SharedThis(this));
DocumentManager->SetTabManager(TabManager.ToSharedRef());
}
void FMyObjectEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
FWorkflowCentricApplication::RegisterTabSpawners(InTabManager);
}
FName FMyObjectEditor::GetToolkitFName() const
{
return FName("MyObjectEditor");
}
FText FMyObjectEditor::GetBaseToolkitName() const
{
return FText::FromString(TEXT("MyObject Editor"));
}
FString FMyObjectEditor::GetWorldCentricTabPrefix() const
{
return FString();
}
FLinearColor FMyObjectEditor::GetWorldCentricTabColorScale() const
{
return FLinearColor();
}
void FMyObjectEditor::RegisterToolbarTab(const TSharedRef<class FTabManager>& InTabManager)
{
//调用父类的这个函数就会创建默认的ToolbarTab
FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
}
void FMyObjectEditor::InvokeMyObjectGraphTab()
{
//预留
}
有了这个类,接下来需要在FAssetTypeActions_MyObject::OpenAssetEditor函数中进行使用,打开一个编辑器实例。
代码如下:
void FAssetTypeActions_MyObject::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor /*= TSharedPtr<IToolkitHost>()*/)
{
EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone;
for (UObject* Object : InObjects)
{
if (UMyObject* MyObject = Cast<UMyObject>(Object))
{
TSharedRef<FMyObjectEditor> NewEditor(new FMyObjectEditor());
NewEditor->InitMyObjectEditor(Mode, EditWithinLevelEditor, MyObject);
}
}
}
通过上述的代码就实现了最简陋的一个编辑器窗口。
- 没有工具栏,菜单栏也只有系统自带的。
3.2.2 创建编辑模式
编辑器类(FWorkflowCentricApplication的继承类)可以使用和切换编辑模式。
编辑模式,继承自FApplicationMode,主要是负责创建布局,并注册相应的页签类。
菜单栏和工具栏可以使用默认,在编辑器初始化时设置。
FMyObjectEditorApplicationMode.h
class STANDALONEWINDOW_API FMyObjectEditorApplicationMode : public FApplicationMode
{
public:
FMyObjectEditorApplicationMode(TSharedPtr<class FMyObjectEditor> InStateMachineEditor);
virtual void RegisterTabFactories(TSharedPtr<FTabManager> InTabManager) override;
virtual void AddTabFactory(FCreateWorkflowTabFactory FactoryCreator) override;
virtual void RemoveTabFactory(FName TabFactoryID) override;
virtual void PreDeactivateMode() override {};
virtual void PostActivateMode() override;
protected:
TWeakPtr<FMyObjectEditor> MyObjectEditor;
FWorkflowAllowedTabSet StardardTabFactories;
};
FMyObjectEditorApplicationMode.cpp
FMyObjectEditorApplicationMode::FMyObjectEditorApplicationMode(TSharedPtr<class FMyObjectEditor> InMyObjectEditor)
: FApplicationMode(FName("StandardMode"))
{
MyObjectEditor = InMyObjectEditor;
//创建一个默认的布局
TabLayout = FTabManager::NewLayout("StardardStateMachineEditorLayout")
->AddArea
(
FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)
->Split
(
// 工具栏
FTabManager::NewStack()
->SetSizeCoefficient(0.186721f)
->SetHideTabWell(true)
->AddTab(InMyObjectEditor->GetToolbarTabId(), ETabState::OpenedTab)
)
);
}
void FMyObjectEditorApplicationMode::RegisterTabFactories(TSharedPtr<FTabManager> InTabManager)
{
TSharedPtr<FMyObjectEditor> Editor = MyObjectEditor.Pin();
// Tool bar tab 注册工具栏
Editor->RegisterToolbarTab(InTabManager.ToSharedRef());
// Other tabs
Editor->PushTabFactories(StardardTabFactories);
FApplicationMode::RegisterTabFactories(InTabManager);
}
void FMyObjectEditorApplicationMode::AddTabFactory(FCreateWorkflowTabFactory FactoryCreator)
{
if (FactoryCreator.IsBound())
{
StardardTabFactories.RegisterFactory(FactoryCreator.Execute(MyObjectEditor.Pin()));
}
}
void FMyObjectEditorApplicationMode::RemoveTabFactory(FName TabFactoryID)
{
StardardTabFactories.UnregisterFactory(TabFactoryID);
}
void FMyObjectEditorApplicationMode::PostActivateMode()
{
MyObjectEditor.Pin()->InvokeMyObjectGraphTab();
}
可以看到在Mode的构造函数中,创建布局。
这一步,可以得到的结果为:
- 相比上一步增加了一工具栏。
3.2.3 创建细节面板
创建细节面板页签,需要一个工厂类。
页签(Tab)工厂类,继承自FWorkflowTabFactory。
细节面板的Slate可使用PropertyEditorModule.CreateDetailView()接口,通过SetObject()设置要显示细节的对象。
该工厂类需要在编辑模式类中注册到Editor中。
MyObjectDetailsTabFactory.h
// 生成类的细节面板
struct FMyObjectDetailsTabFactory : public FWorkflowTabFactory
{
public:
FMyObjectDetailsTabFactory(TSharedPtr<class FMyObjectEditor> InMyObjectEditor);
virtual TSharedRef<SWidget> CreateTabBody(const FWorkflowTabSpawnInfo& Info) const override;
virtual FText GetTabToolTipText(const FWorkflowTabSpawnInfo& Info) const override;
protected:
TWeakPtr<FMyObjectEditor> MyObjectEditor;
};
MyObjectDetailsTabFactory.cpp
FMyObjectDetailsTabFactory::FMyObjectDetailsTabFactory(TSharedPtr<class FMyObjectEditor> InMyObjectEditor)
: FWorkflowTabFactory(FName("Details"), InMyObjectEditor)
, MyObjectEditor(InMyObjectEditor)
{
TabLabel = LOCTEXT("DetailsLabel", "Details");
TabIcon = FSlateIcon(FEditorStyle::GetStyleSetName(), "Kismet.Tabs.Components");
bIsSingleton = true;
ViewMenuDescription = LOCTEXT("DetailsView", "Details");
ViewMenuTooltip = LOCTEXT("DetailsView_ToolTip", "Show the details view");
}
TSharedRef<SWidget> FMyObjectDetailsTabFactory::CreateTabBody(const FWorkflowTabSpawnInfo& Info) const
{
// 创建细节面板
FMyObjectEditor* Editor = FMyObjectEditor.Pin().Get();
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs(false, false, true, FDetailsViewArgs::HideNameArea, false);
// 设置回调
DetailsViewArgs.NotifyHook = Editor;
DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide;
TSharedRef<IDetailsView> DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
DetailsView->SetObject(Editor->GetMyObject());
return
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.HAlign(HAlign_Fill)
[
DetailsView
];
}
FText FMyObjectDetailsTabFactory::GetTabToolTipText(const FWorkflowTabSpawnInfo& Info) const
{
return LOCTEXT("DetailsTabTooltip", "The details tab allows editing of the properties");
}
该工厂类需要在编辑模式类中注册到Editor中:
在编辑模式的构造函数中,添加:
![21-MyObjectDetailTab-UnRegister](images/21-MyObjectDetailTab-UnRegister.png)FMyObjectEditorApplicationMode::FMyObjectEditorApplicationMode(TSharedPtr<class FMyObjectEditor> InMyObjectEditor)
: FApplicationMode(FName("StandardMode"))
{
MyObjectEditor = InMyObjectEditor;
// 注册细节面板工厂类
StardardTabFactories.RegisterFactory(MakeShared<struct FMyObjectDetailsTabFactory>(InMyObjectEditor));
//创建一个默认的布局,
TabLayout = FTabManager::NewLayout("StardardStateMachineEditorLayout")
->AddArea
(
FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)
->Split
(
// 工具栏
FTabManager::NewStack()
->SetSizeCoefficient(0.186721f)
->SetHideTabWell(true)
->AddTab(InMyObjectEditor->GetToolbarTabId(), ETabState::OpenedTab)
)
->Split
(
// 细节面板
FTabManager::NewStack()->SetSizeCoefficient(0.2f)
->SetHideTabWell(true)
->AddTab("Details", ETabState::OpenedTab)
)
);
}
下面列出了几种情况效果图:
1)未注册Tab:
2)注册Tab,但是未自定义DetailView:
3)注册Tab,并自定义DetailView:
- 可以看出
属性细节面板扩展
同样对这里的细节面板有作用!
3.2.4 创建预览窗口
除了以上的功能,编辑器的目标就是对于修改后的内容可以直接进行展示,并达到所见即所得的目标。
因此,编辑器大部分都会有一定预览窗口,来展示效果。
这个部分要结合2.3中介绍的基础知识来进行展示,等有空了,进行下整理。
四、自定义编辑器模式
五、Commandlet
除了对编辑器进行一些改造,有时候我们还需要调用引擎进行一些自动或半自动的工作。
这里我将其归纳为编辑器开发。
实现方式就是基于虚幻的Commandlet。
那么什么是Commandlet呢?
其实,Commandlet就是指通过控制台直接调用一些命令(这些命令是自己定制的、也可以是引擎现有的),可以帮助我们实现自动化或者半自动化流程。
例如虚幻中提供了以下一些:
- 烘焙的UCookCommandlet;
- 自动导入资源的UImportAssetsCommandlet;
- 将Asset转成文本格式的UTextAssetCommandlet;
可以参考官方文档:UCommandlet。
调用格式:
[UE4Editor-Cmd.exe] [XXX.uproject] -run=[Commandlet名字] -这条Commandlet的参数
示例:
D:\SoftWare\UE_4.25\Engine\Binaries\Win64\UE4Editor-Cmd.exe "D:\WorkSpace\Unreal Project\ACT\ACT.uproject" -run=ImportAssets -importSettings="D:/Export/Importsetting.json"
自定义Commandlet
UCommandlet的定义中,有一个Main函数,作为命令的入口。
/**
* Entry point for your commandlet
*
* @param Params the string containing the parameters for the commandlet
*/
virtual int32 Main(const FString& Params) { return 0; }
具体的实现虚幻引擎中自带的,或者参考尝试UE4的Commandlet。