第十九章 模块与构建系统:从 UBT 编译流程到模块加载的完整链路

7344 字
37 分钟
第十九章 模块与构建系统:从 UBT 编译流程到模块加载的完整链路

第十九章 模块与构建系统:从 UBT 编译流程到模块加载的完整链路#

一句话理解:UE 的构建系统是四层架构——Target 层(.Target.cs)定义”构建什么产物”(Game / Editor / Server / Client / Program),Build 层(.Build.cs)定义”每个模块编译时需要什么”(依赖、头文件路径、预编译宏),Module 层(IModuleInterface)定义”运行时怎么初始化与销毁”(StartupModule / ShutdownModule),Directory 层(Public/Private)定义”哪些 API 暴露给外部模块”(Public = 合约,Private = 封装)。理解这四层的分工和 UBT 的编译管线,是区分”会写 UE 代码”和”能在团队中交付工业化产品”的关键分水岭。


19.1 概念直觉 —— 构建系统的四层架构#

flowchart TD subgraph Target["Target 层 —— 构建什么产物"] T1[".Target.cs\nGameTarget / EditorTarget\nServerTarget / ClientTarget"] T2["TargetType\nGame / Editor / Server / Client / Program"] T3["BuildSettingsVersion\n控制 API 弃用行为"] end subgraph Build["Build 层 —— 每个模块编译时需要什么"] B1[".Build.cs\n继承 ModuleRules"] B2["依赖类型\nPublic / Private / DynamicallyLoaded"] B3["编译选项\nPCHUsage / bUseUnity / UndefinedIdentifierWarningLevel"] end subgraph Module["Module 层 —— 运行时初始化与销毁"] M1["IModuleInterface\nStartupModule / ShutdownModule"] M2["FModuleManager\nLoadModule / GetModulePtr / LoadModuleChecked"] M3["IMPLEMENT_MODULE\nIMPLEMENT_PRIMARY_GAME_MODULE"] end subgraph Dir["Directory 层 —— API 可见性边界"] D1["Public/ 目录\n对外暴露的头文件——模块的 API 合约"] D2["Private/ 目录\n内部实现——外部模块无权 include"] end Target --> Build Build --> Module Module --> Dir style Target fill:#d00000,stroke:#e85d04,color:white style Build fill:#e85d04,stroke:#f48c06,color:white style Module fill:#2d6a4f,stroke:#40916c,color:white style Dir fill:#2d6a4f,stroke:#40916c,color:white
// ===== 构建四层架构速记 =====
//
// Target 层(.Target.cs):
// 定义构建产物——Game(客户端+独立服务器)/ Editor(编辑器)/
// Server(专用服务器)/ Client(纯客户端)/ Program(独立工具)。
// 控制:DefaultBuildSettings、bUseLoggingInShipping、bCompileAgainstEngine 等。
//
// Build 层(.Build.cs):
// 定义模块编译依赖——PublicDependencyModuleNames(暴露给下游)、
// PrivateDependencyModuleNames(仅内部使用)、
// DynamicallyLoadedModuleNames(延迟加载、不链接)。
// 控制:PCHUsage(预编译头策略)、bUseUnity(Unity Build 加速)。
//
// Module 层(IModuleInterface):
// 运行时模块对象——StartupModule(初始化)和 ShutdownModule(清理)。
// 由 FModuleManager 统一管理加载/卸载/查询。
//
// Directory 层(Public / Private):
// Public/ 是模块的"API 层"——外部模块可以 include。
// Private/ 是模块的"实现层"——只在本模块内可见。
// 这是物理目录约定,由 UBT 强制执行。

19.2 .uproject 与 .uplugin —— 项目与插件描述文件#

19.2.1 .uproject:项目身份文件#

// ===== .uproject:UE 项目的 JSON 描述文件 =====
//
// 位置:项目根目录 / MyProject.uproject
// 作用:告诉引擎——这个项目叫什么、依赖哪些模块和插件、关联哪个引擎版本
// 类型:纯 JSON——不需要编译,引擎启动时解析
// ---------- 完整的 .uproject 示例 ----------
/*
{
"FileVersion": 3,
"EngineAssociation": "5.4", // 引擎版本——或 "{GUID}" 使用自定义引擎构建
"Category": "Game", // 项目类别
"Description": "My UE5 Game",
// ★ 模块列表——项目的 C++ 模块(不含插件)
"Modules": [
{
"Name": "MyProject", // 模块名——必须与 .Build.cs / 目录名一致
"Type": "Runtime", // 模块类型(Runtime / Editor / Developer)
"LoadingPhase": "Default" // 加载时机(Default / PostConfigInit / PostEngineInit)
},
{
"Name": "MyProjectEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
],
// ★ 插件列表——启用的插件
"Plugins": [
{
"Name": "EnhancedInput",
"Enabled": true
},
{
"Name": "Niagara",
"Enabled": true
},
{
"Name": "ModelingToolsEditorMode",
"Enabled": true,
"SupportedTargetPlatforms": ["Win64"] // 可选:限制目标平台
}
]
}
*/
// ===== .uproject 关键字段 =====
//
// FileVersion:目前固定为 3
//
// EngineAssociation:
// "5.4" → 使用 Launcher 安装的 5.4 版本
// "{1234-5678}" → 使用注册表中对应的自定义源码构建
// "" → 空白——必须由外部指定(如命令行 -EngineDir=)
//
// Modules[].Type:
// Runtime → 随游戏包发布(最常用)
// RuntimeNoCommandlet → 运行时加载但不随 Commandlet 加载
// UncookedOnly → 仅未烘焙(Uncooked)状态下可用——打包烘焙时自动剥离(最常用的开发期类型)
// DeveloperTool → 命令行工具或非编辑器的独立开发期工具
// Editor → 仅编辑器加载
// EditorNoCommandlet → 编辑器中加载但 Commandlet 不加载
//
// Modules[].LoadingPhase:
// Default → 引擎初始化阶段加载(最常用)
// PostConfigInit → 配置系统初始化后加载
// PostSplashScreen → 启动画面显示后加载
// PreEarlyLoadingScreen → 早期加载画面之前
// PostEngineInit → 引擎完全初始化后加载(适用于依赖引擎服务的模块)

