从 Epic 的 GitHub 仓库把 UE5 拉下来,Engine/Source 下面 300 多个模块、几千万行代码,直接 grep 全文等于自杀。读 UE 源码的第一步不是读代码,而是记住这张地图——因为 UE 的目录划分不是随意的文件归类,它和构建系统(UBT 按目录边界切模块)、加载系统(每个模块是一个 DLL)严格一一对应。搞懂目录就是搞懂模块,搞懂模块才知道一段代码归谁管。
Engine/Source 的四大分区
源码树根下只有四个一级目录,分区的依据是代码在什么阶段运行、进不进最终的 Shipping 包:
Engine/Source/
├── Runtime/ # 运行时模块:游戏打包后真正运行的代码
├── Editor/ # 编辑器专属模块:只在 -editor 模式下加载
├── Developer/ # 开发期工具模块:编辑器用,但不属于编辑器框架本身
├── Programs/ # 独立可执行程序:UBT、UHT、UnrealPak 等
└── ThirdParty/ # 第三方库的源码副本(zlib、Vulkan、nvcloth……)
这个四分法背后是一条强约束:依赖只能向下,不能回头。Runtime 模块不能引用 Editor 模块的头文件,Editor 模块也不能依赖 Programs。UBT 在解析 .Build.cs 的依赖时会检查这个层级,Runtime 模块里 include 一个 Editor 头文件会直接报构建错误。所以判断一个功能代码该去哪找,先问"这个功能打包后还在不在"——在就去 Runtime,不在就去 Editor。
Runtime:真正的引擎
Runtime/ 是引擎本体,里面的层级比顶层四分法更实用。按依赖深度从底到顶:
- Core:
Engine/Source/Runtime/Core,不依赖任何其他 UE 模块的底座。容器(Containers/)、字符串(Containers/StringConv.h等)、内存分配(HAL/)、数学库(Math/)、日志断言都在这里。Core 不依赖 UObject 系统,所以它能被 Programs 里的 UBT 这种纯命令行工具用。 - CoreUObject:
Runtime/CoreUObject,UObject 反射、GC、序列化、包加载。它依赖 Core,是整个对象模型的地基。 - Engine:
Runtime/Engine,最大的一个模块,AActor/UWorld/GameFramework/物理接口/渲染高层调度全塞在这里。新手常犯的错是什么都去 Engine 里找,其实 Engine 只是"游戏运行时"层,渲染的真正实现在更靠右的模块。 - 渲染线:
RHI(硬件抽象)→RenderCore(渲染通用结构、RDG)→Renderer(Nanite/Lumen/后处理的具体 Pass 实现)。查渲染问题别在 Engine 里翻FScene,直接去Runtime/Renderer/Private。 - 平台抽象:每个模块的
Public/、Private/、Classes/之外,平台特化代码散在各处但遵循同一约定——HAL定义接口,*Platform*(如WindowsPlatform、ApplePlatform)实现。查平台行为先搜F*Platform*前缀。
Runtime/ 依赖方向(单向):
Core ← CoreUObject ← Engine ← GameplayTasks / AIModule / ...
↘ RenderCore ← Renderer
一个值得记住的检索结论:模块内 Private/ 的头文件对其他模块不可见。UBT 只把模块的 Public/ 和 Classes/ 加进外部 include 路径。所以你在另一个模块里 #include 某个头文件报 "file not found" 时,先确认这个头文件是不是在 Private/ 下——如果是,说明模块作者就没打算让你用,正确姿势是找它 Public 的接口,或者干脆换个思路,而不是手动改 include 路径去硬闯。
Editor 与 Developer 的微妙分界
这两个目录的功能在"不进 Shipping"上是一致的,区别在定位:
Editor/:编辑器框架本身。UnrealEd(关卡编辑器核心)、BlueprintGraph(K2 节点)、PropertyEditor(属性面板)、DetailCustomizations。改编辑器行为、写 Detail 面板定制,代码归这里。Developer/:给编辑器和其他工具用的开发期基础设施。AssetTools、MessageLog、SourceControl、DerivedDataCache、Settings。可以理解为"编辑器用的 Runtime 库"。
实际影响在 .Build.cs 里:模块类型字段(ModuleType)决定了一个新模块该编进哪个目标。你写的插件如果声明成 Editor 类型却在 Runtime 目标里被引用,Cook 时会直接挂掉。
Programs:被低估的阅读入口
Programs/ 下是独立编译的 exe,不是引擎模块。这份名单本身就值得背下来,因为它们是你日常使用频率最高的工具链:
UnrealBuildTool:UBT 本体,所有构建的调度者(下一篇专门讲)。UnrealHeaderTool:UHT,反射代码生成器。UnrealPak:打包/解包 pak 文件,命令行直接可用。ShaderCompileWorker:shader 编译器进程,材质编译卡顿时stat shadercompiling对的就是它。UnrealInsights、UnrealFrontend等。
Programs 的模块只依赖 Core 和少量 Developer 模块,代码量小、没有 UObject 包袱,是入门 UE 代码风格的最好样本。想理解 TMap 怎么用、日志怎么打,去看 UnrealPak 的源码比看 Engine 清爽得多。
ThirdParty 与 BuildConfiguration
ThirdParty/ 里每个库都有一个 .Build.cs(如 zlib.Build.cs),UBT 靠这些文件把第三方库也纳入模块依赖体系——PublicDependencyModuleNames.Add("zlib") 就能用上。注意引擎用的是自带版本的第三方库,和你系统里装的版本无关,别去链接系统的 OpenSSL。
Engine/Source/UE5/(部分版本是 Engine/Source/Developer 同级或 BuildConfiguration)其实不算第五大区,但有一个文件必须知道:Engine/Config/BaseEngine.ini 和 Engine/Source/Programs/UnrealBuildTool/Configuration/——全局构建规则(哪些警告当错误、默认 C++ 标准)在这里定义,不在任何模块里。
项目侧目录:和引擎侧的镜像关系
游戏项目的目录和引擎是镜像的,理解映射关系后找文件很快:
MyProject/
├── MyProject.uproject # 模块清单(等价于引擎的模块声明)
├── Source/
│ ├── MyProject/
│ │ ├── MyProject.Build.cs # 模块依赖声明
│ │ └── *.cpp / *.h
│ ├── MyProject.Target.cs # 游戏目标
│ └── MyProjectEditor.Target.cs # 编辑器目标
├── Binaries/ # 编译产物(模块 DLL)
├── Intermediate/ # UHT 生成物、编译中间文件、UnrealBuildTool 缓存
├── DerivedDataCache/ # DDC,资产的派生数据缓存
└── Saved/ # 日志、截图、自动保存、Cook 产物
三个实战结论:
- 改完
.Build.cs不生效时,先删Intermediate/再让 UBT 重新生成工程文件。UBT 有依赖缓存,缓存键没带全所有输入时会出现"改了依赖没重新编译"的灵异现象。 Binaries/里 DLL 的命名暴露了模块系统:UE5Editor-Engine.dll、UE5Editor-MyProject.dll,一个模块一个 DLL(Monolithic 打包时才会全部静态链接成一个 exe)。加-hotreload后缀的是热重载产物,看到UE5Editor-MyProject-1234.dll这种带数字的,说明热重载发生过且老 DLL 没释放。.generated.h不在你的 Source 里,在Intermediate/Build/<Platform>/<Config>/Inc/<ModuleName>/下。调 UHT 相关的问题(宏说明符不生效、生成代码报错)要去这里看展开结果,源文件里看不到真相。
检索套路:从现象反推模块
最后给一套我自己定位代码的流程,比全文搜索快一个数量级:
- 先定分区:功能打包后还存在吗?存在 → Runtime,不存在 → Editor/Developer,是独立工具 → Programs。
- 再定模块:用类名前缀判断。
F开头的纯数据结构大概率在 Core 或本模块;U开头在 CoreUObject 或 Engine;渲染相关带RHI/RDG/Scene字样的去渲染线。 - 模块内定文件:UE 的目录命名高度约定化,
Private/<子系统名>/<功能>.cpp。查 Nanite 就去Renderer/Private/Nanite/,查序列化就去CoreUObject/Private/UObject/。 - 验证归属:打开文件看一眼文件头注释和
.Build.cs的依赖,确认这个模块确实依赖了你以为的上游——依赖关系是最可靠的"代码导航图",比任何文档都新。
这套流程的本质是把"读代码"转化成"读构建配置"。UE 的模块边界划得很硬,.Build.cs 里的 PublicDependencyModuleNames 就是这个模块对外承诺的能力清单——顺着依赖图走,基本不会迷路。下一篇讲 UBT 怎么把这些 .Build.cs 翻译成实际的编译命令。