【UE4】插件与模块

发布时间 2023-05-23 11:36:36作者: tomato-haha

一、什么是插件与模块

模块是实现某一个或一类功能的集合,当模块足够独立和庞大、复杂之后,可以将其提升为插件。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),一旦项目被重新编译,则预编译的插件动态库及反射文件也会被清理。还有一点是,如果将插件封装、隐藏类的定义,有时会在团队开发时,因看不到源码,变得有些棘手。