19.2.2 .uplugin:插件描述文件#

Plugins/MyPlugin/MyPlugin.uplugin
// ===== .uplugin:UE 插件的 JSON 描述文件 =====
//
// 作用:一个插件可以包含多个模块(Runtime + Editor + Developer)
// ---------- 完整的 .uplugin 示例 ----------
/*
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "My Inventory System",
"Description": "A reusable inventory system with UI and networking support",
"Category": "Gameplay",
"CreatedBy": "My Studio",
"CanContainContent": true, // 是否可以包含 .uasset 内容
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false, // 是否安装在 Engine/Plugins(否则是项目插件)
// ★ 一个插件可以有多个模块
"Modules": [
{
"Name": "InventorySystem", // Runtime 模块——随游戏发布
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "InventorySystemEditor", // Editor 模块——编辑器中扩展
"Type": "Editor",
"LoadingPhase": "Default"
}
],
// 插件依赖——本插件依赖的其他插件
"Plugins": [
{
"Name": "EnhancedInput",
"Enabled": true
}
]
}
*/
// ===== .uplugin vs .uproject 模块的区别 =====
//
// 相同:都使用相同的 Module 描述结构(Name / Type / LoadingPhase)
//
// 不同:
// .uproject 的模块属于"项目自身"——只有这个项目能用
// .uplugin 的模块属于"可复用资产"——任何项目引入插件即可用
//
// 经验法则:
// - 只在当前项目用的代码 → .uproject Modules
// - 多个项目复用的代码 → 封装为 .uplugin
// - 编辑器扩展 → 单独的 Editor 类型模块(放在 .uplugin 内或 .uproject 内)

19.3 .Build.cs —— 模块构建规则#

19.3.1 ModuleRules 核心属性#

Source/MyModule/MyModule.Build.cs
// ===== .Build.cs:每个模块必须有一个 C# 文件定义编译规则 =====
//
// 继承:ModuleRules(UBT 解析并执行其构造函数)
// 语言:C# —— 因为在编译时由 UBT(一个 C# 程序)加载并执行
// ---------- 标准 .Build.cs 完整示例 ----------
/*
using UnrealBuildTool;
public class MyGameModule : ModuleRules
{
public MyGameModule(ReadOnlyTargetRules Target) : base(Target)
{
// ① 预编译头策略(UE5 推荐 ExplicitOrSharedPCHs)
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// 可选值:
// NoPCHs → 完全禁用预编译头(编译慢,适合极小模块)
// NoSharedPCHs → 每个 .cpp 独立预编译(编译慢,隔离性好)
// UseSharedPCHs → 使用引擎共享 PCH(UE4 遗留)
// UseExplicitOrSharedPCHs → UE5 推荐——显式 include 或使用共享 PCH
// ② 公共依赖——暴露给下游模块
// "下游模块如果 include 了本模块的 Public 头文件,
// 而这些头文件又 include 了 Core / CoreUObject / Engine 的头文件,
// 那么下游模块也需要能访问这些依赖"
PublicDependencyModuleNames.AddRange(new string[] {
"Core", // FString / TArray / FName / 基础容器
"CoreUObject", // UObject / UClass / UProperty —— 对象模型
"Engine", // AActor / UWorld / UGameplayStatics
"InputCore" // FKey / FInputAction 等输入类型
});
// ③ 私有依赖——仅本模块内部使用
// "下游模块不需要知道这些——你实现细节变了不影响下游"
PrivateDependencyModuleNames.AddRange(new string[] {
"Slate", // UI 框架
"SlateCore", // UI 基础
"EnhancedInput", // Enhanced Input 系统
"UMG" // Widget 系统
});
// ④ 动态加载依赖——运行时按需加载,不参与静态链接
// "这个模块我只在特定条件下才需要——编译时不链接,运行时手动 LoadModule"
DynamicallyLoadedModuleNames.AddRange(new string[] {
"OnlineSubsystemNull" // 离线模式在线子系统
});
// ⑤ 公开包含路径——外部模块可以 include 的额外目录
PublicIncludePaths.AddRange(new string[] {
// 通常不需要——Public/ 目录默认已是公开路径
});
// ⑥ 私有包含路径——仅本模块内部 include 的目录
PrivateIncludePaths.AddRange(new string[] {
// 通常不需要——Private/ 目录默认已是私有路径
});
// ⑦ 预编译宏——在编译本模块时定义
PublicDefinitions.Add("MY_MODULE_VERSION=1"); // 公开宏——下游模块也可见
PrivateDefinitions.Add("MY_INTERNAL_FLAG=1"); // 私有宏——仅本模块可见
// ⑧ 其他常用选项
bUseUnity = true; // Unity Build——多个 .cpp 合并编译(加速)
bUseRTTI = false; // 禁用 RTTI——UE 默认禁用
bEnableExceptions = false; // 禁用 C++ 异常——UE 默认禁用
// ★ UndefinedIdentifierWarningLevel 属于 .Target.cs 的 TargetRules 全局控制权限
// 模块层级的 .Build.cs 不具备此属性——不要在此设置
}
}
*/

