lzq198731 发表于 2024-7-15 18:54

UE扩展开发,插件的基础布局解析

首先新建个插件模板,选这个窗口这个。


编译完成以后可以在Ide看到多了个Plugins文件夹,没有的话在引擎刷新Ide项目。


先看uplugin这个文件,在虚幻引擎启动的时候,会在Plugins目录里面搜索所有的.uplugin文件,每个.uplugin代表一个插件。


里面大致的意思是:
”FileVersion”: 文件版本号。
”Version”: 插件版本号。
”VersionName”: 插件版本名称。
”FriendlyName”: 友好名称。
”Description”: 插件描述信息。
”Category”: 插件所属的分类。
”CreatedBy”: 插件的创建者姓名。
”CreatedByURL”: 插件创建者的网站链接。
”DocsURL”: 插件文档链接。
”MarketplaceURL”: 插件在商店的链接。
”SupportURL”: 插件的撑持链接。
”CanContainContent”: 暗示插件是否可以包含内容,这里的值为 false。
”IsBetaVersion”: 暗示插件是否为 Beta 版本,这里的值为 false。
”IsExperimentalVersion”: 暗示插件是否为尝试版本,这里的值为 false。
”Installed”: 暗示插件是否已被安装,这里的值为 false。
”Modules”: 描述了该插件包含的模块。这里只有一个模块 ”Sakura”,类型为 ”Editor”,加载阶段为 ”Default”。这里主要看Modules的配置,前面的带过就行。Modules即模块,一个插件可以包含多个模块,只需要在Modules里面添加即可。
”Modules”: [
                {
                        ”Name”: ”Sakura”,
                        ”Type”: ”Editor”,
                        ”LoadingPhase”: ”Default”
                },
                {
                        ”Name”: ”UEImgui”,
                        ”Type”: ”Editor”,
                        ”LoadingPhase”: ”Default”
                }
        ]接着我们看Modules里的参数,每个模块,需要配置名字、类型和加载阶段。
先说模块类型:参考文档
namespace EHostType
{
    enum Type
    {
      Runtime,
      RuntimeNoCommandlet,
      RuntimeAndProgram,
      CookedOnly,
      UncookedOnly,
      Developer,
      DeveloperTool,
      Editor,
      EditorNoCommandlet,
      EditorAndProgram,
      Program,
      ServerOnly,
      ClientOnly,
      ClientOnlyNoCommandlet,
      Max,
    }
}
Runtime任何情况城市加载。
RuntimeNoCommandletRuntime模式下但不包含命令。
RuntimeAndProgram加载到所有撑持的法式和方针上。
CookedOnly仅在已经打包好的游戏中加载。
UncookedOnly仅在未打包的游戏中加载。
Developer只在开发模式和编纂模式下加载,打包后不加载。
DeveloperTool仅在启用bBuildDeveloperTools的方针上加载。
Editor仅在编纂器启动时加载。
EditorNoCommandletEditor模式,不包含命令。
EditorAndProgram仅在编纂器和法式方针上加载。
Program仅在法式方针上加载。
ServerOnly仅在非独立客户端方针上加载。
ClientOnly仅在非独立处事器方针上加载。
ClientOnlyNoCommandlet在编纂器和客户端中加载,但在命令中不加载。接着看加载阶段:参考文档
namespace ELoadingPhase
{
    enum Type
    {
      EarliestPossible,
      PostConfigInit,
      PostSplashScreen,
      PreEarlyLoadingScreen,
      PreLoadingScreen,
      PreDefault,
      Default,
      PostDefault,
      PostEngineInit,
      None,
      Max,
    }
}
EarliestPossible        尽快加载 - 也就是说,uplugin文件可以从pak文件中加载(如果不使用pak文件,则在PlatformFile设置好后当即加载)。用于需要读取文件的插件(压缩格式等)。
PostConfigInit          在引擎完全初始化之前加载,在配置系统初始化后当即加载。
PostSplashScreen        在系统启动画面之后第一屏呈现。
PreEarlyLoadingScreen        在coreUObject之前加载,用于设置手动加载屏幕,用于我们的分块修补系统。
PreLoadingScreen        在引擎完全初始化之前加载,用于需要在加载屏幕触发之前钩入加载屏幕的模块。
PreDefault                正在默认阶段之前。
Default                    在启动期间的默认加载点加载(在初始化引擎之后,在加载游戏模块之后)。
PostDefault                在默认阶段之后。
PostEngineInit          引擎初始化后。
None                    不自动加载此模块。此外插件里面还有两个参数是白名单(WhitelistPlatforms)和黑名单(BlacklistPlatforms)。用途是定义模块只能在哪些平台下加载与不加载。
.uplugin就介绍到这,让我们继续接着看。
Resources目录,看名字就知道是放资源的,凡是情况这里放插件的图标。
Source目录,这里放的是插件的源代码。在Source目录里,每个目录代表一个模块(这里是重点!要考!)。方才我创建了一个Sakura插件,所以这里有个Sakura文件夹而且也是模块名(模块名和插件名可以不异)。


