【ue4】【架构】模块机制

前言

UE4 从源代码的组织上来看是由__模块 (Module) __组成的

从功能上讲, UE4 对 编辑器(Editor) 和 游戏性(Runtime) 都提供了支持

对于编辑器来说, UE4 提供了丰富的编辑界面,

角色编辑器 (Persona) 动画编辑器 (AnimationEditor) 特效编辑器 (Cascade和Niagara) 材质编辑器 脚本编辑器 (蓝图) 等等

同时还可以自定义插件, 以及使用开放的蓝图节点工具和UI工具 Slate 来定制自己的编辑器界面

对于游戏性来说, UE4提供了比较完整的游戏性类, 以实现对整个游戏生命周期的控制

这些游戏性类从宏观上看也是遵循 MVC 模式的

工程目录

Build -- 编译过程中产生的 可执行文件 及其他

Config -- 配置文件 -- 控制引擎行为 -- 游戏目录下的 Config 配置会覆盖引擎的

Content -- 资源文件 模型 地图 动画 蓝图 -- .uasset (地图为 .umap)

Source -- 源码

Saved -- 自动保存的内容 -- 配置.ini 日志(崩溃日志)

Plugins -- 插件 -- 与源代码内容类似, 但可能包含 Content

Shaders -- 着色器

Module -- 模块

每一个 UE 项目, 包括 UE 引擎项目本身,都是由一个或多个__模块(Module)__组成

且至少有一个主模块, 其他模块都是扩展模块

在编译后, 每一个模块都是一个单独的 dll (也有lib), 供运行时可选择性地相互链接

【Tips】 每一个插件也是由一个或多个模块组成

模块的结构

每一个模块大概包含以下文件

Classes -- 包含所有游戏性类的 .h 文件

Private -- 包含所有 .cpp 文件 -- 包括游戏性类的和模块本身的

Public -- 包含模块本身的头文件

ModuleName.Build.cs -- 包含模块的编译信息

【Tips】 Classes 文件夹不是必须的 【Tips】 每个模块至少有一个 .h 和一个 .cpp

模块的依赖

模块的 Build.cs 文件里可以指定其所依赖的其他模块 -- 定义 ModuleRules

每一个模块也可以在其他模块的 Build.cs 里被其他模块所依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// CPTPS.Build.cs
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class CPTPS : ModuleRules
{
public CPTPS(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PublicDependencyModuleNames.AddRange(
new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"HeadMountedDisplay"
});

PrivateDependencyModuleNames.AddRange( new string[] { /* ... */ });

PrivateIncludePaths.AddRange( new string[] { /* ... */ });

PrivateIncludePathModuleNames.AddRange( new string[] { /* ... */ });

}
}

游戏项目中的 .Target.cs (一般有两个, 可自己新增) 声明了游戏和编辑器下需要如何将不同模块链接成目标代码

它定义了 TargetRules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CPTPS.Target.cs
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;
using System.Collections.Generic;

public class CPTPSTarget : TargetRules
{
public CPTPSTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
ExtraModuleNames.Add("CPTPS");
}
}

游戏项目中的模块必须在 .uproject 文件里统一定义才能被 UE 识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// CPTPS.uproject

{
"FileVersion": 3,
"EngineAssociation": "{EBDB0310-437E-3F3E-D665-17822D8A4B5C}",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "CPTPS",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "BlueprintGraph",
"Type": "Runtime",
"LoadingPhase": "Default"

}
]
}

【Tips】 插件中的模块也要在插件的 .uplugin 文件里统一定义 【Tips】 模块大致分为 运行时模块 (Runtime) 和 编辑器模块 (Editor)

模块的建立

当新建一个模块时,我们需要手动创建 ModuleName.h ModuleName.cpp ModuleName.Build.cs 三个文件

并按照上面的规则建立好模块间的依赖关系

最重要的是, 每一个模块必须有一个实现了 IModuleInterface 的类

并将其作为暴露给外部使用的接口(指针)

可以看出, 这个接口是用来操作模块本身整体的, 即加载, 初始化, 卸载及一些标记状态量等

除此之外,为了满足每个模块的dll (或lib) 能被顺利地找到, 即把这个接口暴露出来给其他地方使用, 需要声明一些样板宏, 即 IMPLEMENT_MODULE

IMPLEMENT_MODULE(FDefaultModuleImpl, ModuleName) // 一般的模块声明 FDefaultModuleImpl为空实现

IMPLEMENT_MODULE(FModuleName, ModuleName) // 有自己的初始化逻辑的模块 ModuleNameModule为子类

IMPLEMENT_GAME_MODULE(FModuleName, ModuleName) // 包含游戏逻辑的模块

IMPLEMENT_PRIMARY_GAME_MODULE(FModuleName, ModuleName, "ModuleName") // 当前项目的主模块

【Tips】 必须声明一个主模块 【Tips】 一般用 FModuleName 为类名继承 IModuleInterface, 而 ModuleName 则为实际模块名

模块的引用

在其他地方引用某个模块,只需要调用 FModuleManager 类的 LoadModule() 方法即可

1
FLevelEditorModule& LEModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");

【Tips】 LoadModule() 不论是动态链接还是静态链接, 都会通过模块名称正确地找到模块的接口。 【Tips】 LoadModuleChecked() 会检查返回的模块接口是否合法