找回密码
 立即注册
查看: 700|回复: 0

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

[复制链接]
发表于 2024-7-15 18:54 | 显示全部楼层 |阅读模式
首先新建个插件模板,选这个窗口这个。


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


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


里面大致的意思是:
  1. ”FileVersion”: 文件版本号。
  2. ”Version”: 插件版本号。
  3. ”VersionName”: 插件版本名称。
  4. ”FriendlyName”: 友好名称。
  5. ”Description”: 插件描述信息。
  6. ”Category”: 插件所属的分类。
  7. ”CreatedBy”: 插件的创建者姓名。
  8. ”CreatedByURL”: 插件创建者的网站链接。
  9. ”DocsURL”: 插件文档链接。
  10. ”MarketplaceURL”: 插件在商店的链接。
  11. ”SupportURL”: 插件的撑持链接。
  12. ”CanContainContent”: 暗示插件是否可以包含内容,这里的值为 false。
  13. ”IsBetaVersion”: 暗示插件是否为 Beta 版本,这里的值为 false。
  14. ”IsExperimentalVersion”: 暗示插件是否为尝试版本,这里的值为 false。
  15. ”Installed”: 暗示插件是否已被安装,这里的值为 false。
  16. ”Modules”: 描述了该插件包含的模块。这里只有一个模块 ”Sakura”,类型为 ”Editor”,加载阶段为 ”Default”。
复制代码
这里主要看Modules的配置,前面的带过就行。Modules即模块,一个插件可以包含多个模块,只需要在Modules里面添加即可。
  1. ”Modules”: [
  2.                 {
  3.                         ”Name”: ”Sakura”,
  4.                         ”Type”: ”Editor”,
  5.                         ”LoadingPhase”: ”Default”
  6.                 },
  7.                 {
  8.                         ”Name”: ”UEImgui”,
  9.                         ”Type”: ”Editor”,
  10.                         ”LoadingPhase”: ”Default”
  11.                 }
  12.         ]
复制代码
接着我们看Modules里的参数,每个模块,需要配置名字、类型和加载阶段。
先说模块类型:参考文档
  1. namespace EHostType
  2. {
  3.     enum Type
  4.     {
  5.         Runtime,
  6.         RuntimeNoCommandlet,
  7.         RuntimeAndProgram,
  8.         CookedOnly,
  9.         UncookedOnly,
  10.         Developer,
  11.         DeveloperTool,
  12.         Editor,
  13.         EditorNoCommandlet,
  14.         EditorAndProgram,
  15.         Program,
  16.         ServerOnly,
  17.         ClientOnly,
  18.         ClientOnlyNoCommandlet,
  19.         Max,
  20.     }
  21. }
  22. Runtime任何情况城市加载。
  23. RuntimeNoCommandletRuntime模式下但不包含命令。
  24. RuntimeAndProgram加载到所有撑持的法式和方针上。
  25. CookedOnly仅在已经打包好的游戏中加载。
  26. UncookedOnly仅在未打包的游戏中加载。
  27. Developer只在开发模式和编纂模式下加载,打包后不加载。
  28. DeveloperTool仅在启用bBuildDeveloperTools的方针上加载。
  29. Editor仅在编纂器启动时加载。
  30. EditorNoCommandletEditor模式,不包含命令。
  31. EditorAndProgram仅在编纂器和法式方针上加载。
  32. Program仅在法式方针上加载。
  33. ServerOnly仅在非独立客户端方针上加载。
  34. ClientOnly仅在非独立处事器方针上加载。
  35. ClientOnlyNoCommandlet在编纂器和客户端中加载,但在命令中不加载。