Sakura.Build.cs,这个是模块的配置,常用的配置一般是PrivateDependencyModuleNames.AddRange和PublicDependencyModuleNames.AddRange。如果你添加的模块有私有引用目录或者公共引用目录,在使用的时候也需要添加上去,比如AssetActionUtility类的模块是Blutility,但是它有一行私有引用目录,在我们的build.cs也需要添加。


回到本身的build.cs文件,添加路径(这个路径需要包罗引擎安装的路径,System.IO.Path.GetFullPath(Target.RelativeEnginePath)就是用来获取引擎路径)
System.IO.Path.GetFullPath(Target.RelativeEnginePath) + ”/Source/Editor/Blutility/Private”

打开Sakura.h,


看起来不是很复杂嘛~
先看这个,这里是模块的入口。虚幻引擎的模块担任IModuleInterface的类,然后将该类提供给IMPLEMENT_MODULE宏。IModuleInterface有几个函数会在你的模块加载和卸载时触发,类似于GameInstance**** 类中的 `Startup` 和 `Shutdown` 函数。参考文档
/** IModuleInterface implementation */
        virtual void StartupModule() override;
        virtual void ShutdownModule() override;这个是插件的点击按钮事件,这里先带过,待会再详细说说
/** This function will be bound to Command (by default it will bring up plugin window) */
        void PluginButtonClicked();这个是在东西架上和LevelEditor下拉菜单注册菜单的函数
void RegisterMenus();这个玩意是一个函数,用于生成插件的窗口视图,在插件被打开时被调用。
TSharedRef<class SDockTab> OnSpawnPluginTab(const class FSpawnTabArgs& SpawnTabArgs);这个是一个变量,用于保留插件的命令列表。它可以用来添加和改削插件的命令,并与插件窗口视图交互。
TSharedPtr<class FUICommandList> PluginCommands;打开Sakura.cpp


一滚轮滑下来,感觉还行,


让老夫细细道来~
首先看前两句
static const FName SakuraTabName(”Sakura”);

#define LOCTEXT_NAMESPACE ”FSakuraModule”首先定义了一个名为SakuraTabName的常量,在后面用于注册Nomad Tab Spawner(窗口)。
接着使用了LOCTEXT_NAMESPACE宏来定义一个定名空间,并设置定名空间名称为”FSakuraModule”,以便使用国际化(Localization)系统翻译扩展模块中的字符串。
StartupModule模块入口
void FSakuraModule::StartupModule()
{
        // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
       
        FSakuraStyle::Initialize();
        FSakuraStyle::ReloadTextures();

        FSakuraCommands::Register();
       
        PluginCommands = MakeShareable(new FUICommandList);

        PluginCommands->MapAction(
                FSakuraCommands::Get().OpenPluginWindow,
                FExecuteAction::CreateRaw(this, &FSakuraModule::PluginButtonClicked),
                FCanExecuteAction());

        UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FSakuraModule::RegisterMenus));
       
        FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SakuraTabName, FOnSpawnTab::CreateRaw(this, &FSakuraModule::OnSpawnPluginTab))
                .SetDisplayName(LOCTEXT(”FSakuraTabTitle”, ”Sakura”))
                .SetMenuType(ETabSpawnerMenuType::Hidden);
}
[*]调用 FSakuraStyle::Initialize() 和 FSakuraStyle::ReloadTextures() 函数,用于初始化和从头加载扩展模块的样式文件。
[*]调用 FSakuraCommands::Register() 函数,用于注册扩展模块的命令列表。这里使用了插件宏定义 TSharedPtr<class FUICommandList> PluginCommands;。
[*]创建一个名为 PluginCommands 的 TSharedPtr,用于存储扩展模块的命令列表,并将其映射到相应的 UI 操作。
[*]注册菜单回调函数,在引擎初始化时触发注册扩展模块的菜单。
[*]注册 Nomad Tab Spawner,创建一个新的tab页,将其定名为Sakura,并将其注册到全局 Tab Manager 中。
前面三点先不说,它们写在其他文件。让我们从第四点开始:
这段代码通过创建一个委托对象来注册一个回调函数。具体来说,它使用FSimpleMulticastDelegate类的静态函数FDelegate::CreateRaw来创建一个委托对象,并将其绑定到FSakuraModule类的RegisterMenus方式上。
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FSakuraModule::RegisterMenus));这段代码首先调用了FGlobalTabmanager::Get()函数获取全局的Tab打点器对象,然后使用RegisterNomadTabSpawner函数注册一个新的标签页生成器(Tab Spawner),使得我们可以在法式中创建并打点这个标签页。
RegisterNomadTabSpawner函数需要两个参数:第一个参数是字符串类型的Tab名称,用于标识这个标签页;第二个参数是一个委托,用于指定在需要创建这个标签页时执行的操作。
在这里,使用FOnSpawnTab::CreateRaw函数创建了一个新的委托对象,并将其绑定到FSakuraModule类的OnSpawnPluginTab方式上。OnSpawnPluginTab方式是用来创建和初始化这个标签页的。
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SakuraTabName, FOnSpawnTab::CreateRaw(this, &FSakuraModule::OnSpawnPluginTab))
                .SetDisplayName(LOCTEXT(”FSakuraTabTitle”, ”Sakura”))
                .SetMenuType(ETabSpawnerMenuType::Hidden);ShutdownModule先跳过,待会再说,让我来到OnSpawnPluginTab