19.3.2 依赖传播规则#

// ===== Public vs Private 依赖的传播机制 =====
//
// 这是面试中考察"模块化理解"的核心考点。
//
// 场景:模块 A → 模块 B → 模块 C(B 依赖 C)
//
// PublicDependency(B 公开依赖 C):
// B 的 Public 头文件中 include 了 C 的头文件
// → A 需要能访问 C 的类型才能编译通过
// → B 必须把 C 放在 PublicDependencyModuleNames 中
// → UBT 会自动将 C 的 Public 路径和链接传递给 A
//
// PrivateDependency(B 私有依赖 C):
// B 只在 Private/ 目录中使用 C
// → A 不需要知道 C 的存在
// → B 把 C 放在 PrivateDependencyModuleNames 中
// → UBT 不会将 C 暴露给 A——A 编译时看不到 C 的头文件
//
// 经验法则(面试标准答案):
// "如果你的 Public 头文件 include 了某个模块的头文件 → 公开依赖它。
// 如果某个模块只在你 Private 实现中用 → 私有依赖它。
// 如果某个模块运行时可选加载 → DynamicallyLoaded。"
// ---------- 依赖传递示意图 ----------
//
// A.Build.cs:
// PublicDependencyModuleNames.Add("B");
//
// B.Build.cs:
// PublicDependencyModuleNames.Add("C"); // ← 公开依赖
// PrivateDependencyModuleNames.Add("D"); // ← 私有依赖
//
// 编译 A 时:
// A 可以看到:B(公开依赖)、C(B 的公开依赖传递)
// A 看不到:D(B 的私有依赖——不透传)

19.4 .Target.cs —— 构建目标配置#

19.4.1 TargetRules 核心配置#

// ===== .Target.cs:定义"构建什么产物" =====
//
// 位置:Source/MyProject.Target.cs(Game) / Source/MyProjectEditor.Target.cs(Editor)
// 继承:TargetRules
// 每个 .Target.cs 对应一个输出——一个游戏项目通常有至少两个 Target
// ---------- Game Target(游戏客户端 + 独立服务器)----------
/*
using UnrealBuildTool;
using System.Collections.Generic;
public class MyProjectTarget : TargetRules
{
public MyProjectTarget(ReadOnlyTargetRules Target) : base(Target)
{
Type = TargetType.Game; // ★ 核心:定义产物类型
DefaultBuildSettings = BuildSettingsVersion.V4; // UE5 推荐 V4
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_4;
// ★ 列出这个 Target 包含的模块——通常是项目主模块
ExtraModuleNames.AddRange(new string[] {
"MyProject" // 主游戏模块
});
}
}
*/
// ---------- Editor Target(编辑器)----------
/*
public class MyProjectEditorTarget : TargetRules
{
public MyProjectEditorTarget(ReadOnlyTargetRules Target) : base(Target)
{
Type = TargetType.Editor; // ★ 编辑器构建
DefaultBuildSettings = BuildSettingsVersion.V4;
ExtraModuleNames.AddRange(new string[] {
"MyProject", // 主游戏模块
"MyProjectEditor" // 编辑器专用扩展模块
});
}
}
*/
// ---------- Server Target(专用服务器——不渲染、无客户端逻辑)----------
/*
public class MyProjectServerTarget : TargetRules
{
public MyProjectServerTarget(ReadOnlyTargetRules Target) : base(Target)
{
Type = TargetType.Server;
DefaultBuildSettings = BuildSettingsVersion.V4;
bUsesSteam = true; // 使用 Steam 网络
ExtraModuleNames.AddRange(new string[] {
"MyProject"
});
}
}
*/
// ---------- Client Target(纯客户端——不含服务器代码)----------
/*
public class MyProjectClientTarget : TargetRules
{
public MyProjectClientTarget(ReadOnlyTargetRules Target) : base(Target)
{
Type = TargetType.Client;
DefaultBuildSettings = BuildSettingsVersion.V4;
ExtraModuleNames.AddRange(new string[] {
"MyProject"
});
}
}
*/

19.4.2 TargetType 决策矩阵#

// ===== TargetType:五种构建产物 =====
//
// ┌──────────────┬──────────────────────────────────┬──────────────────────┐
// │ TargetType │ 用途 │ 包含内容 │
// ├──────────────┼──────────────────────────────────┼──────────────────────┤
// │ Game │ 打包发布的游戏 + 独立服务器模式 │ 全部——渲染 + 服务器逻辑 │
// │ Editor │ 编辑器中开发 │ 全部 + 编辑器工具 │
// │ Server │ 专用服务器(Dedicated Server) │ 仅服务器逻辑——无渲染 │
// │ Client │ 纯客户端(配合专用服务器使用) │ 仅客户端——无服务器逻辑 │
// │ Program │ 独立命令行工具 │ 精简引擎——无渲染 │
// └──────────────┴──────────────────────────────────┴──────────────────────┘
//
// 面试常见追问:"Game vs Server 的区别?"
// 标准答案:
// Game = 同时包含客户端 + 服务器代码,可以作为 Listen Server。
// Server = 剥离了所有渲染和客户端逻辑——只保留网络权威代码。
// 专用服务器的二进制体积可以比 Game 小 40%~60%。
// 云部署用 Server Target:更小的磁盘占用、更少的内存开销、更少的攻击面。
// ===== DefaultBuildSettings 版本速查 =====
//
// BuildSettingsVersion.V2 → UE4 遗留行为
// BuildSettingsVersion.V3 → UE5.0~5.2 默认
// BuildSettingsVersion.V4 → UE5.3+ 推荐——包含更多弃用 API 警告和严格检查
//
// 经验法则:新项目一律使用 V4。

