UE4 Module 新建与使用

2023-10-20 09:08:20 浏览数 (2)

UE 的 module 是一堆 C 类和代码的集合,类似于 DLL,而 UE 本身也是由一堆 module 构成的。将代码拆分为 module 的目的是:

  • 封装和功能分离
  • 方便重用代码
  • 降低编译链接时间,减小编译结果体积
  • 允许控制加载 module 的时机

目录结构 #

假设需要创建一个名为 FooBar 的 module,那么首先需要在工程的 Source 目录下创建一个与 module 同名的目录(此处为 FooBar),然后在该目录下创建一个 FooBar.Build.cs 文件,大致会有如下的目录结构:

代码语言:javascript复制
ProjectFolder
└── Source
    └── FooBar  <----------------- module 对应目录
        ├── FooBar.Build.cs  <---- 构建描述文件
        ├── Private  <------------ 私有文件目录
        └── Public  <------------- 公开文件目录

YourModuleName.Build.cs 文件的功能:

  • 声明 module 的构建方式
  • 定义 module 的依赖
  • ……

UE 基于 C# 实现了名为 UBT(Unreal Build Tool)的工具完成构建流程。UBT 根据 .Target.cs 文件和 .Build.cs 文件对一个工程进行构建。它不依赖于特定平台或特定 IDE 的构建描述(如 Visual Studio 的 .sln 文件),因为 UE 要支持不同平台的编译。

在修改或移动 .Build.cs 文件后,最好重新生成一下 IDE 的 Solution 文件,以便 IDE 能同步到最新构建信息,有几种方法可以生成 IDE 的 Solution 文件:

  • 执行 GenerateProject.bat 脚本
  • 右键点击 .uproject 文件,然后选择「Generate xxx Project Files」
  • 在 UE 编辑器的菜单中选择 File - Refresh xxx Project

最简单的 .Build.cs 文件:

代码语言:javascript复制
using UnrealBuildTool;
public class FooBar : ModuleRules {
    public FooBar(ReadOnlyTargetRules Target) : base(Target) {
        PublicDependencyModuleNames.AddRange(new string[] {
            // UE 的 Engine 包含了一些常用的内容,例如 AActor
            "Engine" });
        PrivateDependencyModuleNames.AddRange(new string[] {
            // UE 的 Core module 包含了处理 module 的代码,所以至少要包含它
            "Core" });
    }
}

这里 PrivateDependencyModuleNames 用于声明只在该 module 内部依赖的 module,而 PublicDependencyModuleNames 则用于声明在该 module 对外暴露的接口中依赖到的 module,这个依赖关系会被依赖该 module 的 module 继承,这个关系和 C 类继承中的 privatepublic 关键字的区别类似。例如本 module 对外暴露了一个继承自 AActor 的类型,由于 AActor 被定义在 Engine module 中,因此这里需要将 Engine 添加到 PublicDependencyModuleNames 中。

实现 Module #

注意到上面的目录中源码文件被分别放在两个目录下,一个 Public 一个 Private,其中,需要被其他 module include 的头文件放在 Public 目录下,其他文件(包括源码文件和本 module 私有的头文件)都放在 Private 目录下。

代码语言:javascript复制
ProjectFolder
└── Source
    └── FooBar
        ├── FooBar.Build.cs
        ├── Private
        │   └── FooBarModule.cpp
        └── Public
            └── FooBarModule.h

在一个 Unreal 工程新建时会自动创建一个主游戏 module,这个 module 不会被别的 module 依赖,因此一般也不会区分 Public/Private 目录。

每个 module 至少要包含一个 module 对应的 .h 和 .cpp 文件,用于实现对应 module,这里的文件名可以是任意的,但一般会选用 YourModuleName.h/cpp 或 YourModuleNameModule.h/cpp 这样的形式,例如这里的 FooBarModule.h 和 FooBarModule.cpp。

其中 FooBarModule.h 中大致会有如下的内容:

代码语言:javascript复制
#include "Modules/ModuleManager.h"
class FFooBarModule : public IModuleInterface {
    // 可以通过 override 生命周期回调触发自定义逻辑,如:
    // virtual void StartupModule() override;
    // virtual void ShutdownModule() override;
    // ...
};

这里声明了一个 FFooBarModule 类继承了 IModuleInterface 类。前面提到 Core module 包含了处理 module 的代码,所以任何 module 的依赖中至少要包含它,这里 include 的 ModuleManager.h 文件在就是 Core module 中的一部分。

而在 FooBarModule.cpp 中,则需要至少包含一个类似这样的实现:

代码语言:javascript复制
#include "FooBarModule.h"
IMPLEMENT_MODULE(FFooBarModule, FooBar)

这里 IMPLEMENT_MODULE 宏的第一个参数是对应的类,第二个参数是 module 名。对于游戏 module 和主游戏 module,这里则改为使用 IMPLEMENT_GAME_MODULEIMPLEMENT_PRIMARY_GAME_MODULE

