【UE4】插件与模块
一、什么是插件与模块
模块是实现某一个或一类功能的集合,当模块足够独立和庞大、复杂之后,可以将其提升为插件。UE4引擎就是由众多模块组成,而插件也可以包含一个或多个模块,但模块却不能包含插件。相对于模块来说,插件具有更高的独立性,除使用引擎模块外,一般不使用其它插件或模块。并且插件可以非常方便地移植到不同项目中使用。
二、创建插件
我们可以在插件窗口(Edit → Plugins)选择创建新的插件。 以下为UE4提供的默认插件类型:
![](https://pic3.zhimg.com/80/v2-412fb2e7d4977b668f7c2455af0ced5a_1440w.webp)
![](https://pic3.zhimg.com/80/v2-6265e7e9116114412467f0a72bf4b75e_1440w.webp)
三、插件目录介绍
我们创建了一个带有独立窗口的插件,并命名为SlateUI。SlateUI插件的目录:
![](https://pic3.zhimg.com/80/v2-455da34298515574bcef5f50594ff952_1440w.webp)
插件被放置在Plugins目录下,这个目录包含的是项目插件。 在Source目录下有个SlateUI的文件夹,这个文件夹就是SlateUI插件下的SlateUI模块,每个插件有且至少有一个模块,这个SlateUI模块就是创建插件时生成的默认模块。每个模块拥有在Source目录下的独立文件夹,并且还有一个“ModuleName.Build.cs”的模块配置文件。
四、配置文件
1、插件
SlateUI.uplugin文件:
{
"FileVersion": 3,
"Version": 1, //版本号
"VersionName": "1.0", //版本名
"FriendlyName": "SlateUI", //插件名
"Description": "", //插件描述
"Category": "Other", //插件目录,这个会将其分类到插件启用页面的相应目录下
"CreatedBy": "", //作者
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": false, //是否包含Content目录
"IsBetaVersion": false,
"Installed": false,
"Modules": [ //插件包含的模块,新创建的插件会默认包含一个同名的模块
{
"Name": "SlateUI", //模块名,这里就是创建插件时,默认创建的模块SlateUI
"Type": "Editor", //模块类型,表示模块在什么场景下使用,类型为EHostType
"LoadingPhase": "Default" //模块加载的阶段,类型为ELoadingPhase
}
]
}
Type可填写的值范围:
namespace EHostType
{
enum Type
{
Runtime, //运行时,任何情况下
RuntimeNoCommandlet,
RuntimeAndProgram,
CookedOnly,
Developer, //开发时使用的插件
Editor, //编辑器类型插件
EditorNoCommandlet,
Program, //只有运行独立程序时的插件
ServerOnly,
ClientOnly,
Max
};
}
LoadingPhase的值范围:
namespace ELoadingPhase
{
enum Type
{
PostConfigInit, //引擎完全加载前,配置文件加载后。适用于较底层的模块。
PreEarlyLoadingScreen, //在UObject加载前,用于补丁系统
PreLoadingScreen, //在引擎模块完全加载和加载页面之前
PreDefault, //默认模块加载之前阶段
Default, //默认加载阶段,在引擎初始化时,游戏模块加载之后
PostDefault, //默认加载阶段之后加载
PostEngineInit, //引擎初始化后
None, //不自动加载模块
Max
};
}
2、模块
SlateUI.build.cs文件:
using UnrealBuildTool;
public class SlateUI : ModuleRules
{
public SlateUI(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
//填入引擎模块的某个子目录后,引用包含的头文件可以省去前面的路径
PublicIncludePaths.AddRange(new string[] {/* ......*/});
//填入项目或项目插件某个模块的子目录后,引用包含的头文件可以省去前面的路径
PrivateIncludePaths.AddRange(new string[] {/* ......*/});
//如果此模块依赖其它模块,需要将其添加到下面两个变量中的一个,区别如下
//如果其它模块依赖此模块,则其也可以访问Core模块
PublicDependencyModuleNames.AddRange(new string[]{"Core",});
//如果其它模块依赖此模块,但其不可以访问下面的模块
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Projects", "InputCore", "UnrealEd", "LevelEditor", "CoreUObject", "Engine", "Slate", "SlateCore",
}
);
//动态加载的模块,动态加载和静态加载不在本节讨论范围
DynamicallyLoadedModuleNames.AddRange(new string[]{/* ......*/});
}
}
3、项目
如果我们想使用插件中的某个模块,首先要启用这个插件,我们可以在插件窗口选择Enable插件,或者在文件中配置属性。 Game.uproject文件:
{
"FileVersion": 3,
"EngineAssociation": "4.21",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "StartGame",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine"
]
}
],
"Plugins":[ //添加插件
{
"Name": "BlankP",
"Enabled": true
}
]
}
并且,我们需要在项目模块中添加依赖的模块名: Game.build.cs文件:
using UnrealBuildTool;
public class StartGame : ModuleRules
{
public StartGame(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
//我们这里添加"BlankP"模块的依赖
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "MoviePlayer", "UMG", "BlankP" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
五、创建模块
1. 插件中创建模块
我们创建了一个名为BlankP的插件,它会默认创建一个包含BlankP的模块,我们在此插件下再创建一个名为PluginM的模块。 首先在BlankP插件Souce目录下,将BlankP目录复制一份,并将其命名为PluginM,并且修改其配置文件与代码,将所有BlankP修改为PluginM。并且在插件配置文件中包含PluginM模块。
添加新模块后的插件目录:
![](https://pic3.zhimg.com/80/v2-f238122020881a5034489dd65fae2112_1440w.webp)
BlankP.uplugin文件:
"Modules": [
{
"Name": "BlankP",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{ //添加新模块
"Name": "PluginM",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
另外,我们还需要重新生成这个它的VS项目文件。 所以,如果是在插件中创建模块,则除了创建模块内必须的类和文件,只需要在插件配置文件中包含模块即可。
2. 项目中创建模块
前边与创建插件模块相同,先创建模块所需的模块加载类以及模块配置文件。 项目目录:
![](https://pic4.zhimg.com/80/v2-92fd928b06864c1f3f3f70b46ab1a947_1440w.webp)
创建完模块后,我们需要在StartGame.uproject中添加模块,这步操作类似在插件中添加模块:
"Modules": [
{ //项目包含的模块,因为我没有创建新的项目模块,所以这里只有一个默认的StartGame
"Name": "StartGame",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine"
]
}
]
其次,我们要在StartGame.Target.cs和StartGameEditor.Target.cs中添加模块,如果模块只是在编辑器中有效,则只需要在StartGameEditor.Target.cs中添加。在此处添加模块后,模块才会被链接编译。 StartGame.Target.cs文件:
using UnrealBuildTool;
using System.Collections.Generic;
public class StartGameTarget : TargetRules
{
public StartGameTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
ExtraModuleNames.AddRange( new string[] { "StartGame" } ); //新模块添加在此处
}
}
StartGameEditor.Target.cs文件:
using UnrealBuildTool;
using System.Collections.Generic;
public class StartGameEditorTarget : TargetRules
{
public StartGameEditorTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
ExtraModuleNames.AddRange( new string[] { "StartGame" } ); //新模块添加在此处
}
}
在游戏项目中,我们可以按照LoadingScreen模块、AI模块、Gameplay模块等来将不同的模块分类。
六、模块加载与卸载
在模块文件中,有个继承IModuleInterface的类,其中定义了两个方法,分别是StartupModule()和ShutdownModule()。这两个方法分别在模块加载和卸载时执行,所以,我们可以在这两个方法中执行加载任务和内存清理的功能。 当然,我们也可以将其添加到游戏逻辑模块中,执行游戏模块加载和卸载的一些必要任务。 游戏模块的头文件:
//一般模块类继承的是IModuleInterface,FDefaultGameModuleImpl是IModuleInterface的封装,游戏模块可继承此类
class FStartGameModule : public FDefaultGameModuleImpl
{
public:
virtual void StartupModule() override; //模块加载完成后执行此方法
virtual void ShutdownModule() override; //模块卸载期间执行此方法
};
定义:
#define LOCTEXT_NAMESPACE "FStartGameModule" //这个是为语言国际化用的
void FStartGameModule::StartupModule()
{
//模块加载完成后执行此方法
}
void FStartGameModule::ShutdownModule()
{
//模块卸载期间执行此方法
}
#undef LOCTEXT_NAMESPACE
//这个宏只有唯一的游戏模块可以使用,其它模块使用注释掉的宏,否则打包会失败!
//这个宏负责将模块注册,模块的加载与卸载进入生命周期流程
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, StartGame, "StartGame" );
//IMPLEMENT_MODULE(FNewMModule, NewM)
七、插件封装
如果插件允许暴露类的定义给使用者的话,我们可以直接将Plugins下的插件目录直接提供给插件的使用者。 但,如果不想要插件中类的定义暴露给使用者,则需要进行一些处理。 首先,需要编译插件的不同版本。然后,编译好的动态库文件以及反射文件会分别被保存在Binaries和Intermediate文件夹中。 插件目录:
![](https://pic2.zhimg.com/80/v2-65659a40dbe332066208710ff6d4103d_1440w.webp)
然后,在模块的配置文件中,将预编译变量设为true,这样再编译项目的时候,使用预编译的模块将跳过编译。
using UnrealBuildTool;
public class BlankP : ModuleRules
{
public BlankP(ReadOnlyTargetRules Target) : base(Target)
{
bUsePrecompiled = true; //使用预编译设为true,模块将跳过编译
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(new string[] {});
PrivateIncludePaths.AddRange(new string[] {});
PublicDependencyModuleNames.AddRange(new string[]{"Core",});
PrivateDependencyModuleNames.AddRange(new string[]{"CoreUObject", "Engine", "Slate", "SlateCore",});
DynamicallyLoadedModuleNames.AddRange(new string[]{});
}
}
然后,我们先预编译一遍各个版本的插件,然后将使用预编译设为true。这样,我们即使删除类定义文件(.cpp)也不会影响插件的使用。 但是,需要注意的是,项目不能被重新编译(Rebuilt),一旦项目被重新编译,则预编译的插件动态库及反射文件也会被清理。还有一点是,如果将插件封装、隐藏类的定义,有时会在团队开发时,因看不到源码,变得有些棘手。