19.5 UBT 编译流程 —— 从源码到二进制#

19.5.1 UBT 编译全管线#

// ===== UBT(Unreal Build Tool):编译的总指挥 =====
//
// UBT 是一个 C# 程序(位于 Engine/Binaries/DotNET/UnrealBuildTool/)
// 每次你在 VS/Rider 中编译或运行 Build.bat 时,实际上是在调用 UBT。
//
// 编译全流程(6 步):
//
// ① 收集 Target:
// UBT 读取命令行指定的 Target 文件(如 MyProjectEditor.Target.cs)
// → 确定构建产物类型(Game / Editor / Server / Client / Program)
//
// ② 解析模块依赖图:
// UBT 递归读取所有 .Build.cs 文件
// → 构建完整的模块依赖有向无环图(DAG)
// → 检测循环依赖——报错退出
//
// ③ 调用 UHT(Unreal Header Tool):
// UHT 扫描所有头文件中的 UCLASS / USTRUCT / UFUNCTION / UPROPERTY 宏
// → 为每个带反射的类型生成 .generated.h 和 .gen.cpp
// → 这是 "UE C++ 平行宇宙" 的入口——见 Ch2
//
// ④ 平台适配:
// UBT 根据目标平台选择对应的工具链:
// Windows → Visual C++ (MSVC)
// Mac/iOS → Xcode / Apple Clang
// Linux → Clang
// Android → NDK(Clang)
// Console → 对应 SDK 编译器
//
// ⑤ 并行编译:
// UBT 将编译任务分发到多个工作线程
// → Unity Build:多个 .cpp 合并成一个大文件编译(减少重复解析头文件)
// → 增量编译:只重编译修改过的 .cpp 和依赖它的翻译单元
//
// ⑥ 链接:
// 链接器将所有 .obj 和 .lib 合并为最终的二进制文件
// Game → MyProject.exe(Windows)/ MyProject(Linux/Mac)
// Editor → UnrealEditor.dll(模块化——引擎是 DLL)
// Server → MyProjectServer.exe
// ---------- UBT 关键概念 ----------
//
// Unity Build(加速编译):
// 将 N 个 .cpp 合并成一个翻译单元 → 只解析一次公共头文件 → 显著加速
// 缺点:修改一个 .cpp → 整个 Unity Bundle 全部重编译
// 关闭:.Build.cs 中 bUseUnity = false
//
// Adaptive Unity Build(自适应联合编译):
// 只把"不常修改"的 .cpp 打包进 Unity Build
// "频繁修改"的 .cpp 单独编译——既享受加速,又不增加增量编译成本
// 这是 UE5 的默认行为——通常不需要手动干预
//
// PCH(Precompiled Header——预编译头):
// 将常用的稳定头文件(CoreMinimal.h 等)预编译为二进制缓存
// 每个 .cpp 编译时直接加载 PCH——跳过解析数十万行稳定代码
// UE5 推荐 PCHUsageMode.UseExplicitOrSharedPCHs

19.5.2 常见编译错误与排查#

// ===== 编译错误排查速查 =====
//
// ① "fatal error C1083: 无法打开包括文件: 'MyModule/MyClass.generated.h'"
// 原因:UHT 未生成 .generated.h——检查 UCLASS/USTRUCT 宏是否正确
// 解决:确保 #include "MyClass.generated.h" 是头文件的最后一个 include
//
// ② "unresolved external symbol"
// 原因:缺少模块依赖——你使用了某个模块的类型但未在 .Build.cs 中声明
// 解决:添加 PublicDependencyModuleNames 或 PrivateDependencyModuleNames
//
// ③ "module 'X' could not be found"
// 原因:模块未在 .uproject / .uplugin 的 Modules 列表中注册
// 解决:在 .uproject 或 .uplugin 中声明该模块
//
// ④ "circular dependency detected"
// 原因:两个模块互相依赖(A→B 且 B→A)
// 解决:重新设计模块边界——提取公共接口到第三个模块 C(A→C, B→C)
//
// ⑤ "PCH file not found"
// 原因:预编译头策略与项目配置不匹配
// 解决:检查 .Build.cs 中 PCHUsage 的值——新建模块用 UseExplicitOrSharedPCHs

19.6 Public / Private 目录规范#

19.6.1 模块的物理目录结构#

// ===== UE 模块的目录约定 =====
//
// 标准结构(以 MyModule 为例):
//
// Source/MyModule/
// ├── MyModule.Build.cs ← 编译规则(C#)
// ├── Public/ ← ★ 公开头文件——外部模块可以 include
// │ ├── MyModule.h ← 模块接口(IModuleInterface 实现)
// │ ├── MyPublicActor.h ← 对外暴露的类
// │ └── MyPublicTypes.h ← 对外暴露的类型定义(USTRUCT / UENUM)
// ├── Private/ ← ★ 私有实现——仅本模块可见
// │ ├── MyModule.cpp ← IMPLEMENT_MODULE 所在位置
// │ ├── MyPrivateActor.cpp ← 内部实现
// │ └── Internal/ ← 子目录——进一步组织私有代码
// │ └── DetailHelper.h
//
// UBT 强制执行:
// - Public/ 目录下的头文件 → 自动加入公开包含路径 → 下游模块可 include
// - Private/ 目录下的头文件 → 只有本模块可 include → 下游模块无法访问
// - 根目录(Source/MyModule/)下的文件 → 视同 Private(不推荐在此放头文件)
// ---------- 正确的跨模块 include 方式 ----------
//
// 模块 A 依赖模块 B(B 在模块 A 的 PublicDependencyModuleNames 中)
//
// A 的 Public 头文件中:
// #include "BModule/SomePublicClass.h" // ✓ 正确——B 的 Public 头文件
// // 不需要写完整路径——UBT 已将 B/Public 加入 A 的包含路径
//
// A 的 Private 实现中:
// #include "BModule/SomePublicClass.h" // ✓ 同 Public 规则
// // 你无法 include B 的 Private 头文件——编译器会报"找不到文件"

