ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

游戏引擎架构深度解析:模块划分、主循环与内存管理

游戏引擎架构深度解析:模块划分、主循环与内存管理 先回答一个最容易被问住的问题游戏引擎到底是个什么东西很多人把它理解成渲染器觉得引擎就是画画面的也有人把它理解成引擎盖下的代码觉得能跑起来就行。但如果你真正拆开一个成熟的引擎比如Unity、Unreal、Godot或者自己从零维护过商业引擎你会发现引擎首先是一个骨架其次才是一堆功能模块。骨架没搭对渲染再强、物理再准最后都会演变成维护噩梦。这篇是游戏引擎架构深度解析系列的第一篇我打算先把引擎基础架构这件事彻底讲透模块怎么划分、依赖怎么控制、主循环怎么驱动、内存怎么管理、模块之间怎么通信。这些内容不会教你写某个具体算法而是讲清楚引擎这栋楼的地基和承重墙是怎么立的。适合两种人看一种是打算自己动手写引擎、或者正在看引擎源码但被各种模块绕晕的开发者另一种是用引擎做项目、被框架逼得难受想弄明白为什么引擎非这样不可的人。读完你至少能回答一个问题一个引擎的骨架到底要承担哪些责任。1. 引擎基础架构的边界哪些算骨架哪些算功能模块在我见过的很多项目里架构混乱和功能复杂经常被混为一谈。团队一边抱怨代码难维护一边又觉得是因为功能太多。实际上功能多从来不是问题问题在于架构没有把该稳定的部分固化下来。所以聊引擎架构之前得先把基础架构的范围划出来。1.1 先把架构拆开看三个层面我习惯把引擎的基础架构拆成三个层面来看这样讨论起来不会鸡同鸭讲代码布局目录与工程划分哪些代码放在哪个模块、编译依赖怎么走、测试怎么挂。这是最容易被忽视、却最影响协作效率的一层。运行时骨架生命周期与主循环引擎启动之后按什么顺序初始化、每一帧按什么节奏推进、退出时怎么收尾。这是引擎的心跳。核心机制内存、事件、资源、任务所有功能模块都要依赖的横向设施。渲染用内存、物理用内存、动画也用内存内存管理这种机制必须统一。功能模块则指渲染器、物理系统、动画系统、音频系统、粒子系统、UI系统这些具体业务能力的实现。它们本质上都是豋在骨架上的器官。器官可以换骨架最好不要随便动。判断一个东西属于骨架还是功能模块我有一个很简单的标准如果它被删掉整台引擎直接散架、完全无法启动那它就是骨架如果删掉它只是没了某个能力引擎还能跑那它就是功能模块。1.2 为什么先搭骨架比先做渲染重要得多很多人写引擎是这么起步的先开一个窗口画一个三角形然后一路堆渲染功能。这路子很有成就感但到10万行代码之后会撞上一堵墙——你开始分不清某个全局对象到底是谁创建的、谁销毁的帧循环里该先更新谁后更新谁完全没有依据加了网络同步或者多线程之后直接乱套。我自己也有过类似教训。早期做一个小型引擎一开始觉得反正就自己写写模块化以后再说结果到后期加音频系统时发现音频驱动要访问渲染器的视口信息渲染器又需要音频的时间戳去做节奏同步两个模块互相引用编译依赖打成了一个环。最后花了整整一个多月重构才把依赖理清楚。如果把基础架构当成第一优先级这类问题根本不会出现。骨架的价值不是让首帧画面更快出来而是让引擎在5万行、20万行代码之后依然可维护、可扩展、可调试。1.3 基础架构要回答的三个核心问题总结下来任何引擎的基础架构都必须回答三个问题生命周期问题引擎的各个部分什么时候创建、什么时候运行、什么时候销毁顺序由谁决定依赖关系问题模块A能不能调用模块B如果能依据是什么如果不能那怎么拿数据数据流问题输入怎么变成游戏状态的变化游戏状态怎么变成画面上的像素中间的数据走哪条路径这三个问题想清楚了基础架构的大方向就稳了。剩下的都是在这三个问题下面填具体方案。2. 模块划分与依赖管理从堆代码到分层架构的转变引擎代码一旦超过一定规模最先崩掉的不是性能而是依赖关系。模块划分不是简单地把文件塞进不同文件夹而是要定清楚谁允许知道谁。2.1 一个成熟引擎的典型模块清单先给一张通用模块清单。不同的引擎叫法有差异但骨干结构高度相似模块类别典型模块职责平台层平台抽象层、窗口系统、输入系统屏蔽操作系统差异提供统一接口核心层内存分配器、容器库、数学库、时间系统引擎的基础设施不依赖任何功能模块资源层资源管理、序列化/反序列化、文件系统管理资产从硬盘到内存的全过程功能层渲染、物理、动画、音频、粒子、UI、网络引擎对外表现的核心能力框架层场景图/场景管理、实体组件系统、游戏循环把功能模块组织成可运行的游戏世界工具层编辑器、调试工具、性能剖析器开发期支撑运行时通常剥离这张表本身不稀奇关键在依赖方向平台层和核心层谁都不依赖资源层依赖核心层功能层依赖核心层和资源层框架层依赖功能层工具层依赖一切运行时代码。箭头始终朝下不能反向。2.2 依赖方向怎么定义才不烂我经常用一句话概括依赖管理原则低层模块不能知道高层模块的存在高层模块可以调用低层模块。这条规则听起来简单做起来却非常容易破功。最常见的破功场景就是为了省事直接include。比如物理系统需要把碰撞结果通知给游戏逻辑最偷懒的写法是在物理系统里直接调用游戏逻辑的某个接口。短期确实快但物理系统从此知道了一个不该知道的东西。以后换游戏项目、复用物理模块时它拖着一堆游戏逻辑的依赖根本拆不出来。正确做法是把碰撞结果通知定义成物理系统对外发布的回调或事件谁感兴趣谁自己注册物理系统自己保持纯净。模块之间依赖关系恶化是有征兆的编译时间越来越长、改一个公共头文件导致无数模块重编、想单独测试某个模块变得不可能、删除一个模块时发现根本不知道还有什么牵连。一旦出现这些情况架构已经在腐化了。2.3 接口层与实现层解耦的基本功定义依赖方向之后下一步是在模块内部区分接口和实现。一个模块对外暴露的应该是一组稳定的接口抽象类或者纯C接口内部实现可以随便换。我举个例子渲染模块。对外暴露RenderDevice接口有CreateTexture、DrawMesh、Present之类的方法。内部可以是OpenGL实现、Vulkan实现、或者DirectX实现。上层逻辑只跟接口打交道换后端渲染API不需要动业务代码。这就是依赖倒置——上层定义我要什么下层负责怎么实现。放在整个引擎层面所有功能模块都应当通过注册的方式接入骨架而不是骨架去硬编码每个模块。比如引擎启动时渲染模块把自己注册进渲染管理器物理模块把自己注册进物理世界。这样新增一个模块改的是模块自己而不是骨架本身。2.4 实践经验模块拆分时的三个坑模块拆分做得好不好经验成分很大。我踩过的坑大概有三类列出来供参考过度拆分把强耦合的代码硬拆成两个模块接口比实现还复杂得不偿失。比如把数学库拆成向量模块矩阵模块四元数模块纯粹自找麻烦。共享工具类陷阱把什么函数都丢进Common或者Utils最后Common变成垃圾场依赖它比依赖核心模块还多。工具类应该按领域就近放置而不是集中堆放。循环依赖的隐蔽形式有时两个模块看起来没互相include但都依赖了同一个全局单例间接形成耦合。治理办法是尽量减少全局可达状态明确数据归属。3. 主循环与帧驱动引擎的心跳是怎么转起来的游戏引擎和普通软件最本质的区别在于它是帧驱动的。每一帧它都要处理输入、更新世界状态、渲染画面然后循环往复每秒几十次。这一节说的就是引擎的骨架中枢——主循环。3.1 游戏主循环的经典骨架几乎每个引擎都有一个类似下面的主循环结构。我故意用伪代码写因为真实引擎会在这个基础上加大量细节但核心骨架一样while (running) { // 1. 处理操作系统消息和输入 processInput(); // 2. 更新游戏世界逻辑层 updateWorld(dt); // 3. 渲染帧 renderFrame(); // 4. 调度任务/清理帧数据 endOfFrameCleanup(); }顺序看起来简单但细细追究每一步都有大文章。processInput要处理的不只是鼠标键盘还有窗口尺寸变化、设备插拔、系统事件updateWorld之下可能挂着一整套实体组件系统、物理模拟、动画更新renderFrame更不用说光渲染管线就能写一本书endOfFrameCleanup则是很多人会漏掉的关键一步——及时回收临时数据避免内存吃紧。3.2 固定步长还是可变步长这是引擎循环的核心决策主循环里最核心的决策是dt增量时间怎么算。两种流派方案做法优点缺点固定步长每帧模拟一个固定的时间片比如1/60秒通过累加真实时间决定模拟多少次物理稳定、确定性好、容易网络同步逻辑帧率跟不上时会陷入死亡螺旋大量积压模拟可变步长每帧用真实经过的时间作为dt直接更新实现简单、与渲染帧同步、手感和机器性能一致物理稳定性差、不同机器上的模拟结果不一致成熟引擎通常采用混合方案渲染帧率可以浮动逻辑步长尽量固定。典型做法是accumulator模式——把真实时间存起来每攒够一个固定步长就更新一次不够的部分留给下一帧。这样做的好处是物理模拟永远以稳定步长推进不会因为某帧卡顿而崩溃渲染则可以插值显示最新的中间状态保持流畅。伪代码大致长这样const float fixed_dt 1.0f / 60.0f; float accumulator 0.0f; while (running) { // 一帧真实经过的时间不能太大防止窗口拖拽或者断点后跳帧 frame_time min(clock.getDeltaTime(), 0.25f); accumulator frame_time; while (accumulator fixed_dt) { updateWorld(fixed_dt); accumulator - fixed_dt; } alpha accumulator / fixed_dt; renderFrame(alpha); }末尾的alpha就是插值系数用于在两帧逻辑状态之间做渲染插值让画面比逻辑更丝滑。这套方案在帧率不稳的平台上尤其重要——真机发热降频时逻辑照常稳定推进玩家只是看到帧率降低不会看到物理抖动或者角色飘。3.3 时间管理帧率无关逻辑才能跨设备主循环里另一个容易忽视的问题是时间单位和帧率耦合。新手写移动逻辑时最容易犯的错是每帧移动多少像素把距离和帧率绑死。60帧的机器上角色一秒走60个单位30帧的机器上一秒只走30个单位。正确做法是所有运动都乘以dt用每秒速度 × 时间来算距离。这件事看起来简单但一旦代码里混入每帧抠一次的逻辑后面调手感就是在泥潭里摸鱼。我自己的习惯是引擎内部统一用秒作为时间单位所有跟速度相关的量都带每秒语义。编辑器里显示速度用秒序列化也保留秒只有到最底层的插值计算才碰帧率相关的东西。3.4 渲染与逻辑并行之后节奏又变了现在的大型引擎——Unreal、寒霜甚至不少自研引擎——早就不是单线程主循环了。它们把逻辑更新场景裁剪渲染提交GPU执行拆成一条流水线帧N的渲染和帧N1的逻辑是同时跑的否则现代CPU的多核优势根本发挥不出来。但并行化给主循环带来了全新的架构问题逻辑线程写数据、渲染线程读数据怎么保证安全通常有四层手段双缓冲或环形缓冲渲染线程读上一帧的数据快照逻辑线程写当前帧互不干扰。延迟提交逻辑层把所有渲染指令写给一个渲染命令列表渲染线程在几毫秒后消费它。细粒度锁某些共享资源用锁保护但尽量少用。所有权转移数据在某个时间点归逻辑线程某个时间点转移到渲染线程靠流水线阶段保证。对于基础架构来说最重要的启示是主循环不再是简单的while而是一个带同步点的多线程流水线。每个阶段必须明确谁的帧、谁的数据、什么时候同步。这些同步策略会在系列后续讲渲染架构时展开基础篇只需要建立循环可以多线程化的认识。提示写引擎早期别上来就搞多线程主循环。先把单线程固定步长循环跑稳再引入并行渲染。多线程引入的性能问题往往比它解决的问题更早出现。4. 内存管理引擎层面最容易被忽视的架构问题游戏引擎的性能挑战中内存分配经常被低估。尤其是主机和移动平台堆分配和垃圾回收带来的停顿有时候是致命的。引擎基础架构里的内存管理本质上是在回答一个问题内存从哪来用完还给谁如何保证整个过程确定且高效。4.1 默认的new/delete为什么不够用普通应用开发里new和delete随便用没什么问题。但游戏引擎里这招会翻车原因有三分配开销通用内存分配器要处理任意大小、任意顺序的分配和释放内部维护复杂的数据结构一次分配可能耗时几十到几百纳秒。听着不多但一帧里可能有成千上万次分配累加起来就是大开销。碎片化反复分配和释放不同大小的对象堆内存会越来越碎。碎片多到一定程度明明剩余空间总量够却分配不出连续的大块内存直接导致卡顿甚至崩溃。不确定性通用分配器的性能是不稳定的运气好很快运气差很慢。引擎需要每一帧的消耗都可预期不能把行为交给运气。4.2 三类核心分配器帧分配器、栈分配器、池分配器引擎基础架构里最常用的三种自定义分配器帧分配器Frame Allocator专用于一帧用完就丢的临时数据。向系统一次性申请一大块内存帧内所有临时对象往里分配只做整块重置把指针移回起点不做单个释放。因为大部分临时数据生命周期都在一帧以内这种分配器几乎零开销。栈分配器Stack Allocator按LIFO顺序分配和释放适合嵌套作用域的数据。比如加载场景时一批对象先按顺序创建之后按逆序销毁用栈分配器极其高效。池分配器Pool Allocator预分配一批固定大小的槽位分配和释放都是O(1)操作。适合大量同类型对象比如粒子、子弹、声音实例。释放时只是把槽位标记为空闲不涉及系统堆操作。这三种分配器的共同点一次性从系统申请大块内存之后所有分配释放都在自己地盘内完成不碰系统堆。这就是游戏引擎内存表现稳定的主要原因。4.3 对象生命周期谁来new谁来delete必须说清楚架构层面比用什么分配器更重要的问题是谁负责销毁对象。生命周期含糊几乎必然导致内存泄漏或者悬垂指针。我见过太多项目里对象一会儿被A释放一会儿B又释放一次直接崩溃。我的经验是立三条军规谁创建谁释放。模块A创建的对象由模块A负责销毁不允许别的模块顺手delete。对象归属必须明确。一个对象在同一时间只有一个所有者其他模块只拿裸指针或引用不拥有所有权。跨帧任务对象用引用计数或句柄。如果对象生命周期会跨越多个帧比如异步加载的资源、挂在场景里的实体用智能句柄管理所有权杜绝裸指针悬垂。句柄机制值得多说一句引擎里经常不直接把裸指针暴露给上层而是暴露一个索引——比如ResourceHandle是一个整数内部映射到真正的资源对象。如果资源被销毁了句柄仍然存在但查询时发现失效返回错误而不是访问野指针。这是治理悬垂指针的一种行之有效的妥协方案。4.4 内存碎片与性能稳定的关系除了分配器引擎架构还要关心大块内存的排布。比如场景加载时大量Prefab实例、Mesh数据、纹理上传内存的物理排布会直接影响缓存命中率。游戏性能瓶颈很多时候不是CPU计算而是CPU等数据缓存未命中。所以引擎通常会做内存布局优化把每帧都要一起访问的数据尽量放在相邻地址空间。经典例子是实体组件系统ECS核心思想其实有一半就是内存布局——把同类型组件的数据连续存放遍历时一次缓存行就能处理多个实体。不过ECS的详细拆解我计划放在后续的实体与场景管理篇里这一篇先点明内存布局和性能强相关这个大方向。4.5 实战建议先别过度设计内存管理是最容易过度设计的领域。我这里给一个务实的声音如果你的引擎目标平台是PC且规模不大完全可以直接用new/delete加一个内存跟踪工具做泄漏检测就够了。真到需要自定义分配器的时候先从帧分配器入手——它见效最快、实现最简其他分配器按需添加不要一开始就铺开一整片。还有一个经验排查内存泄漏时最好从对象总数增长曲线下手。在调试面板里打印每种核心对象的当前数量跑一段时间看曲线。如果某个对象数量随场景运行单调增长不管它是不是泄漏它一定出了问题。这比盲目用工具扫堆高效得多。5. 模块之间怎么说话事件、命令队列与数据驱动模块划分好了主循环有了内存管理定了剩下的关键问题是模块之间到底怎么通信渲染模块怎么知道场景里多了一个物体物理模块碰撞了怎么通知游戏逻辑这些通信机制本身也是骨架的一部分。通信方式选错了模块解耦就无从谈起。5.1 三种主流通信方式通信方式特点适用场景直接函数调用最快、最直接但形成编译期耦合同一模块内部、依赖方向明确的上下层调用事件系统观察者模式解耦发送者和接收者支持多对多运行时动态订阅跨模块广播通知如物体被销毁场景加载完成命令队列 / 消息总线把调用封装成数据对象延迟执行支持回放/记录/跨线程输入处理、网络消息、撤销重做、逻辑与渲染解耦在一个设计良好的引擎里三种方式通常同时存在高频且依赖明确的数据走直接调用跨模块的通知走事件需要解耦执行的请求走命令队列。5.2 事件系统优点和代价都要看到事件系统的核心价值是编译期解耦。物理系统不需要知道谁在听碰撞信号它只管发出CollisionEvent游戏逻辑如果需要就注册一个监听器。这样物理系统可以独立测试、独立复用。但事件系统有个容易被忽略的代价debug变难了。函数调用是显式的你在调用栈里一眼能看到谁调用了谁事件是隐式的一个事件发出去监听器是谁、数量多少、执行顺序怎么样都不直观。全链路日志是事件系统必需的配套不然出了问题根本查不下去。事件执行顺序也是个坑。多个监听器监听同一事件时如果顺序敏感比如A监听器要先于B监听器处理事件系统必须提供优先级机制否则会出现逻辑对但结果错的幽灵Bug。我在自己的引擎里给事件注册加了priority参数并且强制监听器按优先级排序这个设计后来救了我很多次。5.3 命令队列把调用变成数据命令队列的思路是模块A不直接调用模块B的方法而是构造一条命令包含函数指针/方法ID和参数的闭包放进队列里由消费者在合适时机取出执行。这层间接带来了几个好处跨线程安全逻辑线程把命令写入队列渲染线程消费天然是生产者-消费者模型不用加锁太多。可记录可回放输入系统、网络系统天然适合命令队列录一段命令流程就能实现回放。延迟执行可以控制命令在合适的时机统一处理而不是立即执行。比如在场景加载完成后统一应用所有待处理设置。代价是多一次间接和动态内存分配。设计时要注意给命令做池化——复用命令对象避免每发一条命令就分配一次内存。5.4 实际架构中的组合拳以我熟悉的框架为例模块通信实际是这样组合的输入系统把底层按键事件转换成语义化输入命令写入输入队列。游戏逻辑每帧从输入队列消费命令更新角色状态。角色状态改变时通过事件系统广播ActorMoved、WeaponFired等通知。渲染模块监听这些事件但只更新自己的表现层数据。音频模块同样监听事件播放声音。UI模块通过独立的UI消息系统接收逻辑层推送的数据变更。这条链路里直接调用只发生在逻辑内聚部分和框架层调功能层的接口上跨模块的通知全部走事件跨线程的工作全部走命令队列。模块之间的关系非常清晰我只知道我发出的事件会被处理但我不需要知道被谁处理、怎么处理。注意事件系统和命令队列都是用来解耦的但解耦是有代价的——额外开销和调试难度。架构经验不足的团队容易滑向万物皆事件的极端。实际上内聚的模块内部应该多用直接调用事件只用在真正的模块边界上。6. 从零起步的最小引擎骨架把上面全部串起来讲了一堆原则和概念如果不在代码层面串一遍总感觉隔了一层纱。这一节我给出一个最小可运行的引擎骨架它只有几百行逻辑但包含了一个引擎基础架构该有的全部要素平台层、核心层、模块注册、主循环、事件、帧分配器。你可以把它当胚子往里面不断添加功能模块。6.1 一个极简骨架的设计先看骨架的整体结构。我用C风格的伪代码忽略具体API细节重点是架构形状// 核心层时间系统 class TimeSystem { float deltaTime; float totalTime; public: void advance(float realDt) { deltaTime min(realDt, 0.25f); totalTime deltaTime; } float getDeltaTime() const { return deltaTime; } }; // 核心层事件总线 class EventBus { unordered_mapstring, vectorListener* listeners; public: void subscribe(const string name, Listener* l); void publish(const string name, Event* e); }; // 核心层帧分配器 class FrameAllocator { byte* buffer; size_t offset; public: void* allocate(size_t size); void reset() { offset 0; } }; // 框架层模块基类 class EngineModule { public: virtual void initialize() 0; virtual void update(float dt) 0; virtual void shutdown() 0; }; // 框架层引擎核心 class Engine { vectorEngineModule* modules; TimeSystem time; EventBus eventBus; FrameAllocator frameAlloc; public: void registerModule(EngineModule* m) { modules.push_back(m); } void run() { // 第一阶段初始化所有模块 for (auto m : modules) m-initialize(); // 第二阶段主循环固定步长逻辑 可变渲染 float accumulator 0.0f; const float fixedDt 1.0f / 60.0f; while (!exitRequested) { float frameDt computeFrameTime(); accumulator frameDt; while (accumulator fixedDt) { for (auto m : modules) m-update(fixedDt); accumulator - fixedDt; } renderAllModules(); // 渲染阶段 eventBus.publish(FrameEnd, nullptr); frameAlloc.reset(); // 帧末统一回收临时内存 } // 第三阶段逆序关闭所有模块 for (auto it modules.rbegin(); it ! modules.rend(); it) (*it)-shutdown(); } };这段骨架最关键的几个设计意图我得标出来模块生命周期统一管理所有模块继承同一个抽象接口引擎统一初始化、更新、关闭顺序可控。主循环采用固定步长逻辑更新永远按1/60秒推进渲染帧率可以浮动。帧末统一回收内存frameAlloc.reset()放在每帧最后配合模块里所有临时数据走帧分配器内存管理非常干净。模块间不直接相互引用它们只通过引擎提供的EventBus通信依赖关系集中管控。6.2 一个功能模块长什么样有了骨架往上面挂一个简单的逻辑模块看看效果class GameLogicModule : public EngineModule { public: void initialize() override { // 注册自己的事件监听只依赖事件总线 listener.on(CollisionOccurred, [](Event* e){ cout 碰撞事件; }); } void update(float dt) override { // 每帧推进游戏逻辑临时数据走帧分配器 auto scratch engine.frameAlloc.allocate(64); // ... } void shutdown() override { // 清理自己拥有的资源 } }; // 入口 int main() { Engine engine; engine.registerModule(new GameLogicModule()); engine.registerModule(new RenderModule()); engine.registerModule(new AudioModule()); engine.run(); }这个模块完全不知道自己以外还有什么模块存在。它只做三件事初始化时订阅事件、每帧更新、退出时释放自己的资源。这就是基础架构带来最直观的好处——写新功能模块不需要关心引擎里别的东西只需要遵守继承接口注册这套规矩。6.3 下一步的扩展路径有了这个骨架后续扩展就按部就班了加资源模块文件系统 资源缓存 异步加载管线挂在核心层。加场景管理场景节点树或者ECS挂在框架层和功能层之间。加渲染后端通过RenderModule内部封装API切换对外接口不变。并行化把逻辑更新和渲染提交拆到两条线程用命令队列同步。每次扩展只改自己的模块不影响骨架。这就是基础架构值钱的地方。7. 基础架构设计里的几个回头浪重构的代价与侥幸心理最后聊一个容易被忽略但每个引擎开发者都迟早要面对的话题架构是动态的没有人能第一次就设计出完美的骨架。所以如何对待需要调整基础架构的时刻本身也是架构能力的一部分。我见过很多团队在这样的时刻做出了错误决策。明明模块边界已经明显错了主循环已经撑不住新的需求但还是硬着头皮往上叠补丁原因往往是重构太贵了或者现在跑得好好的别动。这个心理我完全理解但基础架构的问题不会自己消失它只会转入地下然后在你最不想出问题的那个版本爆发。以我的经验判断要不要重构基础架构有三个信号加一个新功能需要改三个以上既有模块的代码。说明模块边界和依赖方向错了不该让新需求震动这么多老模块。改一个低层模块会导致大量无关模块需要重编或者回归。说明依赖控制失效了。模块之间的通信方式开始出现特例。比如反正就这一次直接全局变量传一下出现这种特例时架构的权威性已经在瓦解。如果三个信号出现了两个别犹豫尽早安排重构窗口。基础架构的重构确实痛——它意味着大量代码需要跟着调整但越晚代价越大。我自己的一个教训是曾在一个自研引擎里硬扛着管理器乱成一团的架构维护了大概一年每次加新系统都要四处打补丁最后实在受不了花了一个季度做了一次大清理。如果当时早点动手那个季度本可以用来做真正的功能迭代。所以说基础架构不是搭好了就一劳永逸的东西它是需要持续照看的公共设施。骨架的稳定不是靠它永远不变而是靠变化发生时你知道规则是什么、边界在哪里、怎么去改而不伤害整体。把这一篇当作整个游戏引擎架构深度解析系列的地基是个不错的选择——骨架立住了后面讲渲染、讲物理、讲资源流、讲关卡编辑器和工具链才都有地方挂。
返回列表