先说明一下我对这篇文章的定位:它不是一篇教科书式的百科讲解,也不是某家引擎的官方文档翻译。我写的是自己做引擎开发这几年,从团队协作到底层代码之间反复踩出来的认知。标题里带着"001",说明这是个系列,第一篇先把"团队分工"和"底层架构"这两根主梁立起来,后面的文章再往渲染、资源、工具链这些具体方向深入。
很多人有个误区,觉得引擎架构就是技术总监画几张框图,团队分工是人事和项目经理该操心的事,两者没太大关系。但真正做过引擎组的人都知道,这两件事根本就是同一个问题的两面:你定了什么样的架构,团队就得按什么方式分工;团队怎么协作,又反过来逼着架构一点一点演变成现在的形状。所以这篇我打算把这条链路的起点讲清楚,适合刚入行两到三年的客户端程序、想从业务逻辑转引擎方向的同学,以及需要和引擎组打交道的策划和TA们阅读。读完你至少能搞明白一件事:一个引擎里那么多模块,到底谁该管什么,底层代码又是怎么把几十个人放在同一个进程里不乱套的。
1. 团队分工:引擎开发从来不是一个人的孤军奋战
1.1 一个完整引擎团队里到底有谁
先说一个现实:商业引擎团队和自研引擎团队的规模差距非常大。Epic、Unity那种几百人的引擎团队,和你们公司里五个人维护一个内部引擎,分工颗粒度完全不同。但剥开规模差异,核心角色其实就那么几种。
第一类是核心框架组,也有人叫底层组或基础组。他们负责的东西听着最无聊但最重要:内存分配器、容器库、字符串库、文件系统抽象、线程与同步原语、模块加载与卸载、日志系统。这个组的人都得有"洁癖",因为你写的每一个接口都会被全引擎最挑剔的渲染组和物理组天天调用。他们在设计容器接口的时候就得想清楚:要不要支持自定义分配器?迭代器失效的规则是什么?在debug和release下行为是否一致?这些看起来不起眼的问题,在引擎里全都会变成大事故。
第二类是渲染组。这个组通常人最多,也最容易出明星工程师。他们管的事从CPU侧的视锥裁剪、遮挡剔除、渲染图(RenderGraph),到GPU侧的着色器、材质系统、后处理、光照阴影。渲染组几乎是所有引擎里沟通成本最高的组,因为他们天上的事情也要管、地下的事情也要管。
第三类是工具链组和资源管线组。这两位经常合并,但我建议在稍大一点的团队里把职责分开。工具链组做的是编辑器、Inspector、Debugger、性能分析面板这类"程序自己在用"的玩意儿。资源管线组管的是美术的DCC软件(Maya、Blender、Substance)到引擎可读取格式的全套流程:格式定义、导入插件、压缩策略、版本升级、增量打包。
还有一类是平台适配组。做PC单机游戏可能感受不深,但只要上了PlayStation、Switch、Xbox或者各种乱七八糟的安卓机型,你就必须有一个小组专门负责硬件平台和系统SDK的差异。比如Sony的异步文件读取API、Nintendo的保存数据机制、安卓厂商的GPU驱动bug清单,这些都是平台组的日常。
这几类角色的存在,本身就是在回答一个问题:为什么引擎开发不能在用git的时候各写各的?因为渲染要改一个资源格式,管线组就得跟着改导出工具;工具链给美术做了个新功能,底层就得提供新的RPC通道。模块之间的依赖关系一旦建立,团队协作的流程也就固定下来了。
1.2 模块归属与接口协议:谁说了算
每个组都有自己的"辖区",但引擎的痛点从来不是辖区内部写什么,而是辖区之间的接口。
举个例子,渲染组要做一个新的材质属性,前端需要暴露给美术一个"次表面散射强度"的滑条,后端需要把它打包进材质常量缓冲。这时候工具链组说:Inspector上我统一用动态面板生成,你只要在注册表里声明一个属性就行。渲染组说:不行,我的属性需要依赖另一个属性的值动态计算,你的动态面板支持吗?两边在MR里改来改去,最后还得架构师出来拍板:属性声明用Gamma统一接口,导出端和运行时端解析同一份Schema。
这种"谁说了算"的问题,靠行政级别解决不了,得靠架构上的原则。我所在的团队定过几条规则,我觉得可以公开分享:
- 跨模块的数据结构定义在哪,接口合同就归谁管。比如渲染资源描述结构放渲染层,工具链可以读但无权修改。
- 依赖方向只能单项流动。运行时层不允许依赖编辑器层;编辑器层可以依赖运行时层,但必须通过公共SDK接口。
- 任何公共接口的变更,要提前两个迭代周期公告,并且提供迁移工具,不给迁移工具就不许改。
这几条听着平淡无奇,但我见过太多团队翻车,都是因为"顺手改一下公共头文件"引发的连锁编译错误。一个引擎项目里,公共头文件是比核心代码更需要防守的部分。
1.3 技术美术和引擎组的协作边界
技术美术这个角色这两年越来越重要,但和引擎组的边界反而越来越模糊。TA往往会写Shader、做材质库、调渲染效果、做PCG(程序化生成)工具,这些工作在有些团队里会被算成"引擎组的事",在另一些团队里会被算成"美术组的事"。
我的建议是:凡是直接读引擎API、需要编译原生代码的工作,都归引擎组代码评审;凡是脚本层面、节点面板、材质图层的产出,归TA负责但走"引擎侧评审+美术侧确认"双通道。这样说可能有点保守,但引擎架构最怕的就是出现一堆绕过公共接口的专属后门。
我第一次见到那个灾难现场是在一个Demo项目里:TA为了让某个草地效果更亮,直接在Shader里写了个#ifdef THIS_IS_GRASS的特判,三个星期之后没人敢动材质系统,因为一改草地的渲染效果就崩。从那以后,我坚决要求:任何引擎代码(Shader也是代码)里出现的分支特判,必须在评审时说明理由,并且有一条明确的清理计划。你可以在这里写特判,但你必须同时写一个任务卡告诉后面的人"如果完善了这套功能,记得删掉这里"。
2. 底层架构的精髓:把变化隔离在正确的位置
2.1 为什么引擎一定要做平台抽象层
在给新手讲引擎架构的时候,我最喜欢先讲平台抽象层。因为它最无聊,但最能让新人理解"隔离"这两个字的分量。
你在Windows上用CreateFile读文件,在Linux上用open,到了主机平台上可能得用SDK提供的异步IO接口。如果引擎代码里到处散落着#ifdef _WIN32或者#ifdef __APPLE__,看起来也能跑,但每一个新平台加入都意味着全代码库的搜索和修改。平台抽象层做的事情,就是把这些操作系统差异压缩到一小撮文件里,对上层只暴露一个稳定接口。
设计平台抽象层有几个关键选择值得留意。第一个是文件系统:不要直接返回std::ifstream,而要返回引擎自己的ReadFileHandle对象,因为游戏里的读取往往不是一次同步读,而是需要异步排队、缓存命中、跨平台路径转换。第二个是线程:不要直接用std::thread到处建线程,而是通过引擎的线程池封装,因为你在主机平台上可能需要把某几个核心专门留给音频或渲染。第三个是内存映射:不是每个平台都有mmap,你要做好在某个平台上只能走malloc兜底的准备。
平台抽象层本身不该膨胀。如果这个层里的代码超过引擎代码总量的百分之二三,你就得怀疑自己是不是把业务逻辑也塞进去了。这个层的职责是"翻译"而不是"实现",任何复杂的算法都不该出现在这里。
2.2 核心运行时:内存分配器、句柄与引用
越过平台层,往上是核心运行时。我在这一节里挑三个最值得新手搞懂的东西:内存分配器、句柄系统和模块生命周期。
先说内存分配器。游戏引擎很少直接用new和malloc,原因不只是性能,还有分配行为的不确定性。引擎代码里通常会分几类分配器:堆分配器(慢但通用)、线性分配器(一帧里快速创建然后整体释放)、池分配器(固定大小的对象反复创建销毁)。渲染帧里海量的小对象,比如每帧提交的绘制命令、变换矩阵、光源数据,如果全部走通用堆分配,CPU上浪费的时间会大得惊人。
用线性分配器的时候有个特别爽的场景,就是帧尾统一回收:FrameAllocator::Reset()一调用,这一帧分配的所有临时数据一次性死亡,没有任何泄漏风险。代价是你不能在线性分配器里长期保留跨帧对象。我见过新人把贴图上传用的临时缓冲区放进了帧分配器,结果下一帧刚开始就被覆盖,变成了随机像素。查这个bug花了一整天,最后定位到挂起分配器里的指针,那种心情真是难以形容。
然后是句柄系统。引擎里为什么不用裸指针到处引用?因为对象可能被销毁啊。你在update里拿着一个指向GameObject的指针,但另一条线程刚好把这个GameObject删了,下一次你就会访问野指针。句柄的经典实现是一张全局句柄表:句柄是一个包含索引和代号的32位整数,查表得到实际指针。当对象销毁时,表中对应的代号加一,旧句柄再访问就直接失效。这比裸指针安全得多,性能开销也就是一次数组访问。
句柄系统有一个细节值得关注:代号是一次性递增还是循环复用。循环复用的代号如果位数太小,可能出现旧句柄恰好匹配新对象的情况,产生"幽灵引用"。我见过一个项目在这上面翻过车,当时调试出来的现象是:某个怪物明明已经死亡了,但它的攻击特效依然会偶发触发。最后查了很久才确认,是句柄代号循环了一圈,新怪物复用了同一个代号,过期逻辑判断"句柄相等"就放行了。从此我建议句柄的代号至少要32位,别在这上面省空间。
2.3 模块生命周期与启动流程
引擎启动时的先后顺序,决定了很多上层功能能不能正常用。我的经验是可以把这个顺序固化成一张表:平台层初始化 -> 内存系统 -> 日志系统 -> 文件系统 -> 线程池 -> 资源管理器 -> 渲染设备 -> 场景管理器 -> 脚本系统 -> 游戏模块。
为什么要这么严格?因为比如资源管理器在加载时才需要文件系统,如果文件系统还没起来,你加载一个纹理就会在空指针上崩溃;脚本系统如果加载太早,没有场景管理器提供的场景容器,脚本里的GameObject查找就找不到对象。这个顺序在代码层面通常体现为一个EngineInit()大函数,里面按序号逐个调用模块初始化。
我踩过的坑是在商业化项目里加"模块热重启"。当时为了减少玩家加载等待,希望"断线重连网络游戏"时只热重启逻辑层,不重启渲染层。结果发现逻辑层和渲染层之间藏了好多隐性共享状态:某个光照探针管理器挂在渲染层上,但它的初始化依赖逻辑层创建的关卡配置。解决办法后来是把所有跨层依赖都收归到上下文对象(Context)里,由Context负责按依赖关系重新初始化。从那以后我强烈建议:所有模块的对外状态,要么放在自己内部,要么放在Context里,绝不允许访问另一个模块的私有单例。
3. 渲染主链路与资源管线:一帧画面靠什么跑起来
3.1 从CPU提交到GPU成像:一帧的旅程
引擎架构里最能让人产生"豁然开朗"感觉的,就是搞懂一帧是怎么从代码跑到屏幕上的。我们先简化一下:CPU这边跑逻辑和渲染提交,GPU那边跑光栅化和着色。CPU和GPU之间通过一个命令缓冲区(CommandBuffer)通信,CPU往里填渲染指令,GPU按顺序消费。
在实际引擎里,这个流程会被多线程化。一个典型的帧循环可能是这样的:逻辑线程Tick游戏状态,渲染线程根据逻辑产出的数据构建RenderGraph和绘制列表,加载线程异步流送纹理和网格,GPU在几十毫秒后慢慢把前面攒下来的工作做完。这里最大的麻烦是CPU不能等GPU,否则帧率链就断裂了。所以引擎都会开一个"帧延迟"机制:CPU上的渲染线程比逻辑线程落后2到3帧,这样GPU始终有活干,不会因为逻辑慢了就无辜空转。
RenderGraph是这几年引擎架构绕不开的概念。它的思路是:渲染线程不直接往GPU命令缓冲里写具体指令,而是先构建一张"我这一帧需要用哪些渲染Pass、这些Pass之间的依赖是什么"的图。等这张图建完,系统再做自动优化:合并可以合并的Pass、发现无用加载就裁剪掉、把依赖关系组织成最优执行顺序。我当初从传统渲染流程迁到RenderGraph时,一开始觉得这纯属过度设计,但真正跑起来后,发现它能解决一个特别实际的问题:跨Pass的资源复用终于有了统一规划,而不是靠程序员手动接线。写RenderGraph时一定要记得,图构建阶段不要干任何GPU活儿,否则你就是在拿CPU时间给GPU性能做交易。
3.2 资源加载、依赖图与热重载
引擎里第二个容易被低估的架构层是资源管理系统。很多项目死在"资源加载卡顿"这个病上,老板打开游戏,进度条转了半分钟,玩家已经流失了。资源系统要处理的不是单个文件读取,而是资源之间的依赖图:一个关卡引用了几十种模型,每种模型引用了若干个材质,每个材质又引用了纹理和Shader。你不能平行地把所有文件一遍读取进来,因为依赖的关系头尾不一。
常见的负载控制方案是这样做的:先扫依赖图,确定加载队列的拓扑顺序;然后按优先级把队列拆分成分批任务,每批任务在时间片内完成,避免单帧里卡死超过几十毫秒;已加载的资源放进缓存池,同一个资源被十次引用时只需要读一次盘。这里涉及一个架构上的取舍:资源加载是异步优先还是同步兜底。我的建议是:UI层和极端紧急关键路径上可以用同步加载,但游戏世界物体的加载一律走异步队列,这样进度条或者流送界面的表现才有优化的空间。
热重载是另一块容易翻车的领域。做工具的时候你会希望改了代码或资源快捷键一按就看到效果,不用重启整个编辑器。但热重载的难点在于:如果资源对象的旧实例还在场景里,新数据进来你怎么迁移?是替换所有引用还是重建对象?我在一个项目里做过多级资源热重载:资源本身热重载、材质参数热重载、Shader热重载。三级越往上风险越大,因为Shader的变更影响到GPU缓存里的PipelineState,牵一发动全身。
给所有接入资源系统的模块立过一条规矩:资源管理器只保证资源数据是新的,不保证场景对资源的引用关系是新的。每个模块自己负责监听资源版本变化并改造自己的内部状态。这条规矩让我后来省下了无数个"刷新了材质但模型颜色没变"的bug。
3.3 数据驱动设计:从硬编码到配置化
早期客户端开发特别喜欢把数值写死在代码里——怪物血量、技能倍率、掉落概率全是const int。后来发现策划改个平衡要拿着需求单找程序员,程序在忙着改bug,两边互等。数据驱动设计解决的就是这个问题:把游戏设计相关的所有数值、事件、表现配置化为外部数据,引擎本身成为"读取数据+解释数据"的执行器。
资源管线在这里的作用就体现出来了。美术导出一个带有碰撞信息的FBX文件,引擎导入器读取后生成一个"碰撞网格资产";策划在Excel里调整掉落概率表,工具链脚本把Excel转成引擎数据格式;程序员只需要定义数据结构,告诉引擎这些数据从哪里加载、怎么反序列化、怎么接入逻辑。这套流程看似平淡,但它把引擎架构从"写下所有流程代码"变成了"描述数据如何解释和运行",这其实是引擎架构现代化的一个关键转折点。
数据驱动设计也有坑:配置项太多会失控。一个技能在Excel里有50个参数,策划真正需要的可能只有10个,剩下的20个是"技术内部折衷"。我的经验是,配置的Schema和引擎数据结构共享一份模板,导出工具和运行时解析都从这份模板生成,这样配置项和代码字段永远不会脱节。这个模板本身也要做版本管理,否则老版本资源在新引擎上会解析错位,那是所有资源系统的噩梦。
4. 多线程与ECS:架构演进的真实驱动力
4.1 从GameObject到ECS:为什么缓存友好这么重要
十几年经验的引擎开发者都会经历过一个阶段:引擎里到处都是GameObject和继承深度五六层的类,每个对象自己更新自己的状态。这种写法在项目早期非常舒服,但到了场景里有几万个对象的时候,性能开始极速恶化。原因并不在于"每帧循环里做的事情太多了",而在于内存访问模式太差。
解释一下:CPU从内存读数据的时候,是按缓存行(通常64字节)成块读取的。如果10000个敌人对象散落在物理内存各个角落,你遍历它们时,每读一个对象就要从内存取一个新缓存行,CPU大部分时间都在等内存。而如果对象数据按紧凑数组存放,遍历时每个缓存行能装下一批对象的字段,计算单元就能吃饱。这个差距在密集物理、大规模人群、大量存活单位时,可能是几十倍的效率差异。
ECS(Entity Component System)之所以在近十年大流行,本质上是为了把"逻辑上属于同一个实体的数据"拆开,改成"按数据类型分组存放"。所有位置放在一个数组,所有速度放在一个数组,系统更新时访问的是两个线性数组,缓存命中率极高。另一个好处是并行友好:不同系统读不同数组,天然可以丢到多个线程上跑而不用大量加锁。实话说,这是我从"面向对象引擎"迁到"数据导向引擎"后感受最明显的变化。
4.2 ECS不是银弹:引入它的时机和代价
但我要给ECS泼一点冷水。网上太多文章把ECS吹成了现代引擎的必选项,好像不用ECS你就是老古董。实际上,ECS对项目类型的适配是有选择的。如果你的游戏是动作冒险、大型开放世界、竞技射击,实体数量和系统复杂度都不低,ECS确实能带来显著收益。但如果你的项目是回合制卡牌、休闲小游戏、强叙事互动电影,实体数量也许只有几百个,你花大功夫改造ECS,换来的性能收益几乎感知不到,增加的调试成本却实打实。
ECS还有一个让团队崩溃的隐性代价:逻辑的可调试性。在传统GameObject写法里,你可以在Inspector里看到每个对象的全部字段。到了ECS,数据分散在几十个组件数组里,游戏运行时要定位一个"速度异常的敌人",你得上调试工具联合查看好几个系统里的组件数组片段。我们项目组为此专门写了一个ECS调试面板,按实体ID拉取它的所有组件数据,才勉强恢复了一部分直观性。
所以我的建议是折中路线:把"高频、海量、逻辑简单"的那部分,比如移动、碰撞查询、动画采样结果缓存,切成ECS系统;而把"低频、复杂、有强状态机"的那部分,比如Boss的AI决策、NPC对话、叙事状态,继续用传统的状态机或脚本对象实现。引擎核心架构只要预留好数据直入口,两种范式是可以共存的。硬把所有逻辑塞进ECS,只会让代码变得奇形怪状。
4.3 线程模型设计:谁在你的游戏里并排开车
多线程不是"开得越多越快"。我先给个经验数字:引擎里真正常驻的线程种类,通常控制在10到15个之间。逻辑主线程、渲染提交线程、音频线程、资源加载线程池(3~4个)、网络线程、物理线程、Job系统的工作线程(根据核心数调整)。听起来人不少,但它们各有各的任务,重叠的地方其实很少。
真正的麻烦来自Job系统。你希望把逻辑更新、动画采样、蒙皮计算、粒子更新这类可以并行的任务分发到所有核心上,这时就会出现海量的并发任务。如果每个任务都要访问共享数据,加锁的成本会比任务本身还高。架构设计上要规避这种局面:Job之间通过数据依赖而不是锁来同步。典型模式是"先写后读调度":任务A产出数据,任务B声明依赖任务A,Job系统按依赖图调度B,等A完成后自动触发B。因为不用锁,这种模式跑得特别顺。
我见过一个反面案例:一个项目为了"稳妥",在每个Job里对共享的碰撞查询接口加了锁,结果8核机器上跑出了2核的效果。后来把碰撞接口改成每个帧分配一份只读的加速结构,查询Job通过只读访问它,彻底去掉锁,性能立刻上去了。这件事给我的教训是:多线程架构里,先想清楚数据的读写归属,再决定线程怎么调度,顺序反了就是白忙一场。
5. 新人接入引擎时最常见的坑与排查心得
5.1 配置环境与构建:第一天就卡住的三个细节
引擎开发新人的第一道坎往往不是看源码,而是把引擎跑起来。我观察过很多次新手接入内部项目时卡住的情况,最典型的有三个。第一个是第三方依赖和SDK版本不一致。引擎通常依赖一堆二方库和三方库:渲染API的SDK、压缩库、JSON库、物理引擎、音频库。每个库都有版本号,新人直接拉最新版通常会导致接口对不上。解决方案很简单:第一次构建前,严格对照项目根目录的依赖清单文件逐项核对版本,不要用"顺手更新一下看看能不能过"的思路。
第二个问题是IDE和编译器的参数不对。Windows上常见的坑是字符集设置不统一,引擎代码里处理路径字符串时用了UTF-8,但IDE默认用的是本地代码页,路径里有中文时就会乱码。这不是逻辑错,纯粹是环境错。如果是在Godot这类开源自研引擎里遇到乱码,排查方向也一样:先看源码文件和编译选项的统一编码,再看控制台的代码页设置。第三个坑是增量构建的脏缓存。改了一个公共头文件后全量重编一遍大概是半小时,新人心里着急,CTRL+S之后就直接跑,结果用了五分钟前编译的旧二进制,还没反应过来是构建没触发。
5.2 调试器与Profiler:别拿眼球当工具
引擎开发最大的敌人是性能回归。我在团队里反复强调:不要用"感觉变卡了"来判断性能问题,你用眼球看一帧的卡顿,怎么知道是CPU慢还是GPU慢?得用数据说话。这也是架构给工程流程带来的价值:CPU和GPU两端的耗时数据都可以按模块拆分出来,对比基准版本和本次修改的帧耗时曲线。
给新人的排查思路框架大致是这样:先看是CPU卡还是GPU卡。GPU卡,看DrawCall数量和渲染Pass的耗时分布;CPU卡,看逻辑更新、渲染提交、物理仿真、资源加载的并发耗时。确认瓶颈模块后,再用调试器挂到线程栈上看函数占比,而不是瞎猜。这个方法对线程模型复杂的引擎特别重要,因为在主线程上看到的耗时,可能大部分都是在等另一个Job写完数据。
另一个经验是,一定要保留一份"性能基准场景"。这个场景不需要很复杂,但必须包含典型数量的物体、几种代表性材质和光照组合。每次重大架构改动后,跑一遍基准场景,对比帧时间曲线。不做这一步,等两个月后性能跌了一半,你根本想不起来是哪个改动造成的。这个教训我交过学费,真心建议所有引擎组都做。
5.3 当封装的资源系统遇到诡异Bug,从哪里下手
资源系统相关的bug是最折磨人的,因为它的表现往往很玄学:同样的场景,有的机器卡,有的机器不卡;模型随机变成粉红色;贴图偶尔撕裂。这类问题我一般按以下几个方向排查:
- 确认是不是资源版本不一致。加载进来的资源和美术最新导出的版本比较,内容是否一致。
- 确认是不是异步加载时序问题。同一份资源同时被两个模块加载,第二个模块拿到的是第一个模块正在修改的中间状态吗?
- 确认是不是缓存失效问题。资源校验的哈希没有更新,导致修改后的资源被误认为还是旧版。
- 确认是不是平台IO差异问题。主机平台和PC的IO特性差异很大,同一套代码在PC上看着没问题,上主机就可能因为IO排队超时加载失败。
最后那个情况是我们上主机平台时最痛苦的一轮联调。PC上磁盘IO快,所有资源的异步加载都像本地一样秒回;到了主机上,整块的读取速度尚可,但小碎片读取就变得极慢。后来通过平台抽象层增加了"读取优先级"接口,把关键资源立到高优先级队列,把背景音乐和普通贴图压到低优先级,才算稳下来。所以架构这种事,往往不是等出了问题再修,而是你要在一开始就知道你的目标平台有哪些脾气。
6. 架构师看的不是代码,是系统在压力下的行为
最后再分享一点我个人的体会。很多人觉得做引擎架构就是设计一套完美的类图,只要把模块划分成正确的形状,一切就顺理成章了。但我见到的真实情况是:架构评估从来不看静态的类图,而看系统在高压力下的行为。场景里有十万个物件时,逻辑线程和渲染线程的负载比是稳定的,还是忽高忽低?资源加载队列里堵了多少个请求,会不会把启动卡死?物理系统在大量碰撞事件时,是否会挤占动画更新的线程时间片?这些问题只靠画图是回答不了的,你得让代码跑起来,用工具量,再根据量出来的数据调整分工、调整依赖、调整线程配置。
像ECS、RenderGraph、数据驱动资源管线这些大方向,它们之所以成为主流,不是因为业界跟风,而是因为在压力测试下,它们确实让系统的行为更容易被预测和并行化。而团队分工和架构层之间的对应关系,也是一个动态平衡的过程。模块边界清晰了,组与组之间的交流成本会下降;依赖方向明确了,代码评审的速度也能变快。万一你不幸加入了一个架构混乱的团队也别怕,建议从本地建立一张"模块依赖地图"开始,先把现有引擎里谁依赖谁画出来,再逐步推动改动。所有的重构,都是从一张准确的现状图开始起步的。
下次有机会的话,我会接着写渲染图的具体实现细节,还有资源管线的版本管理怎么设计。那两块都是能单独撑起两三篇文章的硬骨头,咱们慢慢啃。