19.6.2 大型项目的模块拆分策略#

// ===== 什么时候应该创建新模块? =====
//
// 一个游戏项目的代码全部放在主模块中 = 灾难:
// - 编译时间随代码量线性增长——改一行等 60 秒
// - 命名空间污染——所有类型在一个模块中
// - 不能在运行时按需加载/卸载
//
// 模块拆分信号(满足任一即考虑拆分):
//
// ① 独立功能集 —— GAS 技能系统 / 背包系统 / 对话系统
// → 拆分为独立模块:MyGame_AbilitySystem / MyGame_Inventory / MyGame_Dialogue
//
// ② 不同加载时机 —— 一部分代码在 PostEngineInit 后才需要
// → 后置模块可以延迟初始化——加速引擎启动
//
// ③ 独立团队维护 —— 技能组和 UI 组各自维护自己的模块
// → 模块边界 = 团队边界——减少合并冲突
//
// ④ 可选功能 —— 某些功能在特定平台/版本中关闭
// → 独立模块可以在运行时按需加载 (FModuleManager::LoadModule)
//
// ⑤ 编辑器专用逻辑 —— 编辑器扩展不应该随游戏打包
// → 拆分为 Editor 类型模块——Shipping 包中不存在
// ---------- 模块拆分实战:MyGame 项目架构 ----------
/*
Source/
├── MyGame.Target.cs ← Game Target
├── MyGameEditor.Target.cs ← Editor Target
├── MyGame/ ← ★ 主模块(Runtime)
│ ├── MyGame.Build.cs
│ ├── Public/
│ │ └── MyGame.h ← IMPLEMENT_PRIMARY_GAME_MODULE
│ └── Private/
│ └── MyGame.cpp
├── MyGame_AbilitySystem/ ← ★ 能力系统模块(Runtime)
│ ├── MyGame_AbilitySystem.Build.cs
│ │ PublicDependency: MyGame_Core, GameplayAbilities
│ │ PrivateDependency: GameplayTags, GameplayTasks
│ ├── Public/
│ │ ├── MyGameAbilitySystem.h
│ │ ├── Abilities/ ← 公开的能力基类
│ │ └── Attributes/ ← 公开的属性集
│ └── Private/
│ ├── MyGameAbilitySystem.cpp
│ └── Abilities/ ← 具体能力实现
├── MyGame_Inventory/ ← ★ 背包系统模块(Runtime)
│ ├── MyGame_Inventory.Build.cs
│ │ PrivateDependency: MyGame_Core
│ ├── Public/
│ │ ├── MyGameInventory.h
│ │ └── InventoryItem.h
│ └── Private/
│ └── ...
├── MyGame_Core/ ← ★ 共享核心模块(Runtime)
│ ├── MyGame_Core.Build.cs
│ │ PublicDependency: Core, CoreUObject, Engine
│ ├── Public/
│ │ ├── MyGameCore.h
│ │ └── MyGameTypes.h ← 跨模块共享的类型定义
│ └── Private/
│ └── ...
└── MyGameEditor/ ← ★ 编辑器扩展模块(Editor)
├── MyGameEditor.Build.cs
│ PrivateDependency: MyGame_Core, UnrealEd
├── Public/
│ └── MyGameEditor.h
└── Private/
└── ...
*/

19.7 模块加载与卸载#

19.7.1 IModuleInterface 的生命周期#

// ===== IModuleInterface:每个模块的 C++ 入口 =====
//
// 每个模块必须有一个实现 IModuleInterface 的类
// 这个类是模块的"生命周期管理者"——引擎在加载/卸载模块时调用它
#include "Modules/ModuleManager.h"
// ---------- 标准模块实现 ----------
// 位置:Source/MyModule/Private/MyModule.cpp
#include "MyModule.h"
#include "Modules/ModuleManager.h"
// ★ 将 FMyModule 注册为模块 "MyModuleName" 的实现类
// 模块名必须与 .uproject / .uplugin 中声明的 Name 一致
IMPLEMENT_MODULE(FMyModule, MyModuleName)
void FMyModule::StartupModule()
{
// 模块加载时调用——引擎初始化阶段
// 典型操作:
// - 注册全局资产类型(Asset Type)
// - 注册控制台命令(IConsoleManager::RegisterConsoleCommand)
// - 注册细节面板自定义(FPropertyEditorModule::RegisterCustomClassLayout)
// - 初始化模块级单例
UE_LOG(LogTemp, Log, TEXT("MyModule: StartupModule"));
}
void FMyModule::ShutdownModule()
{
// 模块卸载时调用——引擎关闭或手动 UnloadModule
// 典型操作:
// - 注销控制台命令
// - 注销编辑器扩展
// - 销毁模块级单例
UE_LOG(LogTemp, Log, TEXT("MyModule: ShutdownModule"));
}

19.7.2 FModuleManager:模块管理器#

