AI时代,我为何坚持手写C++游戏引擎?

发布时间:2026/9/14 3:59:11
AI时代,我为何坚持手写C++游戏引擎? 生成式 AI 重构一切的时候我为什么还在为自己的引擎从零写代码公司里最近开了好几轮会主题无非是怎么把生成式 AI 接进产品管线怎么用 AI 批量产出美术资产、生成关卡配置、自动写 UI 脚本。同事们的项目推进得飞快昨天还在跑 demo今天已经准备上生产。我坐在角落每次轮到我发言讲的是这个月我们的引擎又新增了哪个底层特性。不是刻意唱反调。GitHub Copilot 和 Claude 我也在用而且用得不少。但手头这个游戏引擎项目我从两年前就开始写到现在依然坚持用 C一行一行地写。有人问都生成式 AI 时代了连小游戏都能让 AI 几秒钟生成一个你还在手搓引擎图什么这种话听多了我反而把这件事想得更清楚了。这篇就把我的真实经历、技术选型逻辑、踩过的坑以及 AI 在这个过程中到底扮演什么角色一次性说明白。先说结论AI 生成的是结果而我需要的是对结果的掌控力。游戏引擎是掌控力的最终载体C 是这个载体最底层的表达方式。这两个东西在可见的未来都不会被 AI 替代反而会因为 AI 的存在而变得更值钱。1. 在 AI 重构一切的时代一个手写引擎的局外人经历了什么1.1 当同事都在跑 AI Demo时我在修的却是一个崩溃日志上个月有个场景让我印象很深。团队里用 AI 工具把游戏场景生成从两天缩短到了两小时美术同学那边一片欢呼。而我在同一周做的事情是花了一整天定位一个偶发的崩溃最后发现是纹理流送模块里一个资源生命周期管理的问题——某个纹理已经释放了但渲染队列里还有一帧在引用它。这种对比非常有意思。AI 在生成内容这件事上的确把我秒成了渣——给它一句提示词它能产出我会花一下午才能做完的东西。但崩溃日志不会因为说这段代码是 AI 写的就变得更好定位。栈回溯、寄存器状态、内存布局、并发时序这些东西不关心你是人写的还是 AI 写的它们只关心你写的代码是不是真的正确。后来我用 AI 工具帮我复盘了那次崩溃它倒是在几分钟内给出了一条有效的排查思路检查纹理的引用计数逻辑里是否遗漏了对渲染管线中正在使用的资源的保护逻辑。我沿着这个方向去查果然发现了问题。但前提是我得首先能理解它给出的思路能把它翻译回我引擎的架构里判断它是否符合我的设计约束——这套约束用一句话概括就是渲染线程绝不能去访问一个生命周期已经被逻辑线程终结的资源。这件事让我想明白了一个很重要的点AI 能帮你分析问题生成代码但它没法替你做工程判断。而工程判断恰好是把引擎这种复杂系统做好的核心能力。1.2 热搜词里的程序员正在被重新定义但底层规律没有变我发现热搜词里关于程序员的内容越来越焦虑了什么程序员外包程序员转行做什么好经常挂在榜单上。我朋友圈里也确实有朋友在认真考虑要不要跑去做 AI 提示词工程。我的观点是程序员这个身份在实践中被重新定义这件事是客观存在的但被重新定义的是执行层不变的是判断层。过去判断力体现在架构设计、复杂逻辑拆解、系统瓶颈分析现在判断力多了一个维度——怎么有效地跟 AI 协作、怎么判断 AI 生成的代码是否靠谱、怎么在 AI 生成的方案与工程现实之间做取舍。C 手搓引擎这件事恰好同时锤炼了这层判断力的两个侧面一方面你必须对底层机制有足够深的理解才能判断 AI 给的方案是否经得起推敲另一方面你手上掌握的东西越接近最终被依赖的那一层你的不可替代性越强。画皮画骨难画魂AI 能画出很多项目的皮但魂需要有人真正把它雕刻出来而手写代码正是雕刻的基本功。2. 为什么偏偏是 C又为什么偏偏是游戏引擎2.1 选 C 不选 Rust 不选 Zig的真正理由其实我最早也在 Rust 和 C 之间犹豫过。那会儿 Rust 在系统编程圈已经很热了内存安全、无 GC、模式匹配写起来很舒服。但最后我还是选了 C理由可能跟你想的不太一样。第一个理由是生态惯性。游戏引擎领域几十年的技术积累、中间件、平台 SDK、引擎工具链大部分都是 C/C。我接一个平台特性查文档时最有价值的内容往往还是 C 写的虽然现在很多新项目的 README 已经开始默认用 AI 生成底层头文件注释、API 参考文档里的语义说明翻开历史版本沉淀下来的依然以 C 为主。选 C 意味着我可以直接站在这个巨人肩膀上不用花时间做语言层面的适配和封装。第二个理由是我需要的特性C 到现在依然是最平衡的。我要精确控制内存布局要在多线程环境下无惧数据竞争的裸奔快感当然要自己负责要能在需要的时候直接嵌入汇编级优化还要能拿到最小的二进制体积和最快的启动速度。这些事情不是别的语言做不到而是 C 给了我最直接的路径没有额外的运行时抽象挡在中间。第三个理由是我私心很喜欢的C 标准演进的速度和方向让我觉得这个语言还活着。C20 加了概念、协程、模块C23 把 std::expected 这类东西也拉了进来。虽然我们项目因为要兼容移动端工具链暂时卡在 C17但这条路越走越宽而不是越走越窄。后面我单独说一下我们项目实际用的 C17 特性集以及踩过的坑这里先不铺开。2.2 游戏引擎这个项目为什么值得手搓很多人理解不了手搓引擎这件事一个很核心的问题在于现在现成的引擎那么多Unity、Unreal、Godot哪个不比你自己写的强对它们每个都比我强一万倍。但我做这个项目的目标压根不是做出一个商业级引擎我把它看成三件事。第一件事是彻底消化计算机图形学和游戏引擎架构的知识。读了十年图形学文章不如自己写一次渲染管线来得透彻。矩阵变换、视锥裁剪、渲染状态切换、资源生命周期这些概念在书里是知识点在手搓引擎里是每天都要面对的脾性。第二件事是给我自己的游戏想法提供自由度。用现成引擎时我经常因为引擎的框架而改变自己的设计——想做个体素游戏引擎的流送方案可能不支持想做个大世界Unity 的包体结构可能要绕很多弯。自己的引擎想怎么做就怎么做它是一张白纸。而自由度对独立开发者来说是比性能更重要的竞争优势。第三件事可能最实际——它的价值会被 AI 放大。现成引擎已经被 AI 训练数据覆盖得非常充分了你让 AI 写一段 Unity 的 C# 脚本它写得比大多数初级程序员都好。但你让它写一段专门为你的引擎定制的渲染队列管理逻辑它没有你的上下文写出来的东西基本没法直接用。这反而让自己写的引擎成了 AI 时代最能拉开差距的技术资产——因为你的代码库是私有的、独特的、不在大模型的预测分布里。3. 我的引擎架构拆解以及构建过程中的关键决策3.1 从一开始就选对的核心模块拆分项目启动时我先画了一张非常简洁的架构图把引擎分成四个核心模块运行时核心、渲染后端、资源系统和工具层。这张图两年后回头看依然是稳的好的架构应该能扛住你在当初看不到的未来需求。运行时核心负责底层的东西内存分配我直接用了一个可替换的自定义分配器方便以后做内存池优化、平台抽象层后面要有 Android、Windows、macOS 三端、事件系统和数学库。渲染后端实现了 Vulkan 1.2 的一个最小可用子集以及一个软件渲染回退。如果说从长远来看要有更好的跨平台方案当时其实设想过把图形 API 层面单独再抽象一层让平台侧只关心窗口和上下文创建。后来实际接入 macOS 时这个抽象确实帮我省了很多事。资源系统是引擎最容易在后期爆炸的地方。大多数引擎在资源加载上踩过的坑往往不在加载慢而在生命周期管理乱。资源A被场景引着场景又被另一个异步加载的资源引用着卸载顺序不对纹理就黑屏。我在这方面一开始就做了一个设计决策所有资源引用走引用计数不提供手动释放的能力。开发者拿到的只有 ResourcePtr 这种智能指针引用计数归零就自动卸载需要缓存就持有缓存。这个决策一开始让资源管理代码复杂了一些但两年下来它帮我避免了大概七八个最头疼的崩溃类型杠杆率实在太高了。我们的 ECS实体组件系统模块借鉴了很多成熟方案的设计思想。我曾参考过 EnTT 的实现但那玩意儿直接引进来太重了而且它有它自己的一套性能假设不一定适配我的场景。于是我用了一个比较朴素的方式用数组存储组件用稀疏表做实体到组件的索引组件增删只在重建阶段做避免在迭代过程中改容器结构。这套简化版 ECS 在几千个实体场景下的性能已经足够碾压我的游戏需求了。3.2 渲染线程和逻辑线程打交道的方式藏着最多坑多线程渲染是引擎性能的关键也是最容易出 bug 的地方。我的方案是一个经典的数据双缓冲模式逻辑线程把一帧的渲染命令写进一个命令缓冲区渲染线程在收到帧结束信号后交换缓冲区并把上一帧的缓冲里的命令提交给 GPU。这套方案从《游戏引擎架构》这本书里学来的实际落地时有两个容易漏掉的细节。第一个是命令缓冲区的分配策略。你不可能每一帧都 new 一个 vector 然后在帧结束的时候销毁它那样会造成严重的分配开销和缓存 miss。我的做法是预分配一组容量足够的命令缓冲区帧结束交换时只交换指针写完的那一帧不立即清理而是下一轮循环复用的时候再做条件清空。清空不能是 clear 了之而是遍历命令对象逐个调用析构函数如果有必要再重置容器大小。第二个是逻辑线程和渲染线程的交付时机。逻辑线程写完命令之后绝不能直接唤醒渲染线程让它马上执行——因为渲染线程可能还在渲染上一帧你如果直接把命令缓冲区用一个新的地址给过去那根据交换语义也许还是安全的但如果你还复用了同一个缓冲区地址那么旧的渲染可能读到正在被逻辑线程写入的数据。这个竞态条件非常隐蔽我花了差不多三个晚上才在一次偶发的渲染错乱中定位到它。我现在每帧的组织结构就是这样逻辑线程执行系统更新写完本帧命令后用原子变量发布帧序号1渲染线程自旋等待帧序号变化后取走新缓冲区。虽然听起来有点简陋但配合帧尾同步信号量整个流程是非常稳的。3.3 选型细节为什么用 CMake 而不是其他构建系统C 社区构建系统之争可以写一本书了我是站在 CMake 这边的。理由很实在它是当下唯一能在 Windows、macOS、Linux、Android、iOS 上统一描述的构建方案而且 IDE、生成器生态最成熟。版本选型上我用了 CMake 3.28开启了一些比较新的特性比如对 Intel LLVM 和 MinGW 工具链的改进支持具体版本特性我用 mac 自带工具链时遇到过兼容问题后面单独讲。项目里构建配置的一个关键点是生成器优先级策略在 macOS 上优先使用 Xcode 生成器方便接 Instruments 做性能分析在 Windows 上优先使用 Visual Studio 生成器方便接 PIX 做图形调试CI 环境里统一用 Ninja 保持速度这个策略说起来简单落地的时候有一个小坑——CMake 的 CMAKE_GENERATOR 是在配置阶段决定的你没法在一个 CMakeLists 里根据平台连续切换生成器。我在外层包了一个 shell 脚本负责判断当前平台然后调用带 -G 参数的 cmake这样做最简单可靠。3.4 使用的第三方库以及版本锁定策略手搓引擎不代表不用第三方库我只是极度克制地选了几个SDL2 做窗口和输入抽象本来在 SDL 和 GLFW 之间纠结过SDL2 的音视频能力留着备用成本更低Vulkan SDK 官方头文件和加载库做图形 APIstb_image 处理图片解码Dear ImGui 做调试 UIglm 做数学运算自定义 70 行实现的 log 模块不想引 spdlog因为只用了它 5% 的能力还白送了几十 MB 编译时间SDK 管理的策略很关键。我把第三方库全部用 FetchContent 的方式拉取但锁定确切版本 hash。这样做的收益有两点一个是新克隆项目时可以自动拉取正确版本不用手动下 SDK另一个是每次升级库版本非常确定。代价是每次要承担一次编译时间不过我们用 CCache 之后这块基本被吃掉了。最近一次 FetchContent 踩坑比较有代表性——Vulkan SDK 在某次版本更新后vmaVulkan 内存分配器的头文件里因为引入了 C20 特性导致 17 模式下编译报错。我整整花了两个多小时才定位到这个原因当时我们为了兼容移动端工具链还锁死在 C17。后来解决的办法是给 vma 单独建一个目标用 C20 特性去编译它而引擎本体保持 C17两者用纯 C 接口衔接。这种跨标准版本协作的思路在 C 项目里会经常遇到值得记住。4. 生成式 AI 在我的引擎开发里到底发挥了什么作用4.1 AI 是结对程序员不是替代程序员这里用最直接的方式说说我的实践。我日常是这样用 AI 工具的当作一个 7x24 小时在线的结对程序员它负责提供方案、补全样板代码、快速查文档我负责做技术判断和最终审计。比如今天上午我需要写一个组件序列化器。这个功能无非是把每个组件的字段名、类型、偏移拼成一个表然后通过反射机制完成保存和加载。要说难也没多难但写起来很消耗时间。我让 AI 先生成了一份基于模板特化的序列化框架然后我花二十分钟审了一遍代码发现它在字符串池的处理上有一个边界问题它假设所有字段名都来自一个静态表但我的组件里有些字段是运行时才能确定名字的动态属性比如自定义事件回调的标识。我修正了这个部分随后它的价值就体现出来了——省掉了我写那几百行重复代码的时间。关键点在这里AI 生成的那部分代码我能理解它的每一行。为什么要理解因为如果不理解我根本发现不了那个字符串池静态假设。很多同学用 AI 写完代码直接跑跑通了就算完。在一般业务代码里这么干也许问题不大但在引擎里跑通和正确之间的距离是巨大的——它可能 100 次里跑通 99 次剩下那个 1% 的崩溃会在线上咬你一口。4.2 AI 辅助搜索代替翻文档另一个我依赖 AI 比较重的场景是 API 查询。C 标准库和 Vulkan 的 API 文档都厚得能砸死人有时候我就想知道std::optional的reset()之后*this还有没有值这种问题。传统方式要翻 cppreference现在我会直接问 AI让它把标准里的语义解释清楚。但我绝不直接采纳 AI 给出的代码——除非我可以通过本地编译验证它的行为。我的工作流是先把 AI 给的代码放进一个sandbox目录里写个最小用例跑一下确认 API 行为符合我的预期再把它的核心思想拿回到引擎工程里用。这一步验证成本很低但能防住 90% 的 AI 幻觉。有一个很典型的例子有一次我想用std::transform配合并行执行策略做资源文件的批量解码AI 给了一段代码用的是std::execution::par_unseq。编译过了但在我们的自定义分配器环境里跑出的性能比单线程还差而且偶尔有数据竞争。我查了一下才知道原因par_unseq在底层可能会做向量化而向量化要求数据在内存里连续我们那个解码流程的数据访问模式并不满足这个条件反而导致线程切换的开销远大于收益。这个经验后来直接沉淀到了我们引擎的编码规范里除非数据访问模式已经明确满足并行策略的前提否则不要用并行std::transform。4.3 我坚决不让 AI 碰的部分在 AI 面前有三个地方我设置了绝对禁区核心数据结构的实现、渲染管线的状态管理、以及任何涉及资源生命周期的代码。理由很简单这些地方的正确性依赖的是长时间的上下文理解而不是局部代码的看起来合理。举个例子。我们的 ECS 组件存储采用了一个稀疏表结构实体 ID 是一个 32 位整数高 8 位是版本号低 24 位是索引。这个结构保证了实体被销毁后 ID 不会立刻复用这条对异步系统至关重要的语义。如果让 AI 帮我改这个地方它很可能把实体 ID 简单当成一个 index 来用误以为删除后马上可以复用——在本地跑 100 次测试可能都发现不了问题。但一旦进入生产环境一个延迟一帧才释放的旧 ID 引用了一个新实体画面就会出现一个莫名其妙的工件。所有这类 bug都是我花了最惨痛代价才学会的我不会让 AI 用看起来合理来破坏它。如果你也打算用 AI 辅助维护自己的引擎我建议你也在工程里设定类似的红区清单。这不一定基于技术本身它可以是你代码库里所有需要跨子系统上下文理解的部分。AI 生成单点代码的能力很强但跨上下文的一致性那是人类架构师的主场。5. 从零到第一帧画面程序员的勇气与坚持5.1 第一个三角形比想象中更折磨人说句实话最开始的起步阶段是最难的难在万事开头难这件事在图形渲染里格外真实。第一帧画面的背后是窗口系统、交换链、渲染管线、着色器、顶点缓冲等多达十几个环节的协作任何一个环节出问题最终表现都是黑屏或崩溃而你很难知道到底坏在哪一环。我以 Vulkan 为例说一个非常典型的第一个三角形的路径。流程大概是这样的初始化 Vulkan 实例和逻辑设备、创建交换链和深度缓冲、创建渲染管线和框架缓冲、上传顶点数据、录制命令缓冲、提交队列、呈现图像。这中间任何一步出错结果都是窗口能开但画面上什么都没有或者引擎崩溃。我当时用了整整一个周末才走通这条路径。调试过程极其痛苦——因为你不知道是该怀疑初始化的某一步还是渲染管线的某个状态设置还是着色器的编译结果。常常是自己改了一处无关痛痒的代码画面还是黑的但你根本不知道是已经改好了一部分只是另一部分还坏着还是改了半天其实在原来的基础上前进了一步。我的做法是把这条流程拆成几个里程碑每个里程碑单独验证第一个里程碑窗口能创建交换链能获取图像。验证方法是把后台缓冲刷成一个纯色或者往上面绘制一个简单的颜色渐变。第二个里程碑图形管线能创建成功没有验证错误。这一步什么都不用绘制只需要保证管线相关对象创建成功即可。第三个里程碑顶点数据上传和命令录制正确能提交给 GPU。这时候就算画不出东西也可以通过 RenderDoc 的帧捕获看到顶点缓冲和调用链。第四个里程碑画一个最简单的三角形。每跨过一个里程碑我都把项目做一次 tag写一条简单的 commit message。这种可验证的推进方式让漫长的调试过程变成了一条条可以量化进展的台阶。后来我发现这几乎是所有图形程序开发者的共识——越是复杂的系统越要切成可以独立验证的小块。这个习惯后来也延续到了引擎的其他模块开发中。5.2 软件渲染回退的意外价值在做渲染器的时候我临时写了一个软件渲染回退路径这个决策意想不到地为整个开发流程带来了巨大价值。Vulkan 驱动不是哪里都能用的尤其在 CI 环境或者虚拟机上硬件加速经常不可用。软件渲染路径让引擎在没有 GPU 的环境里也能跑通大部分逻辑测试——不需要创建真实的图形管线只把 CPU 端的数学运算和场景管理跑对就行。这对游戏开发流程太重要了。很多引擎的单元测试都被 GPU 复杂度吓退了但我可以轻易地在 CI 里执行一组纯 ECS 和物理测试、数学库测试以及不带渲染的集成测试。等代码合并回主分支时只在本地用硬件渲染做一次视觉验证这种分层验证体系帮我节省了大量调试时间。软件渲染的另一个用途是作为渲染正确性的参考实现。有一次我发现硬件渲染的阴影方向跟预期不一致我先在软件渲染里算一遍相同矩阵下的结果两边一对比才发现是我在做法线变换时遗忘了一个变换细节法线变换矩阵应该用逆转置矩阵而我直接用模型矩阵做了变换。这种错误在有参考实现的情况下几秒钟就能定位没有参考实现就得靠肉眼看画面猜了。5.3 手写引擎给我带来的调试者的骄傲说到调试有一件事我特别有成就感。前段时间项目遇到一个非常刁钻的 bug某些机器上渲染的贴图会有撕裂条纹但只在镜头快速旋转时会偶现。我初步判断是采样坐标回绕模式的问题但换了 CLAMP 和 REPEAT 都不行。后来我把 Vulkan 的验证层打开发现了一条警告提示我某个图像的 mipmap 等级与实际纹理的 mip 数量不匹配。我一查才发现我在一处纹理上传代码里硬编码了一个基于log2(width)计算的 mip 数量但这个纹理的高宽比不是 2 的幂次导致最后一层 mip 的尺寸计算错误采样时产生了一些未定义的边缘行为。这个 bug 的排查如果用 AI 直接帮我看一下基本不可能定位因为上下文太长了。但如果用传统的关键词搜索——Vulkan mip map 撕裂条纹第一页结果大概率也帮不上你什么忙。最终靠的还是对渲染管线的整体成熟度我知道验证层会输出什么信息、我知道一个图像的所有 mip 都是自己手动计算上传的、我能在正确的官网上查到规范的行为。这就是我在第 1.2 节里说的不可替代的判断力。6. 手写引擎和 AI 协作最怕踩的五个大坑6.1 被 AI 看起来正确的代码带偏方向这是我在开发中最常遇到的一类问题必须要单独拿出来提醒。AI 生成代码的速度太快了快到它给出的内容会天然给人一种权威感。尤其在引擎开发这种领域普通程序员能判断 AI 输出是否正确的人本身就不多于是很多朋友姑且信了AI 生成的引擎代码能给我提供正确答案的错觉。我给你举一个具体的例子。有一次我问 AI 怎么写一个基础的平行光阴影映射的 shader。它给了一个看起来很标准的实现深度写入、采样、比较。我投进去一跑——阴影确实出现了但非常脏像屏幕上有随机噪点。我看到噪点的第一反应还以为是采样偏移的问题调了半天后来一查发现 AI 在深度比较时用的 pcf 偏移搞错了坐标系它把偏移加到了裁剪空间的坐标上而不是采样坐标上。这种错误如果让我手写我大概率不会犯。因为它来自于AI 对接口理解不精确——它把不同坐标空间的惯例混在了一起。从这次以后我养成了一种怀疑优先的心态AI 给的代码我先默认它有隐藏错误再通过本地验证来推翻这个假设而不是反过来。这套心态配合上面提到的 sandbox 最小用例验证法能过滤掉几乎所有的低级错误。6.2 忽略工具链之间的兼容性引擎开发里工具链是最容易被忽略的隐藏假设。AI 给的代码经常默认你在用最新标准、默认你的编译器和库之间是兼容的。但真实环境里特别是跨平台项目同一个 C 源码在不同编译器下的行为差异会大得惊人。我踩过最典型的坑是 macOS 上用 Xcode 的 AppleClang 编译我们项目里的一个第三方库突然报错说std::span不可用。查了一下AppleClang 当时对 C20 的支持还停留在标准库层面std::span 这种标准库类型没实现完整。我们 CMake 文件里写的是target_compile_features(engine PRIVATE cxx_std_17)理论上有 C17 就够了但那个库的某个版本头文件里用了一个 C20 的特性做条件编译导致实际编译路径就跑到了不支持的标准库里。这种问题的解决靠的不是 AI而是对工具链发布周期的了解以及 CMake 的 feature 检测机制。后来我在工程里加了CMAKE_CXX_STANDARD检测并检查编译器版本在 CI 里对每个平台至少跑一次编译检查才把这类问题彻底堵住。6.3 构建时间失控AI 的加码是雪上加霜手搓引擎项目天然编译压力大——因为代码库整个是自己的你没法预期编译时间。我之前提到用了 CCache确实帮了大忙但还有另一个隐患AI 生成的代码特别喜欢一次性把大段逻辑全写出来这从代码规模上是高效的但在编译复杂度上是灾难——模板嵌套、编译期计算、包含重型头文件一个文件编译要几十秒这还是中了 CCache 之后的情况。我的处理方式是给引擎设立编译预算制度。每次提交代码时我会记录新增的头文件依赖和模板复杂度如果某个文件的编译时间超过了 5 秒就必须把里面的模板特化拆到一个更小的单元里或者用 pimpl 把依赖隔离掉。AI 生成的代码我基本都会做一次编译友好化重构拆分重型模板、把实现从头文件移到 cpp、尽量少用全特化而用普通重载。这样虽然多花了一点修改时间但换来的是日常开发里更快的迭代速度。6.4 AI 能读懂我的架构是一个危险幻觉这是我必须提醒所有尝试让 AI 在大型项目里长期辅助开发的朋友的一点。很多工具号称了解你的整个代码库但它的了解和你的了解完全是两回事。它知道你的符号表、函数签名、文件依赖但它不理解你的设计意图——为什么某个接口设计成那样为什么某个组件不允许跨线程访问为什么某块代码要故意写成看起来低效的样子。最危险体现在重构上。有一次我想重构一个资源加载模块把同步加载替换成异步加载。我把这个任务描述给 AI 并让它生成主要的改动方案它产出了一个看起来很完整的计划把加载函数改成了接受回调的形式还把调用点都改掉了。我耐着性子审了一遍发现它把两个在逻辑上属于互斥的资源加载请求合并到了一个回调里这在某些竞态条件下会导致死锁。如果我没有理解这个模块的设计约束这套看起来完整的重构方案合进主线后果不堪设想。我因此给自己的协作定制了一条规则——只有在我已经能完整描述某模块的架构约束、数据流和并发模型之后才会允许 AI 做这个模块的批量性变更。在这之前它最多只能做点局部辅助。6.5 过度依赖对话式开发失去全局思维最后一个坑是从人机协作模式层面讲的。长期一条一条地跟 AI 对话开发很容易把你自己训练成一个小任务解决器而不是系统构建者。引擎开发恰恰最需要系统性思维一个 API 改了所有调用点、序列化、调试可视化、关卡编辑器、网络同步都可能受影响。这种改一处牵全身的思维方式AI 不负责替你养成甚至它的回答方式还会反向强化你的碎片化思维。我自己的实践证明最有效的应对方式是定期做无 AI 日。每周我强制留出半天关掉所有 AI 辅助工具只用手写代码和阅读源码。这半天要么用来重构最不熟悉的那块模块要么用来阅读 Vulkan 或 SDL 的源码。做这种事的过程很慢但它能强迫我重新建立对代码库整体的感觉——这种感觉和看 AI 总结出来的架构说明完全不一样。可能是因为当你亲手在代码里穿行你会从一些非常具体的细节里感知到系统的整体气质这些信息密度远远高于任何一篇架构文档。7. 构建流程的优化以及我如何让手搓引擎变得可持续7.1 把构建时间当成第一等公民引擎开发里构建速度直接影响开发心态。如果你一次完整构建要 15 分钟你每次改代码的反馈循环就是 15 分钟心态很容易崩。所以我很早就把构建优化当成一个正式的性能指标来管理。我的具体手段包括使用 CCache 做编译缓存命中率长期维持在 90% 以上。尽量把模块拆小让每次改动能触发的重编译范围尽量小。头文件里尽量前向声明只在 cpp 里包含具体定义。对 Dear ImGui 这类每帧都在变化的调试 UI我把它单独拆成动态库改 UI 代码不用重编引擎主体。这套做下来一台 8 核的开发机上引擎冷编译大概在 40 秒左右增量编译常态只有几秒到十几秒。这个水平对我来说已经够舒服了毕竟我不是在写一个几百人的大项目。7.2 里程碑式开发每个阶段都有可玩的输出引擎开发的另一个大坑是永远在做地基永远没有成品。很多人写了很久的引擎自己的游戏还停留在窗口能开看到个三角形的阶段。为了避免这种状态我从一开始就给自己定了一条规则每个里程碑结束必须有一个可玩的输出物。比如我的第一个里程碑是能渲染出一个旋转的立方体带双线性贴图采样和简单的方向光。这个版本跑通以后我立刻做了一个很简陋的3D 展示间小应用——你可以用 WASD 和鼠标在场景里逛一逛。这个应用今天看来非常粗糙但它让我第一次感受到了引擎从代码变成体验的完整链路。有了这个正反馈之后写物理、写动画系统、写音频模块都是带着这个功能会让我的游戏更好玩的期待去写的而不是为了写而写。这条经验我强烈推荐给所有正在手搓引擎或想开始手搓引擎的人——引擎开发如果没有输出物会大概率烂尾。反过来只要有持续的输出物哪怕很简陋你也会被这周我要看到什么新效果这种内在驱动力推着走。7.3 用 AI 写单元测试把宝贵时间留给架构引擎开发的测试通常分为两类数学库和数据结构这种纯逻辑单元以及渲染、物理这类需要环境和状态的系统测试。前者非常适合 AI 来批量生成后者则不太适合。我会让 AI 帮我生成前者的测试用例特别是边界条件的枚举这一下子省了我很多体力活。比如我们引擎里的Math::Matrix4::LookAt函数我用 AI 生成了十几个传统视角向量、退化输入、极端参数下的测试用例。大部分它生成的是对的但其中有一个边界用例让我印象深刻——当 eye 和 center 重合时up 向量与视线平行叉积为零向量这时应该怎么处理它给出按文档规范的退化处理建议返回单位矩阵并打印警告这个建议我采纳了。而且因为是我让它写的我对这个测试的意图非常清楚后面维护起来也没有障碍。7.4 手写引擎之后我的目标其实也不需要太大网上有人形容手搓引擎的人不是在造轮子而是在造坦克语气里总带一点不值当的味道。但从我自己的实践来看这件事的价值完全取决于你的目标。如果你的目标是快速做出一款独立的商业游戏那用现成引擎肯定是理性的选择但如果你的目标是成为一个真正懂游戏技术底层的程序员我认为手搓引擎的过程是无法被替代的。对我个人来说这个项目的意义已经超越了做一个引擎。它帮我建立了一套完整的、跨越多层抽象的技术直觉从 CPU 缓存到 GPU 缓存、从编译器优化到渲染状态、从资源流送到资产管理我都能在一个统一的框架里理解。这套直觉是我在这个 AI 时代最有竞争力的底牌。因为 AI 可以帮我把引擎的很多局部做得更快更好但只有当我自己清楚整体系统的行为边界时才能判断 AI 的建议哪些真正有价值哪些只是貌似合理的表面功夫。8. 引擎现状以及给同样想手搓引擎的人一份避坑清单8.1 我的引擎现在跑了哪些玩法写到这里顺便交代一下这个项目目前的状态。两年下来这套 C17 引擎已经具备了一个小型 3D 游戏所需的大部分基础能力渲染了多盏点光和平行光、物理引擎用的是自研的一套简化刚体模拟球形和盒形碰撞、资源系统支持 glTF 模型和纹理加载、地形是分块 LOD 的这部分写得还比较粗糙、音频用 SDL2 的 mixer 模块做了个简单的音乐和音效播放。目前我在用它开发一个小的低多边形风格解谜游戏。说实话用自研引擎做游戏你会在开发过程中发现很多奇怪的工作量一边要处理玩家的操作手感一边要处理引擎自身的 bug。但这个东西有一个好处——你永远不用担心引擎不支持我要做的效果。因为如果引擎不支持你就去写引擎。这种感觉非常自由自由到你会觉得一切皆有可能。8.2 给新手的避坑清单全是拿时间换来的如果你也想开始或者已经开始手搓引擎我给你一份最实用的避坑清单这些基本上都是我在这个项目里付过学费换来的教训。第一不要一开始就追求大而全的 ECS 架构。先用简单的组件结构把游戏逻辑跑起来等确实遇到性能问题了再引入更复杂的 ECS。市面上流行的 ECS 库都假设你理解很多底层设计权衡一开始就上容易把你绕晕。第二所有资源生命周期相关的代码一定要从一开始就设计好引用计数。你早晚会需要异步加载如果没有引用计数异步加载和销毁之间的竞态条件会让你的调试变成一个无底洞。第三图形 API 的选择上如果目标是 3D 引擎趁早学 Vulkan 或 DirectX 12 这类现代 API。OpenGL 虽然容易入门但它的状态模型和现代硬件架构差异太大很多概念学到后面反而会阻碍你理解现代 GPU 工作方式。第四多线程渲染一定要早做。我知道单线程渲染先跑通 demo 很诱人但后面再改造多线程的代价比一开始就按多线程设计大好几倍。我的建议是哪怕你暂时只用一个渲染线程也要从架构层面把逻辑更新和渲染生成命令分离为将来多线程留下空间。第五编译时间管理和构建系统优化永远值得提早投入。这件事最容易被新人忽视因为在项目规模不大的时候感觉不到但等到规模上来再改构建系统成本就非常高了。第六调试工具链最好在项目早期就搭好一个帧调试器RenderDoc 免费且强大、一个 CPU 性能分析器macOS 用 Instruments、Windows 用 Visual Studio 的 CPU Profiler以及配套的日志系统。这三件套能覆盖掉 95% 的引擎开发调试场景。不要省这一步不要觉得等有 bug 了再配。8.3 引擎之外我对手写系统的最终理解如果非要浓缩成一句经验的话我会说游戏引擎是程序员的实体作品它的存在感比任何抽象的架构图都强。你写一个 Web 服务它跑在一个你看不见的服务器里你写一个移动应用它在用户的设备里休眠或亮起。而你写一个游戏引擎它会产生一种非常具体的、可以触摸的技术体验——玩家控制角色时画面的变化、物理碰撞时物体的响应、资源加载时屏幕的过渡。这种我的代码直接在三维世界中产生即时反馈的感觉是其他任何软件开发方向都给不了你的。所以我虽然理解并拥抱生成式 AI 带来的效率革命但我仍然偏执地认为在游戏引擎这种对底层掌控力要求极高的领域亲手一行一行把代码写出来的过程会持续产生 AI 无法替代的价值。它不是效率的浪费而是一种对技术本体的深度理解是对程序员这个身份最脚踏实地的回敬。最后分享一个很细节的经验手搓引擎如果遇到想放弃的时刻不用硬撑放下代码去做点完全不相关的事。我很多时候最关键的架构决策是在散步或洗澡时想通的而不是在盯着 IDE 的时候。因为引擎这种系统它的复杂度不在于单点而在于无数个组件之间的相互作用。你的大脑需要一个离线整合的过程把那些你在代码里零散接触到的细节串成一个整体。这个过程AI 目前也给不了你——它会替你总结但那种总结是基于它的逻辑不是基于你的直觉而后者才是真正属于你的资产。