在 Zig 中游戏开发一年的反思
在 Zig 中游戏开发一年的反思
2025年4月29日 • 阅读时长11分钟 • Benjamin G. Thompson 一年前,我开始用 Zig 从零开始编写一款解谜游戏。虽然现在宣布这款游戏还为时过早(更多信息将在今年晚些时候公布),但我希望分享一些到目前为止我在 Zig 中进行游戏开发所获得的一些见解。
我写这篇文章是因为我看到很多人对 Zig 的看法都来自于他们对这个语言的相对较短的使用时间(例如,对于 Advent of Code),但很少有人使用该语言超过一年。
虽然下面的见解和例子最终都来自游戏开发,但我尽量专注于语言和生态系统中那些足够广泛的方面,以便也适用于游戏开发之外的人;希望它们对任何考虑在不久的将来尝试一个大型 Zig 项目的人都有用。
1. Zig 的 Discord 社区在解决初级和中级语言问题方面非常有帮助。
Zig 的 Discord 社区对于任何学习 Zig 的人来说都是一个很好的资源。在任何时候,zig-help
论坛都充斥着来自初学者的问题,比如“如何反向进行 for 循环?”或“在 WASM 中使用什么 allocator?”大多数问题都能在几分钟内得到解答。
然而,任何从零开始编写 3D 游戏的人,无疑会遇到一些难度更高的语言问题。我遇到了一些,并且很高兴地发现,在大多数情况下,Zig Discord 社区不仅回答了这些问题,而且也在几分钟内就做到了。让我举一个例子。
在一次特定的游戏测试环节中,我在一台笔记本电脑上构建了我的游戏,然后将二进制文件复制到 USB 上。当我的朋友试图在他的台式机上从 USB 启动游戏时,它启动了,但随即崩溃了。我们都运行相同的操作系统,并且游戏在我的笔记本电脑上运行正常。这是怎么回事?
我通过创建一个 Zig 中的 “Hello World!” 二进制文件,将问题简化到最基本的情况。当这个二进制文件也在我朋友的台式机上崩溃时,我用一个可移植的调试器([RemedyDB](https://bgthompson.codeberg.page/blog/one-year-zig-gamedev-reflections/https:/remedybg.itch.io/remedybg))运行它。我震惊地发现,该二进制文件包含一个 无法识别的指令。
哈?!我没有以一种奇怪的方式构建二进制文件——我使用的是标准的 zig build --Doptimize=ReleaseFast
命令。
我转向 Zig Discord 社区求助,很快问题就被识别并解决了:当 Zig 在打开优化的情况下编译时,默认情况下它会使用最佳的 微架构。
我们的 CPUs 都期望 x86-64
指令,但我的笔记本电脑期望 x86-64-v4
,而我朋友的台式机期望 x86-64-v2
。我的 CPU 知道如何处理 vpbroadcast
指令;而我朋友的 CPU 不知道。
在使用 -Dcpu=baseline
标志编译游戏后,该二进制文件仅包含 x86-64-v1
指令,允许我的朋友玩游戏。
如果不是 Zig Discord 社区,我可能需要几天的时间才能找到一个修复方案。但是,Discord 上有很多精通编译器内部原理的人,这意味着这个问题在不到一刻钟的时间内就解决了。
2. Zig 对向量有很好的内置支持,但对矩阵没有。
向量显然是任何 2D 和 3D 游戏引擎的重要组成部分。Zig 对它们有内置支持,这意味着许多运算符支持向量类型,并且将编译为在可能的情况下使用 SIMD 指令。
例如,考虑计算两个四维向量之间的欧几里德距离。在 Zig 中,这就像定义以下函数一样简单:
编译后,即使在调试模式下,Zig 也知道这个操作可以用几个 SIMD 指令来计算——当一条 vsubps
和 vmulps
指令就足够时,没有必要单独减去和乘以每个向量的分量:
(@reduce()
步骤的指令有点低效,但同样,这是来自调试模式的汇编。)
我很想报告 Zig 对矩阵有类似的内置支持,但事实并非如此(尚未)。如果你想要即使是基本的线性代数支持,你必须自己编写一个定制的库,或者使用一个现有的 C 库。
这对我来说不是一个致命缺陷,因为我无论如何都在为我的游戏编写引擎,但我知道有些人希望在开始一个类似的项目之前,在某种程度上获得官方的矩阵支持。
3. 与 CMake, Ninja, Meson 等相比,Zig 的构建系统是一股清新的空气。
去年,我尝试了一下高性能计算。我正在阅读 GMP 算术库的源代码,该库广泛应用于计算科学研究中,并发现了其构建系统中的以下内容:
你不需要有构建系统的经验也能意识到这是一个灾难。(注意:截图取自当前 GMP 版本,6.3.0
。)
相比之下,这是 Mitchell Hashimoto 最新的开源项目 Ghostty 的构建文件的一部分,该项目是用 Zig 编写的。区别就像白天和黑夜一样明显:
好吧,我承认我在这里做了选择——我选择了一个由 automake 生成的文件——我并不是要特别挑出 GMP。但我认为这些例子代表了其他构建系统和 Zig 的构建系统之间的差异。
对我来说,为我的游戏使用 Zig 构建系统比我使用过的 所有 其他构建系统都要好。
我不会说谎,学习 Zig 构建系统并不容易——它经常让我头疼。但与此相比,CMake 简直是一场蛛网膜下腔出血。编译一个中等复杂的 C/C++ 项目不应该需要了解神秘的脚本语言。
与大多数构建系统相反,Zig 构建文件本身就是 Zig 程序。由于构建系统的源代码是标准库的一部分,你可以像调试任何 Zig 程序一样调试它(例如,你可以将你自己的 print
语句插入到构建系统库中——我偶尔会这样做)。
4. 标准库中存在不完整的部分(这在很大程度上是可以接受的)。
去年,我想在我的游戏中创建一个图标,该图标由一个绕通过两个相对角的垂直轴旋转的立方体组成,就像这样:
你可以通过两个连续的简单旋转,将一个立方体从其规范方向(以原点为中心,边平行于 XYZ 轴)旋转到这种配置:
- 绕 X 轴旋转 π / 4,使立方体的 “顶部” 变成一条边。
- 沿另一条水平轴旋转 θ。
练习:θ 是多少?(提示:它 不 等于 π / 4。)
然而,当我尝试用 θ 的计算结果编译我的游戏时,我得到了以下错误:
像这样的时刻并不常见,但提醒我为什么我使用的编译器——0.14.0
——前面有一个零。
虽然这个特定的实例有点烦人,但它没什么大不了的:我只是把角度烘焙进去了。但我认为重要的是要知道,今天在做一个大型 Zig 项目时,由于标准库不完整而导致的问题会浮出水面,你需要解决它们。
5. 编译器经常以重大、令人兴奋和破坏性的方式进行更改。
在开发我的游戏的过程中,我已经经历了几个 Zig 编译器版本。每次,当我尝试用新的编译器构建游戏时,都会出现一些问题。要么是构建系统本身已经更新,要么是语言本身有破坏性的变化,或者两者都有。
但在每种情况下,我都能在两个小时内让游戏再次编译。由于每年只有两个主要版本发布,所以这并不是一件很麻烦的事情。
作为回报,我体验了语言和工具链的大量积极变化。
首先,每次发布的编译时间都变得更快。其他人也注意到应用程序性能的提高:Mitchell Hashimoto 报告 说,在从 Zig 0.13.0
过渡到 0.14.0
之后,Ghostty 上的滚动基准测试提高了 3-5%(但 警告说这可能是噪音)。
我应该注意到,调试编译时间的 大幅 减少也即将到来。Zig 团队 几乎消除了 对 LLVM 的依赖,以在 x86-Linux 上生成调试构建。我使用新的 x86 后端编译了一些基本程序,它把 LLVM 后端远远地甩在了后面。虽然新的后端目前由于我广泛使用向量而无法编译我的游戏——我得到了诸如 TODO implement airReduce for @Vector(3, f32)
之类的错误——但我完全期望一旦完成,编译时间的减少将是显著的。
另一个变化是 --watch
编译选项(例如 zig build --watch
)。这使编译器保持永久 “开启” 状态,监视源文件中的更改。一旦它检测到文件中的更改,它就会尝试另一次编译。
在保存编辑后,编译器自动快速地找到我代码中的下一个错误,而无需与 LSP 交互,这节省了我大量时间并且令人满意。当与增量编译(另一个即将推出的功能)结合使用时,保存编辑和编译尝试完成之间的延迟可能最终会比打一个响指所花费的时间更少。
我可以继续说下去,但我现在就到此为止。总的来说,到目前为止,我很高兴用 Zig 构建游戏。我没有太多抱怨。由于语言、工具链和标准库仍在积极开发中,我确实有的一些抱怨很可能会在即将发布的版本中得到解决。我很高兴继续用 Zig 开发游戏,并期待即将发布的 Zig 版本。