复制代码
接着看加载阶段:参考文档
  1. namespace ELoadingPhase
  2. {
  3.     enum Type
  4.     {
  5.         EarliestPossible,
  6.         PostConfigInit,
  7.         PostSplashScreen,
  8.         PreEarlyLoadingScreen,
  9.         PreLoadingScreen,
  10.         PreDefault,
  11.         Default,
  12.         PostDefault,
  13.         PostEngineInit,
  14.         None,
  15.         Max,
  16.     }
  17. }
  18. EarliestPossible        尽快加载 - 也就是说,uplugin文件可以从pak文件中加载(如果不使用pak文件,则在PlatformFile设置好后当即加载)。用于需要读取文件的插件(压缩格式等)。
  19. PostConfigInit            在引擎完全初始化之前加载,在配置系统初始化后当即加载。
  20. PostSplashScreen        在系统启动画面之后第一屏呈现。
  21. PreEarlyLoadingScreen        在coreUObject之前加载,用于设置手动加载屏幕,用于我们的分块修补系统。
  22. PreLoadingScreen        在引擎完全初始化之前加载,用于需要在加载屏幕触发之前钩入加载屏幕的模块。
  23. PreDefault                正在默认阶段之前。
  24. Default                    在启动期间的默认加载点加载(在初始化引擎之后,在加载游戏模块之后)。
  25. PostDefault                在默认阶段之后。
  26. PostEngineInit            引擎初始化后。
  27. 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)就是用来获取引擎路径)
  1. System.IO.Path.GetFullPath(Target.RelativeEnginePath) + ”/Source/Editor/Blutility/Private”
复制代码

打开Sakura.h,


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


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


让老夫细细道来~
首先看前两句
  1. static const FName SakuraTabName(”Sakura”);
  2. #define LOCTEXT_NAMESPACE ”FSakuraModule”
复制代码
首先定义了一个名为SakuraTabName的常量,在后面用于注册Nomad Tab Spawner(窗口)。
接着使用了LOCTEXT_NAMESPACE宏来定义一个定名空间,并设置定名空间名称为”FSakuraModule”,以便使用国际化(Localization)系统翻译扩展模块中的字符串。
StartupModule模块入口
  1. void FSakuraModule::StartupModule()
  2. {
  3.         // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
  4.        
  5.         FSakuraStyle::Initialize();
  6.         FSakuraStyle::ReloadTextures();
  7.         FSakuraCommands::Register();
  8.        
  9.         PluginCommands = MakeShareable(new FUICommandList);
  10.         PluginCommands->MapAction(
  11.                 FSakuraCommands::Get().OpenPluginWindow,
  12.                 FExecuteAction::CreateRaw(this, &FSakuraModule::PluginButtonClicked),
  13.                 FCanExecuteAction());
  14.         UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FSakuraModule::RegisterMenus));
  15.        
  16.         FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SakuraTabName, FOnSpawnTab::CreateRaw(this, &FSakuraModule::OnSpawnPluginTab))
  17.                 .SetDisplayName(LOCTEXT(”FSakuraTabTitle”, ”Sakura”))
  18.                 .SetMenuType(ETabSpawnerMenuType::Hidden);
  19. }
复制代码

  • 调用 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方式上。
  1. UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FSakuraModule::RegisterMenus));
复制代码
这段代码首先调用了FGlobalTabmanager::Get()函数获取全局的Tab打点器对象,然后使用RegisterNomadTabSpawner函数注册一个新的标签页生成器(Tab Spawner),使得我们可以在法式中创建并打点这个标签页。
RegisterNomadTabSpawner函数需要两个参数:第一个参数是字符串类型的Tab名称,用于标识这个标签页;第二个参数是一个委托,用于指定在需要创建这个标签页时执行的操作。
在这里,使用FOnSpawnTab::CreateRaw函数创建了一个新的委托对象,并将其绑定到FSakuraModule类的OnSpawnPluginTab方式上。OnSpawnPluginTab方式是用来创建和初始化这个标签页的。
  1. FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SakuraTabName, FOnSpawnTab::CreateRaw(this, &FSakuraModule::OnSpawnPluginTab))
  2.                 .SetDisplayName(LOCTEXT(”FSakuraTabTitle”, ”Sakura”))
  3.                 .SetMenuType(ETabSpawnerMenuType::Hidden);
