虚幻引擎编辑器开发基础(二)

虚幻引擎编辑器开发基础(二)

一、前言

在上篇虚幻引擎编辑器开发基础(一)中,笔者介绍了虚幻引擎编辑器开发的一些基础内容,如:插件与模块、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 做图。

FMultiBoxBuilder
FBaseMenuBuilder
FToolBarBuilder
FMenuBarBuilder
FMenuBuilder

通过上述的方法即可就行扩展菜单栏或工具栏的菜单按钮。

当然以上仅是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

具体的代码细节还需读者自行进行查看。

三、自定义资源

自定义资源包含两个部分:

  1. 资源类(NewAsset);
  2. 资源的编辑器模块(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 {}

那么接下来,我们就来通过自定义编辑器来解决这个问题。

一个编辑器界面通常可能包含了以下内容:

  1. 菜单栏;
  2. 工具栏;
  3. 细节面板;
  4. 预览窗口;
  5. 图表页签;
  6. …等;
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

参考文章

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值