7天3kB:在自定义字节码VM上制作游戏
摘要
作者详细介绍了在Langjam Gamejam中,用7天时间在自定义字节码VM上制作一款射击游戏,最终生成一个3kB的Windows可执行文件,并带有全屏像素着色器。
<p><a href="https://lobste.rs/s/he9kpd/making_game_on_custom_bytecode_vm_7_days">评论</a></p>
查看缓存全文
缓存时间: 2026/08/12 08:32
# 在自定义字节码 VM 上用 7 天和 3kB 制作一款游戏
来源:https://laurent.le-brun.eu/blog/making-a-game-on-a-custom-bytecode-vm-in-7-days-and-3kb
在过去的几天里,我制作了一款射击游戏(shoot ’em up),通过嵌入一个微型自定义字节码虚拟机,并使用全屏像素着色器渲染图形。结果是一个 3kB 的 Windows 可执行文件。
这是为 Langjam Gamejam(https://langjamgamejam.com/) 做的,一个 7 天挑战赛,要求你创造一种编程语言,然后用它来构建一款游戏。
这个项目结合了我的几个兴趣点:语言工具(https://laurent.le-brun.eu/blog/an-overview-of-starlark)、游戏开发(https://laurent.le-brun.eu/blog/designing-a-video-game-for-language-learners)、程序化图形,以及 demoscene 风格的大小限制(https://www.ctrl-alt-test.fr/2024/how-we-made-an-animated-movie-in-8kb/)。Game jam 的形式迫使我控制范围并探索新想法。而且,它很有趣!
和往常一样,[我的代码在 GitHub 上](https://github.com/laurentlb/shmup8);它也在 [itch.io](https://laurentlb.itch.io/shmup8) 上。为方便起见,这里有一个 YouTube 视频:
## 背景
当我第一次听说这个 game jam 时,我立刻产生了兴趣。我想了好几天,因为要找到一个能从新语言中受益的游戏概念并不容易(除了,嗯,像 TIS-100 这样的编程游戏)。
然后我想起了那些使用自定义字节码来缩小体积的 demoscene 作品。我想到的一个例子是 [Ikadalawampu](https://www.pouet.net/prod.php?which=54561),一个 2010 年在 Amiga 上运行的 4kB demo。我仍然有些怀疑:仅仅为了让实际代码更小,嵌入一个解释器真的值得吗?我必须试一试。
我的另一个灵感是 2004 年制作的第一人称射击游戏 [kkrieger](https://en.wikipedia.org/wiki/.kkrieger),它只有 96kB。从那以后,我们很少看到优秀的、以体积为编码目标的电子游戏。探索这个领域在我脑海中已存在一段时间了。
考虑到这些,我觉得这个 game jam 是一个做探索性工作的好借口。
## 计划
- 设计一种语言。
- 使用 F# 实现一个编译器,将其编译为字节码。
- 使用 C++ 编写一个字节码解释器。
- 使用自定义语言创建一款射击游戏。
- 使用一个 GLSL 着色器渲染图形。
尽管这些设计选择是出于体积编码的考虑,但我也并不想花时间优化代码。我最初估计游戏会控制在 4 到 8kB,所以我把项目命名为 shmup8。最终可执行文件比预期的要小,但这也是因为我没有加入音乐或 3D 图形。和往常一样,着色器代码已[压缩](https://www.ctrl-alt-test.fr/2025/15-years-of-shader-minification/),可执行文件用 [Crinkler](https://github.com/runestubbe/Crinkler/) 压缩。
## 实时编码工作流
当有即时视觉反馈时,编码会更有趣。我希望能够在不重新编译 C++ 代码的情况下编写整个游戏逻辑和视觉效果。我的想法是只运行一次可执行文件,然后通过实时重载进行完整的迭代。
每次我在 IDE 中编辑源代码时,自定义编译器都会被调用,并将字节码转储到文件中。然后 C++ 项目重新加载这个字节码,并在每一帧执行它。类似地,GLSL 着色器在编辑时也会自动重新加载。
早期开发截图:左上角是运行中的游戏,左下角是自定义语言,右下角是 GLSL 着色器,右上角是控制台日志。由于自定义语言会向着色器发送数据以供显示,因此同时拥有两个实时编码环境使得迭代非常容易。
Screenshot%202025-12-16%20000557
快速迭代是提高生产力的关键特性,尤其是在你很难预测什么效果会好的创意环境中。
## 字节码设计
我很快决定,字节码和着色器之间的通信通过浮点数组进行。
本着极简主义,我决定只需要一种类型:float32。所有值都存储在数组中。想要一个局部变量?在浮点数组中选一个槽位来使用。如何索引数组?使用 float,解释器会将其转换为 int。如何编写条件?使用 float,如果它大于 0.5 就为真。
字节码只有两种语句:要么更新数组中的某个单元;要么跳转(可能带条件)到字节码中的另一个地址。
字节码还有表达式概念:因此当你更新一个单元时,该值可以是一个复杂的数学表达式,引用其他数组单元或函数(如正弦)。
0 到 255 之间的常量存储为一个字节。其他浮点数使用[我最喜欢的浮点技巧](https://www.ctrl-alt-test.fr/2018/making-floating-point-numbers-smaller/)存储为 2 个字节。
这种设计避免了栈、寄存器和类型标记之类的东西,从而使解释器和字节码都很紧凑。
[查看字节码元素列表](https://github.com/laurentlb/shmup8/blob/main/src/interp.cpp#L9)
## 语言设计
极简的字节码限制了我能在语言中支持的内容,但我仍然可以使用语法糖来让一些事情变得更优雅。
我使用类似 C 的语法,实现了对赋值、if 条件和 while 循环的支持。语法糖用于支持增广赋值和 for 循环。
每次编译器遇到一个变量,它都会在浮点数组中为它分配一个位置。当一个值需要与着色器共享时,我必须给它一个特定位置。例如,我决定用 state[5] 存储当前分数。为了提高可读性,我添加了对内容内联的支持。如下:
``
inline score = state[5];
``
现在我可以使用 `score` 来读取或写入 state[5]。
在迭代游戏的过程中,我有时不得不停下来补充缺失的功能。但在某些情况下,我也决定绕开这些限制;例如,在编写条件时,我用乘法代替了 &&。这其实是一样的……只要这些值只是 0 和 1。:)
我使用了一个名为 `missiles` 的数组来存储每颗导弹的位置。第一个单元表示屏幕上的导弹数量。如何在不写循环的情况下从数组中移除任意元素?交换它。
``
// 移除屏幕外的导弹
if (missiles[i*2 + 2] > 0.5) {
// O(1) 移除:将元素与数组中的最后一个交换
missiles[i*2 + 1] = missiles[(missiles[0] - 1)*2 + 1]; // position.x
missiles[i*2 + 2] = missiles[(missiles[0] - 1)*2 + 2]; // position.y
missiles[0] -= 1;
}
``
(参见[文件其余部分](https://github.com/laurentlb/shmup8/blob/main/game.source))
## 着色器图形
这与我们在 ShaderToy 中看到的非常相似。它只是根据游戏引擎提供的数据计算一个像素的颜色。
由于 game jam 时间有限,我让图形代码保持简单。
我使用了反馈效果(将上一帧与当前帧混合)来让视觉效果更有趣,并将该效果与一些噪声函数结合起来。
Screenshot%202025-12-16%20025758
## 游戏设计
游戏设计也同样保持简单。游戏是无限循环的。一开始有三个敌人。每 7 秒增加一个敌人。敌人有三种,每种都有其特殊的行为和视觉效果。
与某些人可能认为的相反,敌人不会死亡。当导弹击中敌人时,它会被传送到屏幕外,并且能够重新回到游戏区域。
这种方法让代码非常简单,同时确保游戏内容越来越多。我最初的想法是创建敌人波次列表,但那样设计和平衡难度会更困难。
和《Super Hexagon》一样,我特别注重提供快速重启并鼓励玩家重玩。预计游戏时长在 30 到 60 秒之间。
## 结论
快速迭代的工作流至关重要。并行设计字节码和游戏使得很难预先预测需要哪些功能,许多限制直到我开始编写游戏本身时才显现出来。
当然,还有一个问题……字节码真的比使用编译后的 C++ 更小吗?我把游戏逻辑移植到了 C++,去掉了字节码解释器,然后检查了体积。
C++ 版本比字节码版本大 90 字节。因此,使用字节码所节省的空间比解释器的大小更重要。有些人可能会说我移植的 C++ 引擎并没有真正优化;但解释器和字节码也没有优化。所以对这些数字不必太当真。
总的来说,这是一个有趣的项目。它比预期效果更好,我也学到了一些东西。我相信未来我会做更多游戏开发方面的探索。
(参见 [HackerNews 上的讨论](https://news.ycombinator.com/item?id=46325337))
相似文章
给Fable一个提示:“构建一个.kkrieger的Linux致敬版本。”它在一个C文件中交付了一个51KB的程序化FPS——然后通过截取其无头渲染的截图并实际查看来进行调试
一位开发者用单个C文件创建了一个完全程序化生成的致敬.kkrieger的第一人称射击游戏,生成了一个51KB的二进制文件,在运行时合成所有资产,并通过无头截图进行了验证。
构建Clang后端并将Doom移植到我的自定义字节码虚拟机
作者复活了他的自定义字节码虚拟机(UVM),并借助AI解析文本LLVM IR构建了一个Clang后端,成功将Doom移植到该虚拟机上。
@VraserX: 我仅仅用GPT-5.6 Sol随性编码,一天就做出了一个可玩的FPS游戏。这太疯狂了。用这个模型编码感觉就像…
一位用户报告称,使用GPT-5.6 Sol在一天内创建了一个可玩的FPS游戏,并称赞其编码能力。
GLM 5.2 在首次迭代中生成了这个可玩3D游戏的大部分内容
通过 Jarvis Code 编程代理使用的 GLM 5.2,在首次迭代中生成了一个可玩 3D 版《几何战争》风格游戏的大部分内容,仅需少量后续调整。
一款完全在浏览器中运行的 vibe coding 3D FPS
一个完全在浏览器中运行的赛博朋克 FPS 技术演示,通过 vibe coding 结合多个 LLM 在 3 天内完成。它具有程序化生成、WebGPU 渲染和全合成音频。