复制代码
ShutdownModule先跳过,待会再说,让我来到OnSpawnPluginTab
  1. TSharedRef<SDockTab> FSakuraModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
  2. {
  3.         FText WidgetText = FText::Format(
  4.                 LOCTEXT(”WindowWidgetText”, ”Add code to {0} in {1} to override this window&#39;s contents”),
  5.                 FText::FromString(TEXT(”FSakuraModule::OnSpawnPluginTab”)),
  6.                 FText::FromString(TEXT(”Sakura.cpp”))
  7.                 );
  8.         return SNew(SDockTab)
  9.                 .TabRole(ETabRole::NomadTab)
  10.                 [
  11.                         // Put your tab content here!
  12.                         SNew(SBox)
  13.                         .HAlign(HAlign_Center)
  14.                         .VAlign(VAlign_Center)
  15.                         [
  16.                                 SNew(STextBlock)
  17.                                 .Text(WidgetText)
  18.                         ]
  19.                 ];
  20. }
复制代码
这里他定义了一段文字,概略意思在OnSpawnPluginTab函数重写这个窗口,这个函数在Sakura.cpp。
  1. FText WidgetText = FText::Format(
  2.                 LOCTEXT(”WindowWidgetText”, ”Add code to {0} in {1} to override this window&#39;s contents”),
  3.                 FText::FromString(TEXT(”FSakuraModule::OnSpawnPluginTab”)),
  4.                 FText::FromString(TEXT(”Sakura.cpp”))
  5.                 );
复制代码
让我们把这一串改成这一句
  1. FText WidgetText = FText::FromString(TEXT(”我已经重写了,略略略~”));
复制代码

让我继续细说,SNew是一个宏,用于创建一个新的Slate控件对象。具体来说,SNew宏接受一个模板参数,该参数指定了要创建的控件类别,然后使用括号运算符()暗示控件构造函数(可以带有零个或多个构造器参数)。代码中,我们使用了SNew(SDockTab)创建了一个SDockTab控件。这里,SNew的模板参数是SDockTab,括号中没有传入其他的构造参数。这相当于调用SDockTab的默认构造函数来创建一个新的控件对象。该控件以中心对齐(HAlign_Center、VAlign_Center)的方式显示在标签页的内容区域。在SBox控件中,我们又创建了一个STextBlock控件,并将其Text属性设置为上面创建的WidgetText对象,从而将提示信息显示在标签页中。
最后,我们使用TSharedRef将SDockTab控件封装成一个共享引用,并返回给调用者。参考文档
  1. return SNew(SDockTab)
  2.                 .TabRole(ETabRole::NomadTab)
  3.                 [
  4.                         // Put your tab content here!
  5.                         SNew(SBox)
  6.                         .HAlign(HAlign_Center)
  7.                         .VAlign(VAlign_Center)
  8.                         [
  9.                                 SNew(STextBlock)
  10.                                 .Text(WidgetText)
  11.                         ]
  12.                 ];
复制代码
然后到void FSakuraModule::PluginButtonClicked(),这是一个点击事件函数在这个函数中首先调用FlobalTabmanager::Get()函数返回全局Tab打点器的指针,然后调用TryInvokeTab函数来激活一个标签页。参数就是标签页的名称。
  1. void FSakuraModule::PluginButtonClicked()
  2. {
  3.         FGlobalTabmanager::Get()->TryInvokeTab(SakuraTabName);
  4. }
复制代码
接着我们看void FSakuraModule::RegisterMenus()
  1. void FSakuraModule::RegisterMenus()
  2. {
  3.         // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
  4.         FToolMenuOwnerScoped OwnerScoped(this);
  5.         {
  6.                 UToolMenu* Menu = UToolMenus::Get()->ExtendMenu(”LevelEditor.MainMenu.Window”);
  7.                 {
  8.                         FToolMenuSection& Section = Menu->FindOrAddSection(”WindowLayout”);
  9.                         Section.AddMenuEntryWithCommandList(FSakuraCommands::Get().OpenPluginWindow, PluginCommands);
  10.                 }
  11.         }
  12.         {
  13.                 UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu(”LevelEditor.LevelEditorToolBar”);
  14.                 {
  15.                         FToolMenuSection& Section = ToolbarMenu->FindOrAddSection(”Settings”);
  16.                         {
  17.                                 FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FSakuraCommands::Get().OpenPluginWindow));
  18.                                 Entry.SetCommandList(PluginCommands);
  19.                         }
  20.                 }
  21.         }
  22. }