TSharedRef<SDockTab> FSakuraModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
        FText WidgetText = FText::Format(
                LOCTEXT(”WindowWidgetText”, ”Add code to {0} in {1} to override this window&#39;s contents”),
                FText::FromString(TEXT(”FSakuraModule::OnSpawnPluginTab”)),
                FText::FromString(TEXT(”Sakura.cpp”))
                );

        return SNew(SDockTab)
                .TabRole(ETabRole::NomadTab)
                [
                        // Put your tab content here!
                        SNew(SBox)
                        .HAlign(HAlign_Center)
                        .VAlign(VAlign_Center)
                        [
                                SNew(STextBlock)
                                .Text(WidgetText)
                        ]
                ];
}这里他定义了一段文字,概略意思在OnSpawnPluginTab函数重写这个窗口,这个函数在Sakura.cpp。
FText WidgetText = FText::Format(
                LOCTEXT(”WindowWidgetText”, ”Add code to {0} in {1} to override this window&#39;s contents”),
                FText::FromString(TEXT(”FSakuraModule::OnSpawnPluginTab”)),
                FText::FromString(TEXT(”Sakura.cpp”))
                );让我们把这一串改成这一句
FText WidgetText = FText::FromString(TEXT(”我已经重写了,略略略~”));

让我继续细说,SNew是一个宏,用于创建一个新的Slate控件对象。具体来说,SNew宏接受一个模板参数,该参数指定了要创建的控件类别,然后使用括号运算符()暗示控件构造函数(可以带有零个或多个构造器参数)。代码中,我们使用了SNew(SDockTab)创建了一个SDockTab控件。这里,SNew的模板参数是SDockTab,括号中没有传入其他的构造参数。这相当于调用SDockTab的默认构造函数来创建一个新的控件对象。该控件以中心对齐(HAlign_Center、VAlign_Center)的方式显示在标签页的内容区域。在SBox控件中,我们又创建了一个STextBlock控件,并将其Text属性设置为上面创建的WidgetText对象,从而将提示信息显示在标签页中。
最后,我们使用TSharedRef将SDockTab控件封装成一个共享引用,并返回给调用者。参考文档
return SNew(SDockTab)
                .TabRole(ETabRole::NomadTab)
                [
                        // Put your tab content here!
                        SNew(SBox)
                        .HAlign(HAlign_Center)
                        .VAlign(VAlign_Center)
                        [
                                SNew(STextBlock)
                                .Text(WidgetText)
                        ]
                ];然后到void FSakuraModule::PluginButtonClicked(),这是一个点击事件函数在这个函数中首先调用FlobalTabmanager::Get()函数返回全局Tab打点器的指针,然后调用TryInvokeTab函数来激活一个标签页。参数就是标签页的名称。
void FSakuraModule::PluginButtonClicked()
{
        FGlobalTabmanager::Get()->TryInvokeTab(SakuraTabName);
}接着我们看void FSakuraModule::RegisterMenus()
void FSakuraModule::RegisterMenus()
{
        // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
        FToolMenuOwnerScoped OwnerScoped(this);

        {
                UToolMenu* Menu = UToolMenus::Get()->ExtendMenu(”LevelEditor.MainMenu.Window”);
                {
                        FToolMenuSection& Section = Menu->FindOrAddSection(”WindowLayout”);
                        Section.AddMenuEntryWithCommandList(FSakuraCommands::Get().OpenPluginWindow, PluginCommands);
                }
        }

        {
                UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu(”LevelEditor.LevelEditorToolBar”);
                {
                        FToolMenuSection& Section = ToolbarMenu->FindOrAddSection(”Settings”);
                        {
                                FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FSakuraCommands::Get().OpenPluginWindow));
                                Entry.SetCommandList(PluginCommands);
                        }
                }
        }
}这段代码实现了插件菜单的注册功能,用于向编纂器中添加自定义菜单项和东西栏按钮以打开插件窗口。首先,在函数中创建了一个FToolMenuOwnerScoped对象,其感化是在函数执行结束时清理注册的菜单。然后,在第一个代码块中,我们使用ExtendMenu函数来扩展LevelEditor主菜单的Window菜单项,得到一个UToolMenu对象的指针Menu,该指针引用新创建的菜单项。
接下来,在该菜单项中,我们查找一个名为”WindowLayout”的菜单区域,然后使用AddMenuEntryWithCommandList函数将自定义命令FSakuraCommands::Get().OpenPluginWindow与该菜单项关联起来。
在第二个代码块中,我们使用ExtendMenu函数来扩展LevelEditor的东西栏菜单,得到另一个UToolMenu对象的指针ToolbarMenu,该指针引用新创建的菜单项。
然后,我们查找一个名为”Settings”的菜单区域,使用AddEntry函数添加一个新东西栏按钮,并使用SetCommandList函数将自定义命令FSakuraCommands::Get().OpenPluginWindow与该按钮关联起来。
通俗地讲,这段代码实现了在LevelEditor主菜单和东西栏中添加一个名为”Sakura”的菜单项和按钮,用于打开插件窗口。此中,菜单项和东西栏按钮都与OpenPluginWindow命令相关联。