// ===== FModuleManager:运行时模块管理 =====
//
// FModuleManager 是全局的模块注册表和加载器
// 它在引擎启动时读取所有 .uproject / .uplugin 的 Modules 列表
// 按 LoadingPhase 和依赖图决定加载顺序
#include "Modules/ModuleManager.h"
class FMyModuleUser
{
public:
void DynamicModuleAccess()
{
// ---------- ① 检查模块是否已加载 ----------
if (FModuleManager::Get().IsModuleLoaded("MyModule"))
{
UE_LOG(LogTemp, Log, TEXT("MyModule 已加载"));
}
// ---------- ② 强制加载模块 ----------
// 如果模块尚未加载——触发加载(调用 StartupModule)
// 如果已加载——直接返回
// 如果模块不存在——触发断言(在 Development/Debug 构建中)
IModuleInterface* Module = FModuleManager::Get().LoadModuleChecked("MyModule");
// ---------- ③ 安全加载——返回 nullptr 而非崩溃 ----------
IModuleInterface* ModulePtr = FModuleManager::Get().LoadModule("MyModule");
if (ModulePtr)
{
// 模块存在并加载成功
}
// ---------- ④ 按接口获取模块指针 ----------
// 典型模式:你的模块实现了一个自定义接口
// 其他模块通过接口访问——不依赖具体类型
IMyModuleInterface* MyInterface =
FModuleManager::Get().LoadModuleChecked<IMyModuleInterface>("MyModule");
MyInterface->DoSomething();
}
};
// ===== 模块接口模式:对外暴露 API =====
// 位置:Source/MyModule/Public/IMyModuleInterface.h
#include "Modules/ModuleInterface.h"
class IMyModuleInterface : public IModuleInterface
{
public:
// 纯虚接口——供外部模块调用
virtual void DoSomething() = 0;
virtual int32 GetSomeValue() const = 0;
};
// 在 MyModule.cpp 中实现:
#include "IMyModuleInterface.h"
class FMyModule : public IMyModuleInterface
{
public:
virtual void StartupModule() override { /* ... */ }
virtual void ShutdownModule() override { /* ... */ }
// 实现自定义接口
virtual void DoSomething() override
{
UE_LOG(LogTemp, Log, TEXT("Module API called"));
}
virtual int32 GetSomeValue() const override { return 42; }
};
// 注意:IMPLEMENT_MODULE 不直接支持接口类型检查
// 外部模块通过 GetModulePtr 获取指针后 static_cast 或使用 LoadModuleChecked

19.7.3 模块加载顺序与 LoadingPhase#

// ===== LoadingPhase:控制模块的初始化时机 =====
//
// 加载顺序(由早到晚):
//
// EarliestPossible → 尽可能早——引擎最基础的设施
// PostConfigInit → 配置系统初始化后(可读取 .ini 配置)
// PostSplashScreen → 启动画面显示后(UI 模块可以开始初始化)
// PreEarlyLoadingScreen
// PreLoadingScreen → 加载画面之前
// PreDefault → Default 之前——确保依赖链上前置模块先初始化
// Default → 默认加载阶段(80% 的模块用这个)
// PostDefault → Default 之后
// PostEngineInit → 引擎完全初始化后(所有引擎服务已就绪)
//
// 经验法则:
// - Runtime 模块 99% 使用 Default
// - 依赖 GameInstance 的模块 → PostEngineInit
// - UI 模块在编辑器启动时就需要 → PostSplashScreen 或 Default
// - 控制台命令注册 → PostConfigInit(尽早可用)

19.8 常见陷阱与面试深度追问#

19.8.1 构建系统 TOP 6 陷阱#

// ===== 陷阱 #1:Public 头文件中 include 了未声明为 PublicDependency 的模块 =====
// ✗ 模块 A 的 Public 头文件中 #include "SomeModule/SomeClass.h"
// 但 SomeModule 只写在 PrivateDependencyModuleNames 中
// → 下游模块编译时报 "fatal error: 无法打开包括文件"
// ✓ 规则:任何在 Public 头文件中被 include 的模块 → 必须放在 PublicDependencyModuleNames
// ===== 陷阱 #2:循环模块依赖 =====
// ✗ 模块 A 的 .Build.cs:PublicDependencyModuleNames.Add("B")
// 模块 B 的 .Build.cs:PublicDependencyModuleNames.Add("A")
// → UBT 直接报错——"circular dependency detected"
// ✓ 创建第三个模块 C:
// A 和 B 都依赖 C(A→C, B→C)
// 把 A 和 B 共享的类型/接口下沉到 C
// ===== 陷阱 #3:模块未在 .uproject 中注册 =====
// ✗ 创建了新模块 MyNewModule(有 .Build.cs + IMPLEMENT_MODULE)
// 但忘记在 .uproject 的 Modules 数组中添加
// → UBT 不编译这个模块——运行时 FModuleManager::LoadModule 返回 nullptr
// ✓ 在 .uproject 中添加:{ "Name": "MyNewModule", "Type": "Runtime", "LoadingPhase": "Default" }
// ===== 陷阱 #4:Public/Private 目录不存在 =====
// ✗ 创建了模块但未创建 Public/ 和 Private/ 目录
// → UBT 能编译,但下游模块 include 头文件时需要写完整相对路径
// → 破坏了 UE 的模块化约定——其他开发者找不到你的 API
// ✓ 始终创建 Public/(对外 API)和 Private/(内部实现)目录
// ===== 陷阱 #5:滥用 DynamicallyLoadedModuleNames =====
// ✗ 把所有"可能用到"的模块都放 DynamicallyLoaded → 运行时到处 LoadModule
// → 启动时模块未加载 → 第一次访问时卡顿(加载 DLL + StartupModule)
// → 模块可能被多次意外加载 —— 线程安全问题
// ✓ 只在真正"可选"且"不频繁使用"的模块上用 Dynamic 加载
// 大多数依赖直接用 PublicDependency 或 PrivateDependency
// ===== 陷阱 #6:Shipping 包中包含了 Editor 模块 =====
// ✗ 打包 Shipping 时 Editor 类型模块仍然被编译和链接
// → 包体积膨胀 + 不必要的代码暴露 + 潜在的编辑器功能在客户端可用(作弊风险)
// ✓ Editor 模块的 Type 必须设为 "Editor"——UBT 自动排除
// 如果 Editor 逻辑混在 Runtime 模块中 → 用 #if WITH_EDITOR 条件编译隔离