复制代码
这段代码实现了插件菜单的注册功能,用于向编纂器中添加自定义菜单项和东西栏按钮以打开插件窗口。首先,在函数中创建了一个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 操作。
  1. void FSakuraModule::StartupModule()
  2. {
  3.         // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
  4.        
  5.         FSakuraStyle::Initialize();
  6.         FSakuraStyle::ReloadTextures();
  7.         FSakuraCommands::Register();
  8.        
  9.         PluginCommands = MakeShareable(new FUICommandList);
  10.         PluginCommands->MapAction(
  11.                 FSakuraCommands::Get().OpenPluginWindow,
  12.                 FExecuteAction::CreateRaw(this, &FSakuraModule::PluginButtonClicked),
  13.                 FCanExecuteAction());
  14.         UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FSakuraModule::RegisterMenus));
  15.        
  16.         FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SakuraTabName, FOnSpawnTab::CreateRaw(this, &FSakuraModule::OnSpawnPluginTab))
  17.                 .SetDisplayName(LOCTEXT(”FSakuraTabTitle”, ”Sakura”))
  18.                 .SetMenuType(ETabSpawnerMenuType::Hidden);
  19. }
复制代码
首先说1.用于初始化和从头加载扩展模块的样式文件。没啥意思,就是加载Resources里的图标,注释掉编纂器上的图标就无了。
  1.         // FSakuraStyle::Initialize();
  2.         // FSakuraStyle::ReloadTextures();
复制代码

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


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


在RegisterCommands函数中,我们使用UI_COMMAND宏来注册自定义命令OpenPluginWindow。该宏的参数依次为:命令名称、在菜单项、东西栏按钮以及右键菜单中显示的文本、执行类型和输入手势。这里,我们将命令名称设为”Sakura”,文本为”Bring up Sakura window”,执行类型设为Button,输入手势为空。
  1. void FSakuraCommands::RegisterCommands()
  2. {
  3.         UI_COMMAND(OpenPluginWindow, ”Sakura”, ”Bring up Sakura window”, EUserInterfaceActionType::Button, FInputGesture());
  4. }
复制代码
回到Sakura.cpp
  1.         FSakuraCommands::Register();
  2.        
  3.         PluginCommands = MakeShareable(new FUICommandList);
  4.         PluginCommands->MapAction(
  5.                 FSakuraCommands::Get().OpenPluginWindow,
  6.                 FExecuteAction::CreateRaw(this, &FSakuraModule::PluginButtonClicked),
  7.                 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(),卸载模块
  1. void FSakuraModule::ShutdownModule()
  2. {
  3.         // This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
  4.         // we call this function before unloading the module.
  5.         UToolMenus::UnRegisterStartupCallback(this);
  6.         UToolMenus::UnregisterOwner(this);
  7.         FSakuraStyle::Shutdown();
  8.         FSakuraCommands::Unregister();
  9.         FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(SakuraTabName);
  10. }
复制代码
首先,我们调用了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/
[UnrealCircle武汉]虚幻引擎编纂器扩展基础 | 安宁 武汉艺术先生数码科技有限公司_哔哩哔哩_bilibili
SDockTab | Unreal Engine Documentation
大象无形_虚幻引擎法式设计浅析

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-4 00:47 , Processed in 0.109670 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表