【转载】UE4 编辑器扩展

866 阅读7分钟

原文链接:UE4 编辑器扩展

在 UE 的编辑器中,我们可以通过一些简单的操作在 Content Browser 中创建并编辑一些对象(即资源).

  • Content Browser 空白处单击右键创建资源.
  • Content Browser 选中一个或多个资源,右键从中创建新的资源.
  • 自定义特定资源资源编辑器(双击资源时打开的窗口)

参照 Paper2D 插件, 假设我们需要实现一个播放 Texture 序列的功能, 即将一系列的序列帧图片导入 UE4 中, 以指定的频率播放它. 这里我们关注的是如何表示这些序列帧图片资源, 通常一段序列有上百张, 一种直观的想法是用一个数组按顺序存下这些图片, 但不会有人想一张张地在蓝图中加到这个数组中!

因此, 我们需要更快捷的方式!

1. 创建资产类 —— UObject

我们可以将这种资源的表示抽象为一个 UTexturePlayer 类, 显然它派生自 UObject,这样就可以在编辑器中对它进行管理.

/**
 * A object to play texutre sequence
 */
UCLASS(BlueprintType)
class  YOURMODULENAME_API UTexturePlayer : public UObject
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	TArray<class UTexture2D*> SourceTextures;
    
    // other play function ... ...
};

2. 创建资产对应的工厂 —— UFactory

UE 的 Editor 中, 创建资源通常都是通过 UFactory, 对每一个希望在编辑器的 Content Browser 中创建的资源, 都必须实现一个对应的UFactory.

/**
 * Implements a factory for TexturePlayer objects.
 */
UCLASS(hidecategories=Object)
class UTexturePlayerFactoryNew
	: public UFactory
{
	GENERATED_UCLASS_BODY()

public:
        // Selected Textures
	TArray<class UTexture2D*> InitTextures;

	//~ UFactory Interface
        // 实现这个之后, 在 UE4.24 及之前,就会在 Create Advanced Asset 中的 Miscellaneous 分类中出现创建按钮,点击即会调用这个函数.
	virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;

	/** Returns true if this factory should be shown in the New Asset menu (by default calls CanCreateNew). */
	virtual bool ShouldShowInNewMenu() const override;

	virtual FText GetDisplayName() const override;

};
// 关键部分
UObject* UTexturePlayerFactoryNew::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
	UTexturePlayer* NewTexturePlayer = NewObject<UTexturePlayer>(InParent, InClass, InName, Flags | RF_Transactional);

	NewTexturePlayer->SourceTextures = InitTextures;

	return NewTexturePlayer;
}

由于 UFactory 是派生自 UObject 的,所以我们不必手动注册它, 编辑器会自动搜集所有的 Factory

3. 定义资产在编辑器中的外观 —— IAssetTypeActions

IAssetTypeActions 主要定义和这种资产本身相关的东西, 比如缩略图、颜色、分类 以及 在 Content Browser 中右键单击它的 Action 菜单. 同时,引擎实现好了一个符合大多数资产行为的基类FAssetTypeActions_Base, 我们可以从它派生, 以减少代码量.

class FTexturePlayerAssetActions
	: public FAssetTypeActions_Base
{
public:

	/**
	 * Creates and initializes a new instance.
	 *
	 * @param InType 该资产的自定义分类, 在外面注册再传进来
	 */
	FTexturePlayerAssetActions(EAssetTypeCategories::Type InType) : MyAssetType(InType);

public:

	//~ FAssetTypeActions_Base overrides
	virtual bool CanFilter() override { return true; }

        // 资产分类, 此处使用自定义类型, 或已有类型: EAssetTypeCategories::Misc
	virtual uint32 GetCategories() override {return MyAssetType;} 
        // 资产显示在 Content Browser 的名字
	virtual FText GetName() const override {return NSLOCTEXT("Test", "AssetTypeActions_TexturePlayerAsset", "Texure Player"); }
        // 与这个资产关联的类, 即我们定义的 UObject
	virtual UClass* GetSupportedClass() const override{ return UTexturePlayer::StaticClass(); }
	// 图标的背景色
        virtual FColor GetTypeColor() const override{ return FColor::Purple; }
	// 是否有右键菜单 Actions
        virtual bool HasActions(const TArray<UObject*>& InObjects) const override{return true; }

        virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
	virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;

private:
	EAssetTypeCategories::Type MyAssetType;
};

GetActions() 会在右键单击一个 Contetn Browser 中的这种资源时调用, 由此来扩展右键菜单,执行和这个资源相关的操作. 假设在有了一个 TexturesPlayer 后, 我们需要从中创建一个 UMG, 使其在一张图片上播放这组序列. 我们可以在右键菜单中加一个按钮创建这个 UMG :

void FTexturePlayerAssetActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
	FAssetTypeActions_Base::GetActions(InObjects, MenuBuilder);

	auto TextureAssets = GetTypedObjectPtrs<UTexturePlayer>(InObjects);
	
	MenuBuilder.AddMenuEntry(
		LOCTEXT("PlaySequenceFrame", "Create Texture UMG Player"),
		LOCTEXT("PlaySequenceFrameToolTip", "Create a UMG derived from FlipImageBook"),
		FSlateIcon(FIdeamakeStyle::GetStyleSetName(), "Ideamake.NewUMGState"),
		FUIAction(
			FExecuteAction::CreateStatic(CreateFlipImgaebook_Impl::CreateImageBook,TextureAssets),
			FCanExecuteAction::CreateLambda([=] { // 选中的 Object 至少有一个不是 nullptr 才需要执行创建操作.
				for (auto Texture : TextureAssets)
				{
					if (Texture != nullptr)
					{
						return true;
					}
				}
				return false;
			})
		)
	);
}

OpenAssetEditor() 会在需要打开这个 Object 的编辑器的时候调用, 比如双击 Content Browser 中的资源图标. 我们可以简单地打开一个属性 Details 面板编辑器, 这个是引擎已经写好的.

void FTexturePlayerAssetActions::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor)
{
	FSimpleAssetEditor::CreateEditor(EToolkitMode::Standalone, EditWithinLevelEditor, InObjects);
}

此外, 也可以用自定义的编辑器,这个稍后再说. 与 Factory 不同的是, FTexturePlayerAssetActions及其父类是纯 C++ (从其前缀 F 也可以看出), 引擎是无法自动搜集它的, 所以需要手动注册.

4. 注册 AssetAction

通常来讲,我们需要将一种资产的 UObject 与其相关的 Editor 部分分开, 因为在打包好的程序中是不会有 Editor 部分的. 由此我们需要定义两个模块, 一个用来定义 UTexturePlayer 极其核心功能部分(控制播放逻辑), 另一个用来定义编辑器中的辅助工具, FactoryIAssetTypeActions, 自定义编辑器和 Details 面板等, 假设我们定义 UTexturePlayer 的模块名为 TexturesPlayer, 其对应的编辑器工具模块通常叫 TexturePlayerEditor, 且定义在同一个插件中.

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

	virtual void ShutdownModule() override;
private:
	void RegisterAssetTools();
	void UnRegisterAssetTools();
private: 
	TArray<TSharedRef<IAssetTypeActions>> RegisteredAssetTypeActions;
};

和普通的模块定义差不多, 只是要记得将注册过的东西保留一份引用,以便在模块 ShutdownUnRegister. 和编辑器相关的许多工具都需要在此处注册.

AssetAction 需要注册到 AssetTools 模块中, 调用其 RegisterAssetTypeActions 注册即可. 注意到上面定义我们的 AssetTypeActions 时,需要在构造时传入一个自定义资源类型, 这个类型也需要注册到 AssetTools 中, 用 RegisterAdvancedAssetCategory() , 最终注册我们的 AssetTypeAction 的代码如下:

void FTexturePlayerEditor::RegisterAssetTools()
{
	IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
	
	EAssetTypeCategories::Type MyAssetType = AssetTools.RegisterAdvancedAssetCategory(FName("IdeamakeCustomCategory"), NSLOCTEXT("Ideamake", "Ideamake_Inc", "Ideamake"));
		
	// Create Asset actions
	TSharedRef<IAssetTypeActions> Action = MakeShareable(new FTexturePlayerAssetActions(MyAssetType));

	// Register Asset action
	AssetTools.RegisterAssetTypeActions(Action);

	RegisteredAssetTypeActions.Add(Action);
}

ModuleStartupModule() 函数中调用它:

void FCharlesFrameWorkEditorModule::StartupModule()
{
	RegisterAssetTools();
	FIdeamakeStyle::Initialize();
}

其中 FIdeamakeStyle::Initialize() 注册了自定义图标. 至此我们可以在 ContentBrowser 空白处右键单击, 就可以看到我们自定义的分类以及我们的 TexturePlayer 创建的按钮.

双击创建出来的资源图标就可以进入到一个简单的资产编辑器:

如果想打开自定义的编辑器, 只需在上面 AssetTypeActionOpenAssetEditor() 函数实现中打开自定义的编辑, 通常是直接实例化一个自己实现的派生自 FAssetEditorToolkit 的类, 来创建编辑这个 ObjectSlate 界面.实现方式待下回再说.

5. 自定义图标样式

在上面的注册中提到了对自定义图标的注册:

FIdeamakeStyle::Initialize();

