ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UE架构深度拆解:模块化设计、启动流程与Lyra实战解析

UE架构深度拆解:模块化设计、启动流程与Lyra实战解析 老实说现在聊游戏引擎架构的人不少但真正把 UEUnreal Engine当成一个完整系统来拆的人其实没那么多。尤其是刚接触 UE 的开发者一打开编辑器满屏模块、插件、蓝图类第一反应往往是“我该点哪里”而不是“这套东西到底是怎么被组织起来的”。这轮 UE 实战与高级主题我想把认知从“会操作”拉到“懂机制”这一层讲 UE 的模块化组织、启动流程、Lyra 示例项目怎么读以及“UE 支持能力查询”这类平时容易忽略、但架构决策时特别有用的实操方法。适合想深入理解虚幻引擎内部机制的程序员、技术美术以及正在评估 UE 技术选型的技术负责人。1. 从架构视角理解 UE先弄清楚它为什么长这样1.1 架构思维的价值从“能用”到“能改”很多人写 UE 功能方式是“需求来了先找官方模板再改蓝图”。这没错但遇到一个临界点会卡住需求不再只是改参数而是要动引擎行为、改框架流程、接内部系统。这时候如果脑子里没有架构图就只能盲目翻源码效率极低。我见过不少项目高峰期 30 人团队被一个“启动加载顺序”的问题卡了三天。原因很简单没人说清楚 Engine 初始化、GameInstance、World、GameMode 之间的先后关系改了一处模块加载把另一处的初始化时机搅乱结果一连串崩溃。架构思维的价值就在这里——它能让你在动手之前预判“动了这里会影响哪里”而不是靠试错去救火。对 UE 来说架构思维还有一层现实意义UE 本身迭代很快从 UE4 到 UE5引入了新的渲染管线和大规模场景技术但底层模块化思路没有大变。把架构吃透换来的是跨版本迁移时心里有底不至于每次升级都像重新学一遍。1.2 UE 的全局分层从哪里开始读代码UE 源码量非常大第一次进去容易迷路。我个人习惯按四层去理解整棵源码树平台层包括各平台的 RHI渲染硬件接口、平台抽象、底层内存分配。这块平时写得少但影响所有上层性能。核心层包括Core、CoreUObject、Engine模块处理对象系统、反射、序列化、组件模型。框架层包括GameplayAbilities、GameplayTags、AIModule、GameplayTasks这类偏游戏玩法逻辑的模块。应用层项目自己的Game、Editor模块以及 Lyra 这类官方示例玩法框架。这套分层和大多数业务系统的分层逻辑是一致的底层稳定、上层多变。你写游戏逻辑时九成时间在应用层和框架层只有需要定制渲染或资源管理时才下沉到核心层。理解了这个分层再去看“为什么 UE 的模块依赖是这样单向的”就不会觉得源码目录乱。2. 读懂 UE 的模块化骨架启动流程与模块系统2.1 模块系统UE 的“代码积木”UE 的工程由一个个 Module 组成每个 Module 是一个 C 项目里的编译单元对应一个.Build.cs文件。UE 的链接和加载不是一坨代码全塞进主程序而是按模块依赖关系一个个加载。这意味着你可以只编译改动的模块也可以只加载需要的插件从架构上避免了大型项目的依赖混乱。查看一个模块的依赖最直接的方式是打开.Build.csusing UnrealBuildTool; public class MyGame : ModuleRules { public MyGame(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore }); PrivateDependencyModuleNames.AddRange(new string[] { EnhancedInput, GameplayAbilities, GameplayTags }); } }PublicDependencyModuleNames和PrivateDependencyModuleNames的区别是架构干净与否的关键。Public 依赖会暴露给下游模块放多了会让依赖图变得很“脏”容易产生循环依赖Private 依赖只有本模块能用相当于对外隐藏内部实现。我见过很多团队图省事把所有依赖都丢进 Public结果后期模块之间相互拉扯编译越来越慢。保持 Private 优先是 UE 架构实践中第一条性价比最高的规矩。2.2 启动流程UE 启动时到底发生了什么UE 的启动流程是典型的“分阶段初始化”了解它你才知道为什么有些逻辑必须放在特定位置。简化的顺序大致是Main入口解析命令行参数创建Engine对象。初始化 Core 模块加载反射系统、UObject 基础系统。加载目标平台相关模块包括 RHI 模块、音频模块等。FEngineLoop::Init完成引擎核心初始化加载项目配置。创建GameInstance预加载地图资源。进入地图加载流程World创建、GameMode生成、PlayerController/Pawn生成。很多人写“进入游戏要做某事”直接塞进BeginPlay但BeginPlay在不同 Actor 之间的调用顺序并不严格如果逻辑依赖别的系统已完成初始化就会出问题。更稳妥的方案是放在GameInstance或GameMode的特定回调里因为它们的状态机更明确。这里的架构意义是UE 把启动过程拆成了“引擎阶段”和“游戏阶段”两层。引擎阶段不依赖任何具体玩法游戏阶段则完全由项目控制。你如果要在项目里加入自己的系统应该顺着这个分层去放基础技术放引擎阶段玩法相关放游戏阶段。3. 高级主题实战Lyra 示例项目的拆解思路3.1 为什么建议研究 LyraLyra 是官方推出的多人在线射击示例项目和传统模板最大的区别在于它不再是“教你搭个场景跑起来”而是“给你一套可扩展的玩法框架”。它的热度这么高持续被社区拿出来逐帧分析是因为它几乎把现代 UE 游戏架构的最佳实践都堆了进去数据驱动配置、GAS 技能系统、CommonUI、基于标记Tags的交互、资产管理器AssetManager等。研究 Lyra 时不建议一开始就盯着画面看而是先看它的目录结构。我下面用一个典型目录作为参考Plugins/ GameFeatures/ CommonGame/ LyraGame/ Source/ LyraGame/ GameModes/ Teams/ Equipment/ Inventory/GameFeatures插件是 Lyra 架构的核心之一它的设计思想是“把玩法功能拆成独立特性”每个特性可以单独启用、停用相当于在引擎之上做了一层功能模块化。你写的某个功能如果以后可能复用就可以尝试以 GameFeature 的思路去组织数据而不是全部堆积在 GameMode 里。3.2 数据驱动框架用数据代替硬编码逻辑Lyra 非常强调“数据驱动”。一个英雄能不能放某个技能、能带哪些武器几乎不是靠 C 里一堆if判断而是通过资产组合来配置。关键资产包括LyraExperience定义一次“玩法体验”的组合包。LyraPawnData定义 Pawn 骨骼、动画集、能力集等。UGameplayAbilitySet批量给 Pawn 赋予技能。这种“资产即配置”的思想在多人项目里尤其重要。策划不需要懂代码也可以调整技能组合而且不用触发整包重新编译。架构上的优势是逻辑和数据解耦测试时也可以单独替换数据资产来验证玩法。但数据驱动有一个副作用查找逻辑很难。如果一个GameplayEffect出了问题你顺着资产链查下去要跳好几层资产才能看到最终数值。调试时我建议直接打开“资产依赖”面板或者用编辑器右键“Reference Viewer”效率比硬翻代码高很多。3.3 GAS 与 CommonUI类引用关系的温度计Lyra 里的技能系统用的是官方 GASGameplay Ability System它的核心类就三个AbilitySystemComponent、GameplayAbility、GameplayEffect。三者关系可以这么理解ASC是容器负责管理能力的启停和状态同步。Ability是技能“动作”本身决定施放逻辑。Effect是技能造成的结果负责数值和状态修改。在实际项目里最容易踩坑的是“同时有多个系统对同一个属性做修改”。GAS 里这类问题最终都要回到GameplayEffect的Modifier和Aggregator上。一旦属性值不对先看是不是有多个 Effect 叠加顺序错了而不是急着改公式。CommonUI 则是 Lyra 处理 UI 的方式它的核心价值是把“界面状态”和“输入法模式”绑定在一起。架构上UI 层不再直接监听输入事件而是通过CommonUIAction做映射这样换手柄、换键鼠时只是换映射配置不用重写界面代码。我建议新项目即便是单人游戏也把 CommonUI 纳入起点否则后期做主机平台适配会非常痛苦。3.4 从 Lyra 反推引擎能力边界研究 Lyra 还有一个好处它是官方维护、持续跟进引擎新特性的项目。你想知道某条渲染管线是否稳定某个网络同步特性怎么落地看 Lyra 的提交历史比看官方文档更直观。比如 Lumen 和 Nanite 这样的功能官方示例往往偏“演示”而 Lyra 更多是“真实玩法场景里怎么取舍”。我曾经在一个项目里听到同事抱怨“UE5 的某某功能不好用”后来仔细一查是项目没按 Lyra 那样处理好功能开关和资源格式。要说经验就是在动引擎源码之前先到 Lyra 里搜一下对应功能用到了哪些模块和配置很多时候能省掉你在黑暗里摸索的时间。4. UE 支持能力查询架构决策前的必修课4.1 硬件与平台能力查询“UE 支持能力查询”听起来像官方文档函数但其实是一类操作的合集在写跨平台、跨硬件、跨引擎版本的功能前先确认目标环境到底支持什么。最常用的是 RHI 能力查询。UE 提供了FGenericDataDrivenShaderPlatformInfo和RHIFeatureLevel你可以用它们在运行时判断当前平台对应的着色器模型switch (GMaxRHIShaderPlatform) { case SP_PCD3D_SM5: // DX11 SM5 break; case SP_PCD3D_SM6: // DX12 SM6 break; default: break; }为什么要显示做这种查询因为不同平台的 Feature Level 决定了你能不能用 Nanite、Lumen 或硬件光追。如果项目准备上移动端那这些桌面级特性大概率要降级。架构层面应该在资源加载和渲染设置之前就确定能力等级而不是让各子系统各自乱猜。4.2 运行时硬件查询与配置分级除了引擎底层查询项目层往往需要更细的“设备性能分级”。UE 的FPlatformMemory、FPlatformMisc能拿到内存大小和 CPU 核心数但这些原始数据不能直接映射到“中高低画质”上。我的做法是写一个DeviceProfile服务启动时收集硬件信息再把信息映射到项目自定义的质量档位const FString GPUName FPlatformMisc::GetGPUDriverInfo(nullptr).GetDeviceName(); int32 CoreCount FPlatformMisc::NumberOfCores(); // 结合显存大小和平台类型选择对应 DeviceProfile这一步放在游戏开始之后、加载大型资源之前。架构设计上它应该被做成一个独立模块只依赖 Core 层避免被 UI 或玩法模块反向依赖。否则后续做性能自适应调整时会很难受。4.3 功能特性查询驱动的架构决策当你从 UE4 迁移到 UE5或者打算启用新功能时“支持能力查询”还应该包括引擎版本的特性差距分析。实操上可以写一个小的自动化脚本扫描当前源码里SupportsNanite、SupportsVirtualTexture这类宏生成一份能力表然后和项目需求做差集。举一个真实例子某个项目想用 Virtual Texture 优化大地形内存但目标用户有一部分是老显卡。我们先用引擎提供的能力查询接口在编辑器里模拟了几款显卡的 Feature Level发现如果硬开 VT老平台会出现大面积纹理闪烁。最终决定做成运行时动态判定支持时用 VT不支持时回退传统纹理流送。这个决定在架构上等于增加了一个“能力判断层”但避免了一次发布后的大规模翻车。5. 从架构到性能UE 优化里的结构性问题5.1 别急着调数值先看架构合理性很多新手优化性能一上来就改影音设置、降分辨率收效甚微。更值得做的是在现有架构上找结构性问题。UE 游戏跑得慢常见原因不是某个贴图大而是加载了不必要的模块和插件。蓝图节点调用链过长导致事件驱动效率低下。每个 Tick 都做高频查询而不是用事件通知。网络同步频率过高属性复制没有做条件筛选。这些问题都属于“架构级别的性能问题”改参数只是止痛改结构才是治疗。你可以用 Unreal Insights 和 profiling 抓出前十分钟的热点函数再回到模块依赖图里看哪些模块不必要地拖进了启动和关卡加载。5.2 渲染架构与 CPU/GPU 瓶颈的平衡UE5 的渲染架构比 UE4 复杂很多Lumen 的全局光照、Nanite 的虚拟化几何体、以及后来的多视图渲染都会明显影响 CPU 和 GPU 的负载分布。架构层面的黄金法则是不要修改你不理解的默认配置。如果你开了一个新功能发现帧率掉了 20%比起一味关闭更应该做的是用r.Lumen.DiffuseIndirect.Allow这类控制台命令做二分定位。我自己的习惯是每次只改一个变量记录帧率前后变化再决定是保留还是回退。这听起来原始但比“网上抄一份画质设置”可靠得多。性能问题上Ultra 档以后边际收益极低但边际成本极高。如果你同时开启 Nanite、Lumen、硬件光追和完整后处理大部分中高端显卡也会吃紧。懂得在架构上保留性能弹性也就是做能力查询和分级比优化阶段再疯狂砍细节要高明得多。6. 常见问题与排查技巧实录6.1 编译与链接模块依赖引发的连锁崩溃UE 的 C 项目报错最多的两类一类是Could not find module或链接时unresolved external symbol另一类是游戏能跑但编辑器加载插件时崩溃。排查第一类问题先检查.Build.cs里PrivateDependencyModuleNames是否包含对应模块。比如你用到了GameplayAbilities却只加了GameplayTags编译时偶尔能过链接时就会暴露。UE 有一些模块是“双重依赖”的需要同时把运行时和编辑器模块都加进来否则编辑器下会出现奇怪的断言失败。特别提醒不要随意添加UnrealEd或Blutility到运行时模块依赖。这些模块只在编辑器下存在如果你把游戏工程直接打包发布包含编辑器模块会导致打包失败。我踩过一次最后把相关代码抽到了独立插件里才彻底解决。6.2 运行时崩溃GAS 和资产加载的坑GAS 类项目最典型的崩溃是初始化AbilitySystemComponent时找不到对应的DefaultStartingData或者GameplayEffect资产没有正确加载导致空指针引用。形象地说就是“技能特效挂在一个空手上”。排查思路很简单在调用GiveAbility或ApplyGameplayEffectToSelf之前先确认相关资产已经在内存里。你可以用TSoftObjectPtr做异步加载或者直接打成PrimaryAssetId并在AssetManager里提前注册。直接用硬引用最省事但会拖大包体和加载时间。Lyra 的做法是使用FGameplayTag和资产标签Asset Tags做筛选加载时才按需实例化。这个设计在大型项目里非常推荐但代价是资产引用链不好追踪。调试时多利用Gameplay Debugger和AbilitySystemDebugger插件能看到当前角色持有哪些能力、哪些 Effect 还在生效。6.3 Lyra 学习中的误区和应对很多人学 Lyra 一上来就复制整个项目结果编译过了改一行逻辑却不知道从哪下手。我建议按这个顺序学先跑通项目熟悉它的输入映射和 UI。把LyraExperience的资产引用链完整点开看一遍。找一个简单功能比如换武器从头追踪到Equipment模块。再尝试给角色新增一个被动技能。过程中大概率会遇到“为什么我的角色的 GAS 没生效”这类问题。经验是先检查 Pawn 身上有没有挂AbilitySystemComponent再检查它有没有注册到LyraPawnExtensionComponent。Lyra 里几乎每个玩法功能都依赖 Pawn 扩展组件Pawn Extension来分发生命周期事件如果这个扩展组件没被初始化后面的功能全塌。7. 写在最后的实操心得UE 架构的学习没有捷径但你不用从头到尾把所有源码都读完。我自己最受益的方式是“带着问题读”遇到一个性能瓶颈就去查 Unreal Insights 里的热点遇到一个框架设计就去 Lyra 里找它怎么落地。反复几次大脑里自然会形成一张模块地图。另外一个小技巧给项目做一个“模块白名单”文档把每个模块的依赖方向和初始化职责写清楚。虽然前期会花一点时间但后面每个新成员入职照着这份文档就能快速定位问题几乎是被问过无数次之后我觉得最值得坚持的一件事。版本升级、功能模块增多时这份文档也能帮你判断哪些依赖正在失控而不是等编译时间爆炸那天再来重构。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进