
很多在Unity和Unreal里做了多年开发的同事一旦遇到引擎为什么不这么做为什么这个模块要这样组织之类的问题时往往会陷入一种无从下手的尴尬。功能写多了以后大家都会隐隐约约感觉到引擎不再只是一个工具而是一套有自己内在逻辑的复杂系统。这个系列的第一篇我就想把引擎的基础架构这件事讲透——不是去罗列某个引擎的模块清单而是从架构设计的角度讲清楚一个引擎在底层是怎么组织起来的为什么它要这样组织以及理解之后对你实际开发尤其是性能优化、平台移植、工具链扩展能带来什么实实在在的好处。这个系列适合三类人一类是写游戏逻辑写了两三年、开始觉得引擎底层是个黑盒的开发者一类是准备踏入引擎研发岗位、想建立整体认知的在校学生还有一类是项目里负责技术选型或自研工具链的TL和技术负责人。我不能保证看完这一篇你就能写出引擎但至少能让你的脑海里建立起一张还算清晰的引擎内部地图。1. 宏观骨架游戏引擎是一层一层叠出来的1.1 从实时循环说起一切架构都围绕每帧要干完这些活理解引擎架构的第一步是先把游戏运行这件事翻译成机器视角。游戏引擎的核心运行模型不是传统应用那种等待事件、处理事件的被动循环而是一个每帧都有硬性截止时间的主动循环。这个循环的要害在于你必须在1/60秒或者1/30秒内把输入、逻辑、物理、渲染、音频这一整套流程全部走完否则帧率就会肉眼可见地掉下来。这一条听起来简单但它是整个引擎架构的总纲。很多初学者不理解为什么引擎模块划分要那么细为什么要做多线程渲染为什么资源加载要搞异步流送答案全都指向这个总纲——你在为一个必须周期性完成且不允许超时的大任务做工程化管理。引擎里的一切分层、抽象、缓存、调度本质上都是在把这道超时即事故的流程切割成可以预测、可以并行、可以度量的子任务。拿厨房做个类比。一个高效的后厨不会等客人点完菜才开始洗菜切菜而是提前做好备料、按流程分给不同工位、哪道菜慢就提前开火。引擎的模块划分和调度设计就是给这套后厨流程画分工图——谁负责拿菜资源加载谁负责切配逻辑运算谁负责掌勺渲染谁负责传菜呈现。分工图没画好后厨就会乱成一锅粥。1.2 分层思想平台、核心、功能、游戏四层模型不同引擎的分层命名各有差异但抽象到一定高度后你会看到一个共性的四层结构。最底下是平台抽象层负责屏蔽操作系统、图形API、硬件输入、文件系统这些外部差异往上是核心层提供内存、容器、数学、诊断、任务调度这些与游戏内容完全无关的通用能力再往上是功能层把物理、渲染、动画、音频、场景管理这些面向具体游戏类型的子系统组织起来最顶上是游戏层由具体项目的玩法、关卡、角色、UI逻辑构成。这个四层模型的每一层只允许依赖它的下层不能反向依赖这是一个硬性的架构纪律。Unreal的Runtime模块分组、Unity的引擎程序集划分、Godot的SceneTree/Server架构仔细看都是在这个总体思路上做变体。你在上一层的代码里看到了下一层的东西很正常但如果发现下一层的代码反过来要持有上一层的东西那就要警惕了——这通常意味着架构设计出现了上瘾依赖也就是常说的循环依赖问题后面我会专门讲它。理解分层还有一个实际价值你修改代码时能快速判断自己改对了地方。前几天有个朋友跑来问我说他的项目要在Windows上新增对某个国产操作系统的适配问我要改哪块。我一听就知道这属于平台抽象层的事——只要他项目里对文件路径、动态库加载、窗口创建的访问没有散落到处都是就只需要在平台层加一个实现上面的模块全部无感。1.3 模块边界比模块本身更重要层与层之间只是大方向真正的架构设计功夫落在相邻模块之间的边界上。一个引擎内部往往有几十个模块你不能让它们互相乱指于是会形成依赖图渲染模块依赖资源模块物理模块依赖数学库场景管理模块依赖多个功能模块。这张依赖图就是引擎架构的宪法。怎么检查这张图是否健康我有个土办法在代码库里随机挑一个模块看它的头文件include列表。如果某个模块的头文件里include了一大堆别的模块而且这些模块彼此之间没什么必然联系说明这个模块的职责发散太严重或者边界切割错了。好的模块边界是高内聚、低耦合内部东西尽量藏起来暴露出来的接口要小而稳定。放在游戏开发里最直观的例子就是渲染模块好的渲染模块暴露给外界的可能就三四个接口提交可见对象、设定相机、等待帧完成。你完全不需要知道它内部是延迟渲染还是前向渲染是PC管线还是移动端管线。2. 地基工程那些看不出功能却决定引擎上限的底层子系统2.1 内存管理引擎的生死线不是性能测试的加分项游戏引擎和普通后端服务在内存管理上有一个本质差别游戏的内存使用模式极其复杂而且对延迟极其敏感。后台服务可以依赖操作系统内置的malloc/free勉强能用就行引擎里你要是裸用系统分配器一帧里几百上千次的小对象分配会直接拖垮GC如果托管语言或者产生严重的内存碎片如果C。所以商用引擎几乎都会做自己的内存分配器。Unreal的Malloc模块提供了多种分配策略可以切换Godot的Memory模块对分配做了统计与对齐处理自研引擎一般也会在Core层写一个基于按大小分桶的池化分配器。核心思想就是小块内存走池大块内存走系统分配器帧内临时数据用线性分配器一帧结束直接整体回收长期驻留数据走专用堆。这些设计的底层动机在移动平台和主机平台上尤其明显。移动设备内存带宽和总容量有限主机平台甚至要求你预估并汇报内存预算。你如果做的是轻量级独立游戏可能感受不到这个基建的重要性但项目一旦进入大批量战斗单位、大世界无缝地图的阶段内存布局的好坏会直接体现在掉帧、加载卡顿和崩溃率上。2.2 数学库与容器被当成轮子却是架构的地基基础架构里最容易被低估的是数学库。很多业务程序员觉得数学库不就是一堆向量矩阵运算的封装吗其实一两年前我在公司面试过一个候选人他对渲染管线聊得头头是道但问到矩阵的存储布局对CPU缓存的影响时就明显迟疑了。游戏引擎的数学库必须解决三个层面的问题性能要求SIMD向量化、一致性左手系还是右手系、行主还是列主必须全引擎统一、精度跨平台编译浮点行为一致。容器库同样被低估。引擎不能用STL容器直接打天下不是因为STL写得差而是因为第一你没法控制STL分配器的底层行为做不到对内存做统一统计和预算第二STL容器的异常处理和迭代器失效规则在游戏每帧快速创建销毁的模式下不够可控第三游戏里的实体ID、句柄这类东西需要专门设计的高效查找结构。所以引擎核心层普遍提供自己的容器实现并规定全引擎统一使用。2.3 对象生命周期、RTTI与序列化架构稳定性的隐形推手这个模块在业务开发里存在感很低但它几乎是引擎能否支持热更新、编辑器可视化、网络同步、存档系统的决定性因素。先说生命周期。引擎里少则几千、多则几十万的游戏对象谁创建、谁持有、谁销毁、什么时候销毁都必须有明确规则。否则一个Actor在A系统还引用着、在B系统已经销毁了就成了悬空引用跑起来的崩溃完全是随机的。商业引擎普遍采用所有权清晰垃圾回收/引用计数禁用手动delete的组合策略本质上是用一套规则对抗C的手动内存管理带来的不确定性。再说RTTI运行时类型信息和序列化。为什么引擎非得引入这些复杂的反射能力因为引擎层需要以通用方式操作游戏层对象编辑器要动态创建某个类型的实例并显示属性面板存档系统要把对象图序列化成二进制或JSON网络同步需要按类型分发表决对象状态。没有这一层反射机制这些功能全都得用一堆switch-case硬编码硬扛扩展一个新类型就要改六七处地方。2.4 事件系统与任务调度让模块间对话而不联姻模块之间不可能完全独立迟早需要通信。通信方式的选择会显著影响架构的演化方向。直接调函数是最快的但会让两个模块编译期就锁定在一起事件总线是解耦利器但会增加一次间接跳转和调试复杂度。成熟引擎通常在这个地方做分层处理同帧内、同线程的高频操作允许模块间直接调用接口例如物理模块反馈给场景管理的回调而跨模块、跨帧、可能跨线程的通知走统一的事件/消息系统例如资源加载完成存档写入完毕输入状态切换。这里有个实用建议不要所有通信都走事件总线。一个只有直接调用的架构太僵硬但一个全是事件的架构会陷入一个bug查半天到处是发出去没人听的幽灵事件的困境。用频率和生产者/消费者关系来做判断是最务实的分寸。任务调度是另一个容易被当成Java线程池的东西。游戏引擎的任务调度要处理的是帧内依赖并行化渲染和物理可以并行但要等逻辑结果资源加载可以后台做但流送切换时需要同步。引擎的任务调度器本质上是一个有依赖关系的DAG执行引擎做得好的可以跨核心调度数千个微任务配合JobSystem把多核利用率拉满。3. 设计模式在引擎里为什么是刚需而不是时髦3.1 组件模式对象-组件-系统三段式的历史合理性如果只能选一个最影响引擎架构的设计模式我选组件模式。这个模式解决了游戏对象模型一个核心矛盾你用类的继承树去表达游戏对象就必然遇到会飞的马这种多继承困境但你让每个对象都持有所有能力又会造成巨大的内存浪费和功能耦合。组件模式的解法是把对象是什么改写为对象由哪些组件拼装而成。所以Unity里挂一堆ComponentUnreal里Actor持有若干ComponentGodot里Node带所属的脚本和子节点本质都是一样的思路。组件模式带来的两大好处一是对象组合可以动态变化玩家死亡可以移除控制组件、添加倒地组件不必在类层级里预先定义所有状态二是同类组件可以聚合存储这一点是ECSEntity Component System的架构起点为的是把相同类型的数据连续排列让CPU缓存命中率大幅提升这对于动辄上万个单位的游戏来说往往是决定性差异。3.2 观察者模式与事件驱动解耦的代价与收益事件系统在游戏引擎里的应用比后台系统还要广泛原因在于游戏帧循环天然适合发布-订阅式的通信玩家按下按键输入系统不关心谁会响应击杀事件发生成就系统、音效系统、任务系统都想知道但它们彼此不需要知道对方存在。观察者模式在基础架构里的代表性落地就是事件系统。不过我必须提醒一点观察者模式的代价是调试困难。事件发出去以后你很难像直接函数调用那样单步追踪完整的调用链。所以在引擎里我的经验是给事件加上来源标签和订阅关系日志两类工具。来源标签用于线上数据统计比如某个事件每秒触发多少次、常被谁订阅订阅关系日志用于开发期调试你可以随时查看并打印这个消息最终被哪些系统消费了。没有这两个东西事件系统用久了会让团队心里发毛。3.3 服务定位器引擎如何给模块提供随取随用的入口引擎内部有大量全局唯一的服务渲染设备、资源管理器、音频播放器、输入处理器。这些服务遍布全引擎各处你要是每次使用都从参数层层传下去接口会被撑爆你要是用一堆全局单例裸奔又回到全局毒瘤的老路上。于是有了服务定位器模式。简单说就是引擎启动时把所有核心服务注册到一个统一的服务容器里运行期任何模块都可以通过一个服务名称按需取用。Unreal的FEngineLoop、Unity的引擎模块注册表、O3DE的ModuleManager都是这个思路。这里有一个很重要的架构经验服务定位器的Key要用抽象接口名不要用具体实现类名。依赖抽象接口你才能在测试环境注入假实现在正式平台切到真实现依赖具体类你的测试和引擎就焊死了。这个细节我见过太多团队在前期忽略、后期重构。3.4 状态模式只适合放在有限的地方游戏里到处是状态角色状态、AI状态、UI状态、游戏流程状态。初学者容易把状态模式神化遇到啥都套一个状态机。架构层面真正值得用状态机的只有生命周期明确、状态切换频繁、行为差异大的场景比如网络连接状态、战斗阶段切换。而角色动画这类过度拆分状态机反而会让行为配置变得极其笨重一般会用分层状态机加行为树组合。引擎基础架构层面的状态模式更常见的是引擎生命周期状态机启动Init、运行Tick、暂停Suspend、关闭Shutdown、崩溃Recovery。这套状态机的设计很影响引擎的稳定性和恢复能力。好的引擎生命周期设计会让每一阶段做什么、出错了往哪回退都有清晰定义这也是为什么Unreal的模块有StartupModule和ShutdownModule这类接口——引擎启动不是一个main函数从头走到尾而是一组模块按依赖顺序完成各自的初始化和清理。4. 数据驱动让架构不只是代码结构而是内容管线4.1 资源系统是引擎最容易被误解的基础架构行业里聊基础架构大家惯常聊渲染、物理、内存但资源系统的重要性经常被放到很后面才被提到。实际经验告诉我资源系统才是项目进入量产阶段后的真正瓶颈——它的架构设计决定了团队协作的效率上限。资源系统的核心任务不复杂用统一的抽象来管理所有具体的资源类型网格、贴图、音频、动画、蓝图、关卡并解决三个问题资源怎么来导入、格式转换、资源怎么存二进制缓存、平台版本、资源怎么用加载、引用计数、卸载。商用引擎里ResourceManager普遍提供类型注册器你新增一种资源类型时只需要实现加载器、转码器和引用处理逻辑注册进去就行。这种做法把资源类型的新增从改引擎核心代码降级成了写一个扩展插件这恰恰是架构稳定性的体现。4.2 引用计数与依赖图为什么不能想释放就释放资源的生命周期管理是资源系统设计里最容易踩坑的地方。一个模型资源可能同时被角色A、场景B、UI图标C引用你不能因为某个系统用完就随手释放。所以引擎必须维持引用计数或者依赖更细粒度的依赖图来做可达性分析。这也是为什么编辑器里你删一个资源时引擎会提示你先解引用——底层要被保证那些还在用它的对象不受影响。依赖图的价值还不止于此。资源之间有依赖关系一个关卡引用了很多模型模型引用了贴图和材质。加载一关时如果加载器能读取依赖图并采用并发加载那么关卡加载时间可以被显著压缩。Unity的Addressables、Unreal的Pak文件加载核心计算逻辑本质都是在处理这个有向无环图。理解了这一点你以后看引擎的加载优化就再也不会晕了一眼就能看出它在用按依赖序多线程加载还是整块序列化丢进内存。4.3 配置与数据资产分离代码和内容解耦数据驱动的另一个维度是把行为参数从代码里抽出来。做法上通常是识别出哪些是稳定的数值哪些是会变的设计数值。伤害值、冷却时间、掉落概率、音效音量这些都是策划频繁调整的内容如果散落在代码里每次调整都要重编程序产能就崩了。引擎基础架构要提供一套从配置到运行时对象的映射和校验机制。这里我要提醒一个过度数据驱动的坑不是所有东西都要配置化。如果一个参数的变体组合空间巨大且验证逻辑复杂比如战斗技能的表现组合硬做成纯数据配置会导致配置表变形为另一种代码而且调试体验比真实代码差很多。合理的边界是可枚举、变化频繁、非逻辑型的参数走配置逻辑分支、流程控制、判定规则留在代码或蓝图/脚本里。这是一个架构品味的问题很多人要踩过几年坑才能把握住。5. 引擎架构的成长痛为什么自研引擎很少一步到位5.1 单体仓库和编译时间你构建系统的方式也是架构的一部分这一节不是纯流程吐槽而是要指出一个很多人忽略的观点引擎的模块化程度最终会反映在你的编译体验上。一个上千个源文件、几十个模块的引擎项目如果架构没有做好物理隔离哪怕模块边界画得很正确也会面临一个现实问题——改动一个基础头文件全引擎重新编译一小时。让我算一笔账一个中等体量的引擎有3000个C文件前后端开发人员各5人。如果一个常用公共头文件被800个文件依赖那谁动这个头文件就等于触发800文件的重新编译。假设每个文件编译两秒就是1600秒接近半小时。这种编译地狱会直接压低团队的迭代速度也逼迫架构设计往头文件稳定化和实现隐藏化的方向走。代码要编译隔离通常靠前置声明、Pimpl指针指向实现技法、模块间接口头文件瘦身来做到。你看到的商业引擎运行速度远不如编译速度话题受欢迎但对做引擎的人来说编译速度就是日常的生产力。5.2 模块依赖规则的执法靠命名约定还是靠工具模块边界画好之后更难的问题是让团队长期守住。你画了一张漂亮的依赖图两周后有人图方便从渲染模块直接include了游戏层的类架构就开始腐化。这种腐化不会立即出问题但积累到一定程度模块就再也无法独立演进。业界一般有两种执法手段一是基于工具的强制检查比如在CI流程里加一个依赖扫描脚本把所有include之间的依赖关系实跑一遍不符合依赖规则的直接拦下二是基于构建系统的物理控制比如按模块生成静态库或者动态库DLL边界本身就是一道物理隔离墙。我个人的建议是小团队早期用工具检查加代码评审就够了到模块必须给第三方做SDK扩展的时候再考虑按模块拆DLL。过早拆DLL会让调试和迭代都变慢这又是一个架构过度设计的提醒。5.3 商业引擎、开源引擎、自研引擎不同定位下的架构取舍聊引擎架构总绕不开那到底是用Unity/Unreal还是自研的路线问题。我的观点很直接架构没有绝对好坏只有适不适合当前目标和团队规模。商业引擎架构的优势在于它已经为广泛的项目类型做了多轮抽象覆盖面广稳定性和文档都成熟劣势恰恰也在这——通用架构为了覆盖绝大多数场景必然带有抽象层冗余你对底层控制力越弱越难做极致优化。自研引擎的优势是所有设计只为你的游戏服务一条垂直优化可以做到极致代价是你要长期维护那棵架构之树——渲染前端、工具链、资源管线、平台适配样样俱全没有能力和耐心支撑的话自研引擎就是拖垮团队的深渊。我见过两类团队最让我惋惜。一类是产品都还没验证就花半年打磨自研引擎的等到玩法被否定引擎也白写了。另一类是明明要对渲染做深度定制却死守着商业引擎的默认管线的。业内现在的主流通解是玩法验证期用商业引擎验证通过后再按需求评估是否自研或深度魔改。这个次序虽然听着不酷但它在多数情况下是风险最低的路径。6. 建立你自己的引擎架构认知阅读路线和最小实践6.1 别一上来就啃引擎源码先搭最小可运行循环很多人问我怎么入门引擎架构第一反应都是去读Unreal源码。我一般要泼一盆冷水没有一定的上下文直接读几百万行源码的体验跟看天书差不多。更有效的路径是先自己动手写一个只有1千行代码的迷你引擎。这个迷你引擎不必有任何游戏性只要包含一个主循环收输入、更新逻辑、渲染一帧、一台极简的渲染设备清屏画三角形、一个资源管理类加载一个简单模型、一个组件挂载系统对象加位置组件和渲染组件。你要亲手体会模块之间的依赖怎么建立、生命周期怎么管理、首帧加载和运行期加载怎么衔接。我当时就是靠这种迷你工程把分层、模块边界、数据驱动这些抽象概念第一次变成了手上看得见摸得着的东西。6.2 带着问题去读引擎源码从它做了什么到它为什么这样做当你有了迷你引擎的经验再读商业引擎源码时就会产生完全不同的视角。不要逐行读而是带着问题去搜我想知道Unreal的AActor和UComponent的注册关系我怎么搜我想知道Unity的脚本生命周期InitializeOnLoad是怎么被引擎调用的当你带着具体问题进入代码你会发现引擎源码不再是一堆陌生的符号而是指向你脑海中那张架构地图的一次次验证。阅读顺序上我建议先读最基础的模块内存分配、容器、数学库、反射系统。这些模块完全独立接口清晰是最适合入手的软柿子。之后再去碰场景管理和资源系统最后才是渲染、物理这些大模块。还有一个技巧优先读模块的公开头文件看对外暴露了什么、隐藏了什么。公开头文件本身就在描述这个模块的架构意图读懂了头文件模块内部的实现细节反而次要了。6.3 从架构到项目的最佳实践让架构知识真正为开发服务架构知识如果不能反哺到日常项目学起来就很飘。我的做法是把引擎架构思维拆成三个可落地的清单放在团队协作里反复用。第一张清单是模块边界审查清单用于新人提交代码前的自检我这个改动是否跨过了所属模块的边界是否引入了对无关模块的头文件依赖是否单向依赖了然第二张清单是生命周期审查清单对象的创建和销毁是否在同一所有权语境下是否会出现静态变量在销毁后还被引用的情况编辑器运行结束时的清理是否完整第三张清单是扩展点审查清单如果两周后要加一个新的资源类型、一种新的渲染特性、一个第三方平台我的现有架构是不用改代码还是需要改动核心模块这三张清单看似简单但每一条背后都有真实的崩溃或加班的代价。写在最后的几句体己话引擎基础架构这块内容很多细节真要展开讲的话足够写好几本书。我在项目里摸爬滚打这些年最大的体会有两点。第一架构设计永远服务于可预测性。好的架构不会让所有事都变快但会让出问题时你能快速定位这件事变得确定。第二架构能力是练出来的不是看出来的。你只需要亲手写坏过一两个模块再亲手把它们的边界重新切干净你才真正理解低耦合这三个字分量有多重。下一篇我打算沿着这个系列往下走聚焦引擎核心层最硬核的一块——内存与多线程调度聊聊游戏引擎怎么样在有限的时间内安全地压榨每一颗CPU核心。到时候见。