19.8.2 面试速记三连#

Q: ".Build.cs 和 .Target.cs 的区别是什么?"
A: .Target.cs 定义"构建什么产物"——一个 .Target.cs 对应一个可执行文件或 DLL
(Game / Editor / Server / Client / Program)。它描述的是整个构建产物的属性。
.Build.cs 定义"一个模块怎么编译"——每个模块一个 .Build.cs,描述依赖关系、
预编译头策略、编译宏等。关系:一个 Target 包含多个 Module,
Module 通过 .Build.cs 声明"我需要哪些其他模块的头文件和链接库"。
Q: "PublicDependency 和 PrivateDependency 的区别?什么时候用哪个?"
A: PublicDependency:你的 Public 头文件中 include 了该模块的头文件
→ 下游模块也需要能访问该模块才能编译通过 → 必须声明为公开依赖。
PrivateDependency:该模块只在你的 Private/ 实现中使用
→ 下游模块不需要知道 → 声明为私有依赖即可。
判断标准:运行"下游模块能否只包含你的 Public 头文件就编译通过?"
能 → 你的 Public 依赖配置正确。不能 → 缺少 PublicDependency。
Q: "UE 编译为什么这么慢?怎么加速?"
A: UE 编译慢的核心原因:① 巨型头文件链——CoreMinimal.h 展开后数十万行;
② 模板——TArray/TMap 等大量模板导致翻译单元膨胀;③ 宏——UHT 宏展开 +
反射代码生成。加速手段:① Unity Build(多个 .cpp 合并编译)——减少重复解析;
② PCH(预编译头)——稳定头文件预编译;③ Incredibuild/FastBuild 分布式编译;
④ 合理模块拆分——只重编译修改到的模块;
⑤ 关闭不必要的模块依赖——Private 依赖不透传减少包含路径;
⑥ LiveCoding(Ctrl+Alt+F11)——运行时热重载 .cpp 修改,无需重启编辑器。

19.9 30 秒速答#

面试被问:“UE 的模块系统是怎么工作的?从创建到加载的完整链路是什么?”

四层架构——Target 层.Target.csTargetType.Game/Editor/Server 定义构建产物)、Build 层.Build.csPublicDependencyModuleNames / PrivateDependencyModuleNames 定义编译依赖)、Module 层IModuleInterface::StartupModule / ShutdownModuleIMPLEMENT_MODULE 注册 → FModuleManager::LoadModuleChecked 运行时加载)、Directory 层Public/ = API 合约对外暴露,Private/ = 封装仅本模块可见)。完整链路:.uproject 声明模块列表 → UBT 解析 .Build.cs 构建依赖图 → UHT 生成反射代码 → 编译器生成 .dll / .exe → 运行时 FModuleManagerLoadingPhase 依次加载 → StartupModule 执行初始化。

面试追问:“一个新模块怎么加到项目中?最少需要创建哪些文件?”

最少 4 个文件:① Source/MyModule/MyModule.Build.cs(编译规则 + 依赖声明);② Source/MyModule/Public/MyModule.h(模块头文件——IModuleInterface 子类声明);③ Source/MyModule/Private/MyModule.cppIMPLEMENT_MODULE + StartupModule / ShutdownModule 实现);④ 在 .uproject"Modules" 数组中添加条目 {"Name": "MyModule", "Type": "Runtime", "LoadingPhase": "Default"}。如果模块需要依赖其他模块——在 PublicDependencyModuleNamesPrivateDependencyModuleNames 中声明。

面试追问:“Unity Build 是什么?有什么优缺点?”

Unity Build 将多个 .cpp 合并成一个大的翻译单元一起编译——避免每个 .cpp 单独解析一遍公共头文件(CoreMinimal.h 展开 几十万行)。优点:全量编译速度提升 24 倍。缺点:① 修改一个 .cpp → 整个 Unity Bundle 全部重编译(增量编译变慢);② 匿名命名空间冲突——两个 .cpp 中同名的匿名函数/变量会冲突;③ 静态函数/全局变量冲突。UE5 的 Adaptive Unity Build 折中——只把不常修改的 .cpp 打包,频繁修改的单独编译。

面试追问:“循环模块依赖怎么解决?”

提取公共接口到第三个模块——A 和 B 互相依赖 → 创建 C,把 A 和 B 共享的类型/接口下沉到 C,A→C、B→C。如果 A 和 B 需在运行时互相通信 → 使用接口模式(IModuleInterface 虚函数)或 Observer 模式——模块 A 调用 C 的接口注册、模块 B 调用 C 的接口通知——通过 C 解耦。

面试追问:“怎么在运行时按需加载一个模块?”

使用 FModuleManager::Get().LoadModule("ModuleName") 安全加载(返回 nullptr 不崩溃)或 LoadModuleChecked("ModuleName") 强制加载(模块不存在时触发断言)。注意:模块必须在 .uproject / .uplugin 中注册——FModuleManager 只知道已注册的模块。动态加载的模块必须在 .Build.csDynamicallyLoadedModuleNames 中声明——告诉 UBT 不要静态链接该模块。


