编辑器扩展_UE4编辑器扩展

编辑器扩展_UE4编辑器扩展_第1张图片

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

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

参照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 SourceTextures;
    
    // other play function ... ...
};

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

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

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

public:
    // Selected Textures
	TArray 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(InParent, InClass, InName, Flags | RF_Transactional);

	NewTexturePlayer->SourceTextures = InitTextures;

	return NewTexturePlayer;
}

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

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

IAssetTypeActions主要定义和这种资产本身相关的东西, 比如缩略图, 颜色, 分类 以及 在ContentBrowser中右键单击它的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;} 
    // 资产显示在ContentBrowser的名字
	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& InObjects) const override{return true; }

    virtual void GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) override;
	virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor = TSharedPtr()) override;

private:
	EAssetTypeCategories::Type MyAssetType;
};

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

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

	auto TextureAssets = GetTypedObjectPtrs(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的编辑器的时候调用, 比如双击ContentBrowser中的资源图标.我们可以简单地打开一个属性Details面板编辑器, 这个是引擎已经写好的.

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

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

4. 注册AssetAction

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

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

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

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

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

void FTexturePlayerEditor::RegisterAssetTools()
{
	IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get();
	
	EAssetTypeCategories::Type MyAssetType = AssetTools.RegisterAdvancedAssetCategory(FName("IdeamakeCustomCategory"), NSLOCTEXT("Ideamake", "Ideamake_Inc", "Ideamake"));
		
	// Create Asset actions
	TSharedRef 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创建的按钮.

编辑器扩展_UE4编辑器扩展_第2张图片

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

编辑器扩展_UE4编辑器扩展_第3张图片

如果想打开自定义的编辑器, 只需在上面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 Get();
	static FName GetStyleSetName();
private:
	static FString InContent(const FString& RelativePath, const ANSICHAR* Extension);
private:
	static TSharedPtr StyleSet;
};

#include "IdeamakeStyle.h"

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

TSharedPtr FIdeamakeStyle::StyleSet = nullptr;
TSharedPtr 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前缀)成为这个类显示在ContentBrowser中的图标.

6. ContentBrowser扩展

如果我们希望在点击其它资源时(Textures)创建我们的TexturesPlayer, 但又无法直接扩展Texture的AssetTypeAction(改源码?),这时可以直接扩展ContentBrowser模块的选中资源时的右键菜单, 参考之前的一篇文章:https://zhuanlan.zhihu.com/p/164695117

Summary:

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

编辑器扩展_UE4编辑器扩展_第4张图片

reference:

  • https://learn.unrealengine.com/course/2504894?r=False&ts=637365057953224512
  • PPT: https://www.slideshare.net/GerkeMaxPreussner/fmx-2017-extending-unreal-engine-4-with-plugins-master-class
  • EnginePlugins2DPaper2DSourcePaper2DEditorPrivatePaper2DEditorModule.cpp

Toolkits:

  • EnginePlugins2DPaper2DSourcePaper2DEditorPrivateSpriteEditorSpriteEditor.h
  • EngineSourceEditorKismetPublicBlueprintEditor.h

自定义:FSlateStyleSet

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

你可能感兴趣的:(编辑器扩展)