【UE4】插件与模块

一、什么是插件与模块

模块是实现某一个或一类功能的集合,当模块足够独立和庞大、复杂之后,可以将其提升为插件。UE4引擎就是由众多模块组成,而插件也可以包含一个或多个模块,但模块却不能包含插件。相对于模块来说,插件具有更高的独立性,除使用引擎模块外,一般不使用其它插件或模块。并且插件可以非常方便地移植到不同项目中使用。

二、创建插件

我们可以在插件窗口(Edit → Plugins)选择创建新的插件。
以下为UE4提供的默认插件类型:
创建插件列表
创建插件列表

三、插件目录介绍

我们创建了一个带有独立窗口的插件,并命名为SlateUI。SlateUI插件的目录:
插件目录
插件被放置在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模块。

添加新模块后的插件目录:
插件目录

BlankP.uplugin文件:

	"Modules": [
		{
			"Name": "BlankP",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		},
		{	//添加新模块
			"Name": "PluginM",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		}
	]

另外,我们还需要重新生成这个它的VS项目文件。
所以,如果是在插件中创建模块,则除了创建模块内必须的类和文件,只需要在插件配置文件中包含模块即可。

2. 项目中创建模块

前边与创建插件模块相同,先创建模块所需的模块加载类以及模块配置文件。
项目目录:
项目目录
创建完模块后,我们需要在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文件夹中。
插件目录:
插件目录
然后,在模块的配置文件中,将预编译变量设为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),一旦项目被重新编译,则预编译的插件动态库及反射文件也会被清理。还有一点是,如果将插件封装、隐藏类的定义,有时会在团队开发时,因看不到源码,变得有些棘手。

八、模块工具

开发者工具里面有一个模块工具,可以用加载、重载和编译模块,另外也可以查询模块名信息。

打开模块工具
我们可以在这里编译模块。与关卡编辑器中工具栏的热编译不同的是,这个可以只编译单个模块,当然,如果是编辑器模块就需要重启才能看到效果。
模块工具

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章