19.10 本章自查清单#

  • 能画出 UE 构建系统的四层架构(Target → Build → Module → Directory)
  • 能区分 .Target.cs(构建产物)和 .Build.cs(编译规则)的职责
  • 理解 TargetType 的五种类型(Game / Editor / Server / Client / Program)和各自用途
  • 能解释 PublicDependency / PrivateDependency / DynamicallyLoaded 的区别和传播规则
  • 能写出标准的 .uproject 和 .uplugin JSON 结构
  • 理解 UBT 编译全流程(收集 Target → 解析依赖图 → UHT → 平台适配 → 并行编译 → 链接)
  • 知道 Unity Build 和 PCH 的加速原理和各自的取舍
  • 能写出 IMPLEMENT_MODULE 的标准用法和 StartupModule / ShutdownModule 的典型操作
  • 理解 FModuleManager 的 LoadModule / LoadModuleChecked / IsModuleLoaded 的使用场景
  • 知道 Public/ 和 Private/ 目录的物理约定和 UBT 强制执行机制
  • 掌握循环模块依赖的解决方案(提取公共接口到第三个模块)
  • 能说出至少 5 个编译错误的排查方法

📚 第四部第一章完结。 构建系统是工程实践的基石——Target 定义产物、Build 定义依赖、Module 定义生命周期、Directory 定义 API 边界。掌握这四层,你就能在大型项目中设计模块架构、排查编译错误、加速构建管道。接下来进入 Ch20:Epic 编码规范与最佳实践——命名前缀体系、check/ensure/verify 错误处理、Cast<T> 类型安全转换。

💡 前置依赖提醒

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

第十九章 模块与构建系统:从 UBT 编译流程到模块加载的完整链路
https://firefly-7a0.pages.dev/posts/ue_cpp/19_build_system/
作者
lonelystar
发布于
2026-06-08
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
第十六章 动画系统:从状态机到 Motion Warping 的完整链路
UE C++深入笔记 **UE C++ 系列 · 第十六章。** 从 AnimBlueprint 与 AnimInstance 的 C++ 交互体系到状态机(StateMachine)与混合空间(BlendSpace)的动画选择逻辑,从 IK 三剑客(TwoBone/CCD/FABRIK)到 Animation Notify/NotifyState 的事件系统,从 Montage 与 Slot 的动画组合机制到 Root Motion 驱动移动的网络同步,从 UE5 Motion Warping 自适应动画到完整角色动画管线实战——覆盖 UE 动画系统的全部面试与工业级核心。
2
第十八章 音频系统:从 MetaSounds 到空间音频的完整链路
UE C++深入笔记 **UE C++ 系列 · 第十八章。** 从 UE5 音频系统的四层架构(AudioComponent/SoundBase/Submix/Spatialization)到 AudioComponent 的 C++ 完整控制(Play/Stop/FadeIn/FadeOut/Parameter),从新一代 MetaSounds 可编程音频源与 SoundCue 节点编辑器的对比到 Submix 与 AudioBus 的 DSP 路由管线,从 Attenuation 距离衰减与 HRTF 空间化到脚步声/枪声/环境音/UI 音效的完整工业级音频管线——覆盖 UE 音频系统的全部面试与工业级核心。
3
第二十一章 性能分析与优化:从 Insights 追踪到迭代优化的完整链路
UE C++深入笔记 **UE C++ 系列 · 第二十一章。** 从 UE 性能优化的四层金字塔(追踪→定位→修复→验证)到 Unreal Insights 的 CPU/GPU/Memory 多通道追踪体系,从 stat 命令族的运行时性能仪表盘(stat unit/game/scenerendering/memory/gpu)到七大常见性能陷阱的排查与修复(Tick 滥用/蓝图边界/GC 抖动/构造脚本/同步加载/过度复制/Widget 重建),从 Slate 无效化面板到 RenderDoc GPU 帧分析——覆盖 UE 性能分析与优化的全部面试与工业级核心。
4
第十五章 AI 系统:从行为树到感知体系的完整链路
UE C++深入笔记 **UE C++ 系列 · 第十五章。** 从 AI 控制器(AIController)与导航网格(NavMesh)的基础设施到行为树节点全谱系(Selector/Sequence/Decorator/Service/Task)的完整解析,从黑板(Blackboard)作为 AI 记忆系统的键值存储到 EQS 环境查询系统(Generator/Test/Context)的空间决策,从 AI Perception 三通道感知(视觉/听觉/伤害)到完整巡逻→发现→追击→攻击的 AI 敌人实战——覆盖 UE 人工智能系统的全部面试与工业级核心。
5
第十三章 数据驱动与资源管理:从 DataAsset 到异步加载体系
UE C++深入笔记 **UE C++ 系列 · 第十三章。** 从 DataAsset 体系(UPrimaryDataAsset/UDataAsset 替代硬编码)到 DataTable 全链路(FTableRowBase/UDataTable/Composite DataTable/CurveTable),从软引用三剑客(TSoftObjectPtr/FSoftObjectPath/FSoftClassPtr)到 Asset Manager 与 StreamableManager 异步加载管线,从 Primary Asset 标签体系到 Level Streaming 世界分区加载——第二部收官之章,覆盖 UE 数据驱动开发与资源管理的全部面试核心。
随机文章 随机推荐

评论区

Profile Image of the Author
LonelyStar
Hello, I'm LonelyStar.
公告
欢迎来到我的博客!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
158
分类
13
标签
488
总字数
329,363
运行时长
0
最后活动
0 天前

目录