原文链接: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 极其核心功能部分(控制播放逻辑), 另一个用来定义编辑器中的辅助工具, 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<TSharedRef<IAssetTypeActions>> RegisteredAssetTypeActions;
};
和普通的模块定义差不多, 只是要记得将注册过的东西保留一份引用,以便在模块 Shutdown 时UnRegister. 和编辑器相关的许多工具都需要在此处注册.
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);
}
在 Module 的 StartupModule() 函数中调用它:
void FCharlesFrameWorkEditorModule::StartupModule()
{
RegisterAssetTools();
FIdeamakeStyle::Initialize();
}
其中 FIdeamakeStyle::Initialize() 注册了自定义图标. 至此我们可以在 ContentBrowser 空白处右键单击, 就可以看到我们自定义的分类以及我们的 TexturePlayer 创建的按钮.
双击创建出来的资源图标就可以进入到一个简单的资产编辑器:
如果想打开自定义的编辑器, 只需在上面 AssetTypeAction 的 OpenAssetEditor() 函数实现中打开自定义的编辑, 通常是直接实例化一个自己实现的派生自 FAssetEditorToolkit 的类, 来创建编辑这个 Object 的 Slate 界面.实现方式待下回再说.
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.
reference:
- 插件创建和使用最佳实践
- PPT: FMX 2017: Extending Unreal Engine 4 with Plug-ins (Master Class)
- Engine\Plugins\2D\Paper2D\Source\Paper2DEditor\Private\Paper2DEditorModule.cpp
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, 否则会打包失败。