其实现主要是定义一个 FSlateStyleSet 并用 FSlateStyleRegistry::RegisterSlateStyle() 注册, 这样就可以在别的地方直接使用, 比如用作某个 Action 的图标, 可以直接将以下 FSlateIcon 传进去:

FSlateIcon(StyleSetName, "AssetActions.CreateSprite")

StyleSetName 是我们注册的 StyleSet 的名字, 用来标识我们的 StyleSet, 后面的字符串AssetActions.CreateSprite 表示的是这个 StyleSet 中的特定的资源。

class FIdeamakeStyle
{
public:
	static void Initialize();
	static void Shutdown();
	static TSharedPtr<class ISlateStyle> Get();
	static FName GetStyleSetName();
private:
	static FString InContent(const FString& RelativePath, const ANSICHAR* Extension);
private:
	static TSharedPtr<class FSlateStyleSet> StyleSet;
};

#include "IdeamakeStyle.h"

#include "Interfaces/IPluginManager.h"
#include "SlateStyleRegistry.h"
#include "SlateOptMacros.h"

TSharedPtr<FSlateStyleSet> FIdeamakeStyle::StyleSet = nullptr;
TSharedPtr<class ISlateStyle> FIdeamakeStyle::Get() { return StyleSet; }

FString FIdeamakeStyle::InContent(const FString& RelativePath, const ANSICHAR* Extension)
{
	static FString ContentDir = IPluginManager::Get().FindPlugin(TEXT("MyPluginName"))->GetContentDir();
	return (ContentDir / RelativePath) + Extension;
}

FName FIdeamakeStyle::GetStyleSetName()
{
	static FName IdeamakeStyleName(TEXT("IdeamakeStyle"));
	return IdeamakeStyleName;
}

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION

void FIdeamakeStyle::Initialize()
{
	const FVector2D Icon16x16(16.0f, 16.0f);
	const FVector2D Icon32x32(32.0f, 32.0f);
	const FVector2D Icon64x64(64.0f, 64.0f);

	if (StyleSet.IsValid())
	{
		return;
	}

	StyleSet = MakeShareable(new FSlateStyleSet(GetStyleSetName()));
	StyleSet->SetContentRoot(FPaths::EngineContentDir() / TEXT("Editor/Slate"));
	StyleSet->SetCoreContentRoot(FPaths::EngineContentDir() / TEXT("Slate"));

	FSlateImageBrush* ThisH = new FSlateImageBrush(InContent("Icon/TigerHead", ".png"), Icon64x64);
	FSlateImageBrush* ThisH1 = new FSlateImageBrush(InContent("Icon/File", ".png"), Icon64x64);
	FSlateImageBrush* ThisH2 = new FSlateImageBrush(InContent("Icon/Screen", ".png"), Icon64x64);

	StyleSet->Set("Ideamake.NewUMGState", ThisH);
	StyleSet->Set("ClassThumbnail.SpritePlayer", ThisH1);
	StyleSet->Set("ClassThumbnail.FlipImageBook", ThisH2);

	FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

void FIdeamakeStyle::Shutdown()
{
	if (StyleSet.IsValid())
	{
		FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
		ensure(StyleSet.IsUnique());
		StyleSet.Reset();
	}
}

在使用时:

FSlateIcon(FIdeamakeStyle::GetStyleSetName(), "Ideamake.NewUMGState")

其中以 ClassThumbnail 为前缀的图标, 会自动匹配后面的类名(去掉各种 U,A 前缀)成为这个类显示在 Content Browser 中的图标.

6. Content Browser 扩展

如果我们希望在点击其它资源时(Textures)创建我们的 TexturesPlayer , 但又无法直接扩展 Texture 的AssetTypeAction(改源码?), 这时可以直接扩展 Content Browser 模块的选中资源时的右键菜单, 参考之前的一篇文章: UE4-编辑器 Content Browser 右键菜单扩展

Summary:

  • 声明资产类型的 C++ 类, 通常继承自 UObject.
  • 实现用户创建资产实例的方法, 即 Asset Factories.
  • 自定义资产在编辑器中的外观, 快捷菜单、缩略图颜色和图标、过滤、分类等.
  • 特定资产的 Content Browser actions. 即右键菜单.
  • TODO: 对复杂 Asset 类型, 自定义 Asset 编辑器 UI.

image.png

reference:

Toolkits:

  • Engine\Plugins\2D\Paper2D\Source\Paper2DEditor\Private\SpriteEditor\SpriteEditor.h
  • Engine\Source\Editor\Kismet\Public\BlueprintEditor.h

自定义: FSlateStyleSet

  • Engine\Plugins\2D\Paper2D\Source\Paper2DEditor\Private\PaperStyle.cpp

uplugin 配置文件中, 对 Runtime 模块的 LoadingPhase 最好是 PreDefault , 否则会打包失败。