回到void FSakuraModule::StartupModule(),方才我们有1.2.3点没讲。

[*]调用 FSakuraStyle::Initialize() 和 FSakuraStyle::ReloadTextures() 函数,用于初始化和从头加载扩展模块的样式文件。
[*]调用 FSakuraCommands::Register() 函数,用于注册扩展模块的命令列表。这里使用了插件宏定义 TSharedPtr<class FUICommandList> PluginCommands;。
[*]创建一个名为 PluginCommands 的 TSharedPtr,用于存储扩展模块的命令列表,并将其映射到相应的 UI 操作。
void FSakuraModule::StartupModule()
{
        // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
       
        FSakuraStyle::Initialize();
        FSakuraStyle::ReloadTextures();

        FSakuraCommands::Register();
       
        PluginCommands = MakeShareable(new FUICommandList);

        PluginCommands->MapAction(
                FSakuraCommands::Get().OpenPluginWindow,
                FExecuteAction::CreateRaw(this, &FSakuraModule::PluginButtonClicked),
                FCanExecuteAction());

        UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FSakuraModule::RegisterMenus));
       
        FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SakuraTabName, FOnSpawnTab::CreateRaw(this, &FSakuraModule::OnSpawnPluginTab))
                .SetDisplayName(LOCTEXT(”FSakuraTabTitle”, ”Sakura”))
                .SetMenuType(ETabSpawnerMenuType::Hidden);
}首先说1.用于初始化和从头加载扩展模块的样式文件。没啥意思,就是加载Resources里的图标,注释掉编纂器上的图标就无了。
      // FSakuraStyle::Initialize();
        // FSakuraStyle::ReloadTextures();

接下来说后面两点,打开SakuraCommands.h


在FSakuraCommands类的构造函数中,我们调用父类TCommands<FSakuraCommands>的构造函数,传入指定的名称、上下文和样式集参数。这里,我们将该命令集定名为”Sakura”,上下文名称为”Sakura Plugin”,样式集使用FSakuraStyle::GetStyleSetName()来设置。
FSakuraCommands()
                : TCommands<FSakuraCommands>(TEXT(”Sakura”), NSLOCTEXT(”Contexts”, ”Sakura”, ”Sakura Plugin”), NAME_None, FSakuraStyle::GetStyleSetName())
        {
        }接下来,声明了一个指向FUICommandInfo类型的共享指针OpenPluginWindow。此指针用于在创建菜单项和东西栏按钮时与命令关联起来。
TSharedPtr< FUICommandInfo > OpenPluginWindow;最后,声明了一个RegisterCommands函数,用于注册自定义命令。
virtual void RegisterCommands() override;打开SakuraCommands.cpp文件


