在UE的编辑器中,我们可以通过一些简单的操作在ContentBrowser中创建并编辑一些对象(即资源).
参照Paper2D
插件, 假设我们需要实现一个播放Texture
序列的功能, 即将一系列的序列帧图片导入UE4中, 以指定的频率播放它. 这里我们关注的是如何表示这些序列帧图片资源, 通常一段序列有上百张, 一种直观的想法是用一个数组按顺序存下这些图片, 但不会有人想一张张地在蓝图中加到这个数组中!
因此, 我们需要更快捷的方式!
我们可以将这种资源的表示抽象为一个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 ... ...
};
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
。
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
也可以看出), 引擎是无法自动搜集它的, 所以需要手动注册.
通常来讲,我们需要将一种资产的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;
};
和普通的模块定义差不多, 只是要记得将注册过的东西保留一份引用,以便在模块Shutdown
时UnRegister
.和编辑器相关的许多工具都需要在此处注册.
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);
}
在Module
的StartupModule()
函数中调用它:
void FCharlesFrameWorkEditorModule::StartupModule()
{
RegisterAssetTools();
FIdeamakeStyle::Initialize();
}
其中FIdeamakeStyle::Initialize()
注册了自定义图标. 至此我们可以在ContentBrowser
空白处右键单击,就可以看到我们自定义的分类以及我们的TexturePlayer
创建的按钮.
双击创建出来的资源图标就可以进入到一个简单的资产编辑器:
如果想打开自定义的编辑器, 只需在上面AssetTypeAction
的OpenAssetEditor()
函数实现中打开自定义的编辑, 通常是直接实例化一个自己实现的派生自FAssetEditorToolkit
的类, 来创建编辑这个Object
的Slate
界面.实现方式待下回再说.
在上面的注册中提到了对自定义图标的注册:
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中的图标.
如果我们希望在点击其它资源时(Textures)创建我们的TexturesPlayer, 但又无法直接扩展Texture的AssetTypeAction(改源码?),这时可以直接扩展ContentBrowser模块的选中资源时的右键菜单, 参考之前的一篇文章:https://zhuanlan.zhihu.com/p/164695117
Summary:
reference:
Toolkits:
自定义:FSlateStyleSet
在uplugin配置文件中, 对Runtime模块的LoadingPhase最好是PreDefault, 否则会打包失败。