如果其他 module 想使用这个 module 类中的方法,可以使用 FModuleManager 获取对应 module 类对象的引用:

代码语言:javascript复制
FModuleManager::Get().LoadModuleChecked<FFooBarModule>(TEXT("FooBar")).SomeMethod();

暴露接口 #

类似于 DLL,module 对外暴露的接口都需要进行额外声明,否则默认情况下并不会暴露出去。例如下面是 NicknamedActor.h 中的代码:

代码语言:javascript复制
#pragma once
#include "GameFramework/Actor.h"
#include "CoreMinimal.h"
#include "NicknamedActor.generated.h"
UCLASS(Blueprintable)
class ANicknamedActor : public AActor {
    GENERATED_BODY()
  public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString Nickname;
    UFUNCTION(BlueprintCallable)
    void SayNickname();
};

它的声明和实现在目录中的位置如下:

代码语言:javascript复制
ProjectFolder
└── Source
    └── FooBar
        ├── FooBar.Build.cs
        ├── Private
        │   ├── NicknamedActor.cpp
        │   └── FooBarModule.cpp
        └── Public
            ├── NicknamedActor.h
            └── FooBarModule.h

此时,如果其他 module 添加了 FooBar 的依赖,那么它能在代码中访问到 ANicknamedActor 。但仍然无法链接到 ANicknamedActor 中的方法。为了将其中的方法暴露给其他 module,需要手动声明。

如果仅仅是想将类型信息暴露出去,那么可以在 UCLASS 声明中加 MinimalAPI

代码语言:javascript复制
UCLASS(Blueprintable, MinimalAPI)
class ANicknamedActor : public AActor { /* ... */ };

这样一来,其他 module 可以:将别的类型转换到该类型;Spawn 该类型的对象;继承该类型;使用该类型中的内联函数。

在此基础上,如果想额外暴露一些方法出去,则需要在对应方法声明前添加 API 声明,形式为 YOURMODULENAME_API ,这个宏是 Unreal 自动为每个 module 生成的。例如这里就是 FOOBAR_API

代码语言:javascript复制
UCLASS(Blueprintable, MinimalAPI)
class ANicknamedActor : public AActor {
    // ...
    UFUNCTION(BlueprintCallable)
    FOOBAR_API void SayNickname();
};

当然,一种更为常见的情况是我们直接将这整个类型的信息都暴露出去,此时可以将 API 声明放到类型名前面,例如:

代码语言:javascript复制
UCLASS(Blueprintable)
class FOOBAR_API ANicknamedActor : public AActor {
    // ...
};

添加 Module 描述 #

每一个 module 都需要在工程的 meta 文件中进行声明,对于游戏工程而言是在 .uproject 文件中,对于插件而言是在 .uplugin 文件中。这两个 meta 文件都是 JSON 格式的,module 相关的描述在 Module 字段下:

代码语言:javascript复制
{
    // Other meta data...
    "Module": [
        {
            "Name": "FooBar",
            "Type": "Runtime",
            "LoadingPhase": "Default"
        },
        // Other modules ...
    ]
}

显然,这里的 Name 字段对应 module 的名字。这里的 Type 字段对应 module 被加载的环境,最常用的就是 RuntimeEditor 分别对应运行时(包括编辑器)和仅编辑器,完整列表可以参考 EHostType::Type 的内容。而 LoadingPhase 则对应于 module 的加载阶段,最常见的是 Default ,如果是写一个包含 shader 的 module,则会用到 PostConfigInit 这个阶段,因为 Unreal 需要在引擎初始化前加载 shader,完整列表可以参考 ELoadingPhase::Type。

简化 Module 操作 #

新建一个 module 的过程其实相当麻烦,一般为了避免出错,会复制一个现成的 module,然后将内容删除再将名字改掉,不仅麻烦还容易出错,这里实现了一个简单的小工具,可以简化这个过程。项目地址在 urem,这是个命令行工具,可以用 go install 命令安装。

先将 urem.exe 所在的位置加入 PATH ,然后就可以执行如下命令新建一个 module 了:

代码语言:javascript复制
# 新建一个属于工程的 module
urem.exe new mod ModuleName YourPorjectRoot/Source
# 新建一个属于插件的 module
urem.exe new mod ModuleName YourPorjectRoot/Plugins/YourPlugin/Source

这个过程会在 Source 目录下新建一个以 ModuleName 为名的目录,其中包含 ModuleName.Build.cs 文件,以及对应的 ModuleNameModule.h/cpp 文件。另外,为了方便日志打印,还会新建 Log.h/cpp 文件,包含 log category 的声明。在新建文件后,会自动刷新 .sln 工程。

除了新建 module 之外,这个工具还集成了一堆便利的小功能,比如刷新 .sln 工程,刷新 clangd 工程,新增 gitignore 模板、clang-format 模板等。使用 urem.exe --help 查看帮助。

参考资料 #

  • UE4 Modules - Ari Arnbjörnsson

0 人点赞