在RegisterCommands函数中,我们使用UI_COMMAND宏来注册自定义命令OpenPluginWindow。该宏的参数依次为:命令名称、在菜单项、东西栏按钮以及右键菜单中显示的文本、执行类型和输入手势。这里,我们将命令名称设为”Sakura”,文本为”Bring up Sakura window”,执行类型设为Button,输入手势为空。
void FSakuraCommands::RegisterCommands()
{
        UI_COMMAND(OpenPluginWindow, ”Sakura”, ”Bring up Sakura window”, EUserInterfaceActionType::Button, FInputGesture());
}回到Sakura.cpp
      FSakuraCommands::Register();
       
        PluginCommands = MakeShareable(new FUICommandList);

        PluginCommands->MapAction(
                FSakuraCommands::Get().OpenPluginWindow,
                FExecuteAction::CreateRaw(this, &FSakuraModule::PluginButtonClicked),
                FCanExecuteAction());首先,我们调用了自定义命令类FSakuraCommands的函数Register(),该函数通过调用自定义命令类的RegisterCommands函数来注册自定义命令OpenPluginWindow及其相关信息。
接下来,我们创建了一个指向FUICommandList类型的共享指针PluginCommands,该指针用于存储所有与该插件相关联的自定义命令。
然后,我们使用PluginCommands->MapAction函数将自定义命令OpenPluginWindow与回调函数PluginButtonClicked关联起来。
MapAction函数的第一个参数为要关联的自定义命令,该命令在FSakuraCommands类中声明并实例化,并在RegisterCommands函数中注册了该命令。FSakuraCommands::Get().OpenPluginWindow暗示获取OpenPluginWindow自定义命令的引用。
MapAction函数的第二个参数为一个执行命令时要调用的回调函数。这里我们使用FExecuteAction::CreateRaw来封装回调函数PluginButtonClicked,CreateRaw函数会将当前对象(即FSakuraModule)和回调函数PluginButtonClicked封装成一个可以执行的Function指针。
MapAction函数的第三个参数是一个可选参数,类型为FCanExecuteAction。该参数是一个返回值为布尔类型的函数指针,用于测试当前自定义命令是否可执行。如果测试函数返回true,则暗示自定义命令可以被执行;如果返回false,则自定义命令无法执行。
这里我使用FCanExecuteAction委托接口。当动作能够执行时返回true。
剩下最后一部门void FSakuraModule::ShutdownModule(),卸载模块
void FSakuraModule::ShutdownModule()
{
        // This function may be called during shutdown to clean up your module.For modules that support dynamic reloading,
        // we call this function before unloading the module.

        UToolMenus::UnRegisterStartupCallback(this);

        UToolMenus::UnregisterOwner(this);

        FSakuraStyle::Shutdown();

        FSakuraCommands::Unregister();

        FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(SakuraTabName);
}首先,我们调用了UToolMenus类的UnRegisterStartupCallback函数,并将当前模块对象作为参数传入。这个函数是用来打消注册编纂器菜单系统的启动回调函数,以防止不才次启动编纂器时反复注册。
然后,我们又调用了UToolMenus类的UnregisterOwner函数,并将当前模块对象作为参数传入。这个函数是用来打消当前模块在Unreal编纂器菜单系统中注册的所有菜单和东西栏按钮。这样,不才次加载该模块时就不会发生反复项。
接着,我们调用FSakuraStyle类的静态函数Shutdown(),用于释放自定义UI样式资源。
接下来,我们调用FSakuraCommands类的静态函数Unregister(),用于注销自定义命令和相关信息。这样,不才次加载该模块时就不会发生命令反复声明的问题。
最后,我们调用FGlobalTabmanager类的静态函数UnregisterNomadTabSpawner,并传入模块名称作为参数。这个函数是用于注销自定义标签页生成器的,如果该模块注册了自定义标签页,则需要在模块卸载时将其注销,以免不才次加载该模块时呈现反复的标签页。

新手发文不免有错误纰漏,还得请大师多多包容!
参考文献
https://docs.unrealengine.com/5.0/zh-CN/unreal-engine-modules/
https://docs.unrealengine.com/5.1/en-US/API/Runtime/Projects/EHostType__Type/
https://docs.unrealengine.com/5.1/en-US/API/Runtime/Projects/ELoadingPhase__Type/
虚幻引擎编纂器扩展基础 | 安宁 武汉艺术先生数码科技有限公司_哔哩哔哩_bilibili
SDockTab | Unreal Engine Documentation
大象无形_虚幻引擎法式设计浅析
页: [1]
查看完整版本: UE扩展开发,插件的基础布局解析