UE4插件创建
分类:
- Blank
- BlueprintLibrary
- ContentOnly
- Editor Toolbar Button
- Editor Standalone Window
- Editor Mode
- Third Party Library
插件中的代码
生成Visual Studio或Xcode的项目文件时,含有 Source
文件夹(包含 .Build.cs
文件)的插件将被添加到项目文件,以便导航到其源代码。编译游戏项目时,UBT将自动编译此类插件。
插件可含有任意数量的模块源目录。多数插件仅有一个模块(但可创建多个模块,例如插件包含纯编辑器功能时),及游戏期间要运行的其他代码。
插件源文件的大部分布局与引擎中其他C++模块相同。
在模块的 Source
目录(或其子目录)内,插件可在标头文件中声明新反映的类型(UCLASS
、USTRUCT
等)。引擎的构建系统将检测此类文件,并按需要生成代码支持新类型。需遵守C++模块中使用 Uobjects
时的一般规则,例如在模块的源文件中包含生成的标头文件和模块 generated.inl
文件。
UE4支持共生模块和插件。通过在自身.uproject文件中启用插件,项目模块可依赖插件。类似地,通过在自身.uplugin文件中启用其他插件,插件可表明依赖性。但其中有一项重要限制:插件和模块将拆分为若干层级,仅能依赖同一级或更高级的其他插件或模块。例如,项目模块可依赖引擎模块,但引擎模块无法依赖项目模块。这是因为引擎(及其所有插件和模块)的级别高于项目,须能在无项目的情况下编译。以下图表展示了项目和模块间的依赖性层级:
箭头表明可能的依赖性。各插件或模块类型可依赖同级别或更高级别的其他插件或模块类型。
引擎插件
虚幻引擎4的 Engine
目录下包含部分内置插件。引擎插件和项目插件类似,但可用于所有项目。此类插件通常由引擎和工具程序员创建,目的在于提供可在多个项目中使用并能在单一位置维护的基线功能。利用此功能,用户可直接添加或覆盖引擎功能,而无需修改引擎代码。
项目中的插件
插件位于项目目录的 Plugins
子文件夹下,将在引擎或编辑器启动时被探测和加载。
如插件包含具有 Source
文件夹(和 .Build.cs
文件)的模块,插件代码将被自动添加到生成的C++项目文件,以便在开发项目时开发插件。编译项目时,有可用源的插件都被作为游戏依赖项进行编译。
项目生成器将忽略无 Source
文件夹的插件,其不会出现在C++项目文件中,但若存在二进制文件,启动时仍将加载此类插件。
目前,无法将插件配置文件与项目打包。未来版本中可能会支持此功能,但目前需手动将此类文件复制到项目的
Config
文件夹。
插件描述文件
插件描述文件是命名以 .uplugin
结尾的文件。文件名的第一部分固定为插件命名。插件描述文件固定位于插件目录中,启动时将被引擎发现。
插件描述文件使用Json(JavaScript对象表示法)文件格式。
描述文件示例
此范例描述文件来自引擎的 UObjectPlugin
。
{
"FileVersion" :3,
"Version" :1,
"VersionName" :"1.0",
"FriendlyName" :"UObject Example Plugin",
"Description" :"An example of a plugin which declares its own UObject type.This can be used as a starting point when creating your own plugin.",
"Category" :"Examples",
"CreatedBy" :"Epic Games, Inc.",
"CreatedByURL" :"http://epicgames.com",
"DocsURL" :"",
"MarketplaceURL" :"",
"SupportURL" :"",
"EnabledByDefault" : true,
"CanContainContent" : false,
"IsBetaVersion" : false,
"Installed" : false,
"Modules" :
[
{
"Name" :"UObjectPlugin",
"Type" :"Developer",
"LoadingPhase" :"Default"
}
]
}
描述文件格式
描述文件为JSON格式的变量列表,此类列表为 FPluginDescriptor
类型。其中具有一个附加字段"FileVersion",其是文件结构中唯一的必需字段。"FileVersion"提供插件描述文件的版本,通常应设为引擎支持的最高版本(当前为"3")。由于此版本应用于插件描述文件的格式,而非插件本身,因此其可能不会频繁变化,也不应随插件后续版本的发行而改变。要与引擎旧版本进行最大化兼容,可使用较旧版本号,但不建议进行此操作。
欲了解其他支持字段的相关详情,参见FPluginDescriptor API参考页面。
bEnabledByDefault
插件入口
添加按钮的方法
首先找到设置“显示UI扩展点”便于我们看到我们添加按钮的地方
给任意窗口的工具条、菜单条、菜单等添加按钮
FUICommandList
FUICommandInfo
UI_COMMAND
可以实现:扩展已有菜单栏、添加新菜单栏、扩展已有工具栏,无法实现添加新工具栏
UI_COMMAND基础认知
其实和UI_COMMAND相关模块有三个,分别是FUICommandList,FUICommandInfo以及UI_COMMAND宏,他们的关系是FUICommandList包含FUICommandInfo,并且映射FUICommandInfo到委托,UI_COMMAND宏负责正式注册FUICommandInfo。
UToolMenus——新版本的方法
先定位菜单类型通过UToolMenus::Get()->ExtendMenu获得一个UToolMenu对象指针,这个Menu可能是菜单条可能是工具条
获得到Menu后通过 Menu->FindOrAddSection定位段落Section,最后对段落添加Menu来拓展编辑器,添加Menu可以选择AddMenuEntryWithCommandList(工具条就是AddEntry),也可以添加子菜单SubMenu
工具栏
//----------4.24版本的做法-------------
//拓展到已有工具条 位置 Setting
{
UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar");
{
FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("Settings");
{
FToolMenuEntry& Entry = Section.
AddEntry(FToolMenuEntry::InitToolBarButton(FFirstEditorToolBarButtonPluginCommands::Get().PluginAction));
Entry.SetCommandList(PluginCommands);
//位置设置到段前
FToolMenuEntry& Entry1 = Section.AddEntry(
FToolMenuEntry::InitToolBarButton(FFirstEditorToolBarButtonPluginCommands::Get().PluginAction,
TAttribute<FText>(),
TAttribute<FText>(),
TAttribute<FSlateIcon>(),
NAME_None,
TOptional<FName>("YourEntryName")));
Entry1.SetCommandList(PluginCommands);
Entry1.InsertPosition.Position = EToolMenuInsertType::First;
}
}
}
// //尝试添加到新工具条 位置 File 失败
// {
// UToolMenu* ToolbarMenu = UToolMenus::Get()->RegisterMenu("LevelEditor.NewLevelToolBar");
// {
// FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("MyToolBarSection");
// {
// FToolMenuEntry& Entry = Section.
// AddEntry(FToolMenuEntry::InitToolBarButton(FFirstEditorToolBarButtonPluginCommands::Get().PluginAction));
// Entry.SetCommandList(PluginCommands);
//
// }
// }
// }
菜单栏
//拓展到已有的菜单条
{
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");//扩展菜单
{
FToolMenuSection& Section = Menu->FindOrAddSection("WindowLayout");//找到一个段
FToolMenuEntry& Entry = Section.AddMenuEntryWithCommandList(FFirstEditorToolBarButtonPluginCommands::Get().PluginAction, PluginCommands);//用命令列表天啊及菜单入口
Entry.InsertPosition.Position = EToolMenuInsertType::First;//设置插入方式
// Section.AddSubMenu("New Sub Menu", FText::FromString("???"), FText::FromString("???"),
// FNewToolMenuChoice(
// FNewMenuDelegate::CreateRaw(
// this, &FOfficial_EditorToolbarButtonModule::FillSubmenu)));
}
}
//添加一个新的菜单栏
{
UToolMenus* ToolMenus = UToolMenus::Get();
UToolMenu* MyMenu = ToolMenus->RegisterMenu("LevelEditor.MainMenu.MySubMenu");//注册一个新入口
FToolMenuSection& Section = MyMenu->AddSection("MyMenuSection");
Section.AddMenuEntryWithCommandList(FFirstEditorToolBarButtonPluginCommands::Get().PluginAction,PluginCommands);
}
FExtender——经典方法
分为几个步骤
1、加载模块
2、创建Extender对象
3、给Extender对象添加扩展属性(在哪里扩展,绑定什么委托)、
4、将Extender对象绑定到对应模块
void FMyEditorToolbarButtonModule::RegisterMenus()
{
{//蓝图编辑器ToolBar
FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>("Kismet");
BlueprintEditorModule.OnRegisterTabsForEditor().AddRaw(this, &FMyEditorToolbarButtonModule::OnBPToolBarRegister);
}
//Source\Engine\Source\Editor
IAnimationBlueprintEditorModule& AnimationBlueprintEditorModule = FModuleManager::LoadModuleChecked<IAnimationBlueprintEditorModule>("AnimationBlueprintEditor");
{//动画蓝图拓展已有菜单栏
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender);
MenuExtender->AddMenuExtension("HelpApplication", EExtensionHook::After, PluginCommands, FMenuExtensionDelegate::CreateRaw(this, &FMyEditorToolbarButtonModule::AddMenuExtension));
AnimationBlueprintEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
{//动画蓝图拓展添加新菜单栏
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender);
MenuExtender->AddMenuBarExtension("Help", EExtensionHook::After, PluginCommands, FMenuBarExtensionDelegate::CreateRaw(this, &FMyEditorToolbarButtonModule::AddMenuBarExtension));
AnimationBlueprintEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
{//动画蓝图拓展添ToolBar
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender);
MenuExtender->AddToolBarExtension("Settings", EExtensionHook::After, PluginCommands, FToolBarExtensionDelegate::CreateRaw(this, &FMyEditorToolbarButtonModule::AddToolBarExtension));
AnimationBlueprintEditorModule.GetToolBarExtensibilityManager()->AddExtender(MenuExtender);
}
/*
If you try to do
FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>(“Kismet”),
the code will compile, but the engine will crash when starting up.
One solution I found was to change LoadingPhase in .uplugin file to PostEngineInit.
*/
{
FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>(TEXT("Kismet"));
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender);
MenuExtender->AddMenuExtension("HelpApplication", EExtensionHook::After, PluginCommands,
FMenuExtensionDelegate::CreateRaw(this, &FMyEditorToolbarButtonModule::AddMenuExtension));
BlueprintEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
}
}
void FMyEditorToolbarButtonModule::AddMenuExtension(class FMenuBuilder& Builder)
{
Builder.BeginSection(TEXT("MyButton"));
Builder.AddMenuEntry(FMyEditorToolbarButtonCommands::Get().PluginAction);
Builder.EndSection();
}
void FMyEditorToolbarButtonModule::AddMenuBarExtension(class FMenuBarBuilder& Builder)
{
Builder.AddMenuEntry(FMyEditorToolbarButtonCommands::Get().PluginAction);
}
void FMyEditorToolbarButtonModule::AddToolBarExtension(class FToolBarBuilder& Builder)
{
Builder.BeginSection(TEXT("MyButton"));
Builder.AddToolBarButton(FMyEditorToolbarButtonCommands::Get().PluginAction);
Builder.EndSection();
}
void FMyEditorToolbarButtonModule::OnBPToolBarRegister(FWorkflowAllowedTabSet& TabSet, FName Name, TSharedPtr<FBlueprintEditor> BP)
{
TSharedPtr<FExtender> ToolBarExtender = MakeShareable(new FExtender);
ToolBarExtender->AddToolBarExtension("Settings", EExtensionHook::After, PluginCommands, FToolBarExtensionDelegate::CreateRaw(this, &FMyEditorToolbarButtonModule::AddToolBarExtension));
BP->AddToolbarExtender(ToolBarExtender);
}
一个模块有扩展器容器,可以容纳很多Extender
扩展的位置取决于Extender想Add什么Extension,可以是ToolBar、MenuBar、Menu
想改那个编辑器就取出那个编辑的ExtensibilityManager然后进行添加
能够进行扩展的编辑器都继承了IHasMenuExtensibility、IHasToolBarExtensibility的接口!!
class IAnimationBlueprintEditorModule : public IModuleInterface, public IHasMenuExtensibility, public IHasToolBarExtensibility
{
}
特殊的:蓝图的编辑器不能扩展ToolBar方法特殊,如果插件要动到蓝图编辑器,我们需要将uplugin中的LoadingPhase设置到PostEngineInit在引擎初始化之后启动
class FBlueprintEditorModule : public IModuleInterface,
public IHasMenuExtensibility
{
...
}
插件面板
编辑方法
直接使用Slate做
用Slate硬写,大体上生成的UI树长这样,重要的是要绑定委托,MVC框架下,VC代码写一起
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(MyTab1Name,
FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& SpawnTabArgs) {
TSharedRef<SSlider> MySlider = SNew(SSlider).Value(0.2f);
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
// Put your tab content here!
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
MySlider
]
+ SVerticalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text_Lambda(
[MySlider]() {
int32 Result = (int32)(MySlider->GetValue() * 100);
return FText::FromString(FString::FromInt(Result));
}
)
]
];
}))
.SetDisplayName(LOCTEXT("MyTab1TabTitle", "MyTab1"))
.SetMenuType(ETabSpawnerMenuType::Disabled);
Slate语法
SNew创建对象
SAssignNew创建对象并赋值
使用设计器
局限性很大 UMG类继承Slate类,很多Slate类都没有对应的U类
1、首先设计器必须是UMG类的内容
2、然后他好像只能在关卡编辑器下起作用
自定义一个SWidget类
继承SUserWidget类,在引擎SUserWidget.cpp文件中有对应的教程
/**
* Use SUserWidget as a base class to build aggregate widgets that are not meant
* to serve as low-level building blocks. Examples include: a main menu, a user card,
* an info dialog for a selected object, a splash screen.
*
* See SUserWidgetExample
*
* SMyWidget.h
* -----------
* class SMyWidget : public SUserWidget
* {
* public:
* SLATE_USER_ARGS( SMyWidget )
* {}
* SLATE_END_ARGS()
*
* // MUST Provide this function for SNew to call!
* virtual void Construct( const FArguments& InArgs ) = 0;
*
* virtual void DoSomething() = 0;
* };
*
* SMyWidget.cpp
* -------------
* namespace Implementation
* {
* class SMyWidget : public ::SMyWidget
* {
* public:
* virtual void Construct( const FArguments& InArgs ) override
* {
* SUserWidget::Construct( SUserWidget::FArguments()
* [
* SNew(STextBlock)
* .Text( NSLOCTEXT("x", "x", "My Widget's Content") )
* ]
* }
*
* private:
* // Private implementation details can occur here
* // without ever leaking out into the .h file!
* }
* }
*
* TSharedRef<SMyWidget> SMyWidget::New()
* {
* return MakeShareable( new SMyWidget() );
* }
*/
class SUserWidget : public SCompoundWidget
{
...
}
官方也给了一个Example的类
自定义资源
创建一个自己的资源类型,一般需要两个模块,包括资源描述、资源编辑器(右键菜单要出现、特殊的编辑器等等)
plugin中分模块
依赖的模块
Editor依赖的较多,UnrealEd模块管理资源创建面板,AssetTools管理资源操作 、PropertyEditor管理属性显示,以及上面提到的资源模块
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UnrealEd",
"NewAsset",
"AssetTools",
"PropertyEditor",
// ... add private dependencies that you statically link with here ...
}
);
资源模块一般是私有的,Editor模块的build的include路径应该增加一下
toolkit 在引擎内部 等于 editor
引擎默认会给一个资源编辑界面
创建新资源
需要继承一个UFactory类,不需要绑定
/**
* 从4.25.1版开始,若想新资源类型出现在内容浏览器的右键创建菜单中,必须为它注册IAssetTypeActions对象,具体见以下修改
* https://github.com/EpicGames/UnrealEngine/commit/e8a2922b50b7659edabb9b0779ed9bfc7e593009
*/
UCLASS()
class UNewAssetFactoryNew : public UFactory
{
GENERATED_UCLASS_BODY()
public:
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override;
};
新版本
新版本的操作不太一样,需要额外实现一个资源操作类
#include "AssetTypeActions_Base.h"
class FNewAssetAction :public FAssetTypeActions_Base
{
public:
virtual uint32 GetCategories() override;
virtual FText GetName() const override;
virtual UClass* GetSupportedClass() const override;
virtual FColor GetTypeColor() const override;
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;
virtual bool HasActions(const TArray< UObject* >& InObjects) const override;
virtual void GetActions(const TArray< UObject* >& InObjects, FMenuBuilder& MenuBuilder) override;
};
再在进入模块时进行注册
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
Action = MakeShareable(new FNewAssetAction());
AssetTools.RegisterAssetTypeActions(Action.ToSharedRef());
}
关闭的时候也要处理一下
{
FAssetToolsModule* Module = FModuleManager::GetModulePtr<FAssetToolsModule>("AssetTools");
if (Module) {
IAssetTools& AssetTools = Module->Get();
if (Action.IsValid()) {
AssetTools.UnregisterAssetTypeActions(Action.ToSharedRef());
}
}
}
导入资源
工厂继承FReimportHandler类
构造函数需要继承文件的后缀,编辑器看到某个后缀的文件就将其使用该工厂进行导入
导入函数FactoryCreateFile的实现
UObject* UNewAssetFactory::FactoryCreateFile(UClass* InClass, UObject* InParent,
FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms,
FFeedbackContext* Warn, bool& bOutOperationCanceled)
{
UNewAsset* NewAsset = nullptr;
TArray<uint8> Bytes;
if (FFileHelper::LoadFileToArray(Bytes, *Filename) && Bytes.Num() >= sizeof(int32))
{
NewAsset = NewObject<UNewAsset>(InParent, InClass, InName, Flags);
for (uint32 i = 0; i < sizeof(int32); ++i) {
NewAsset->IntValue |= Bytes[i] << (i * 8);
}
}
bOutOperationCanceled = false;
return NewAsset;
}
自定义资源编辑器
在NewAssetAction类中继续重写一个OpenAssetEditor,不重写他会用默认的给我们
然后在这个Open函数里
void FNewAssetAction::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor)
{
EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone;//套路
for (auto Obj = InObjects.CreateConstIterator(); Obj; ++Obj)
{
auto NewAsset = Cast<UNewAsset>(*Obj);
if (NewAsset)
{
TSharedRef<FNewAssetToolkit> EditorToolkit = MakeShareable(new FNewAssetToolkit());
EditorToolkit->Initialize(NewAsset, Mode, EditWithinLevelEditor);
}
}
}
这里涉及到一个新类,ToolKit,其实就是和Editor一样的意思
把资源传到我们的自定义一个编辑器里
toolkit
#include "Toolkits/AssetEditorToolkit.h"
class FNewAssetToolkit : public FAssetEditorToolkit
{
public:
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
void Initialize(class UNewAsset* InNewAsset, const EToolkitMode::Type InMode, const TSharedPtr<IToolkitHost>& InToolkitHost);
virtual FText GetBaseToolkitName() const override;
virtual FName GetToolkitFName() const override;
virtual FLinearColor GetWorldCentricTabColorScale() const override;
virtual FString GetWorldCentricTabPrefix() const override;
private:
class UNewAsset* NewAsset;
TSharedPtr<STextBlock> TextBlock;
TSharedPtr<class IDetailsView> DetailsView;
};
资源编辑后需要NewAsset->MarkPackageDirty();来让编辑器知道他需要保存
自定义细节面板
没有做操作就会给一个默认的细节面板
写一个自己的类的Detail对应类,继承IDetailCustomization接口,然后这个类的特殊显示属性继承IPropertyTypeCustomization接口
#include "CoreMinimal.h"
#include "IDetailCustomization.h"
#include "IPropertyTypeCustomization.h"
class FNewAssetDetailCustomization : public IDetailCustomization
{
public:
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
};
在Module启动位置,给FPropertyEditorModule注册属性编辑器类和detail类的关系
RegisterCustomPropertyTypeLayout
RegisterCustomClassLayout
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.RegisterCustomPropertyTypeLayout("StructMember", FOnGetPropertyTypeCustomizationInstance::CreateLambda(
[] {return MakeShareable(new FNewAssetMemberCustomization); }
));
PropertyEditorModule.RegisterCustomPropertyTypeLayout("ClassMember", FOnGetPropertyTypeCustomizationInstance::CreateLambda(
[] {return MakeShareable(new FNewAssetMemberCustomization); }
));
PropertyEditorModule.RegisterCustomClassLayout("NewAsset", FOnGetDetailCustomizationInstance::CreateLambda(
[] {return MakeShareable(new FNewAssetDetailCustomization); }
));
PropertyEditorModule.NotifyCustomizationModuleChanged();
类的属性特定显示
class FNewAssetMemberCustomization : public IPropertyTypeCustomization
{
public:
virtual void CustomizeHeader(
TSharedRef<IPropertyHandle> InPropertyHandle,
FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& CustomizationUtils) override;
virtual void CustomizeChildren(
TSharedRef<IPropertyHandle> InPropertyHandle,
IDetailChildrenBuilder& ChildBuilder,
IPropertyTypeCustomizationUtils& CustomizationUtils) override;
private:
TSharedPtr<class STextBlock> TextBlock_MemberValue;
};
貌似是Header里负责填写改变属性row的样式,而children函数负责处理property数据
待补充
参考
https://zhuanlan.zhihu.com/p/338067229
https://www.bilibili.com/video/BV1tv41117qg
https://docs.unrealengine.com/4.26/zh-CN/ProductionPipelines/Plugins/