“耗时32小时做的游戏原型,被Godot毁了!我用Rust全部重写了一遍”

B站影视 内地电影 2025-10-14 17:33 1

摘要:在游戏开发的世界里,性能优化往往是“血的教训”。这篇来自独立开发者的真实经历,从一个看似无害的Godot体素原型开始,却一路跌进了性能地狱,最后不得不用Rust重构一切、重塑对“抽象”的概念。

【CSDN 编者按】在游戏开发的世界里,性能优化往往是“血的教训”。这篇来自独立开发者的真实经历,从一个看似无害的Godot体素原型开始,却一路跌进了性能地狱,最后不得不用Rust重构一切、重塑对“抽象”的概念。

作者 | daymare 翻译 | 郑丽媛

出品 | CSDN(ID:CS DNnews)

大概是几个月前吧,某天我在酒店房间里,坐在那台明显是为学习而买的笔记本前,玩着 Factorio(异星工厂)。突然,我灵机一动:要不做一个自动化游戏吧!类似于 Factorio,但更大、更好!

没有做游戏的经验?这不重要!我的计划很简单:把 Factorio 的自动化系统、Terraria 的 BOSS 战、还有 Minecraft 风格的体素世界融合起来。于是我心想,既然我没图形开发经验,那就先从简单的入手——做一个体素引擎。

耗时 32 小时的原型开发

“好的游戏都需要一个原型。”

所以,下一步自然是选择引擎。我想要一个快速、简单、跨平台的引擎——Godot 看起来完美:轻量开源、最近还更新了炫酷的新3D渲染器。当然,我知道 GDScript 在体素世界里撑不住,所以我打算把模拟部分用 Rust 写,只用 Godot 负责渲染。

初步计划完成,看起来很简单,怎么可能出问题呢?于是,我开始了:写了一个 Rust 扩展,然后画一个平面,再画一个立方体,最后画出整块区块(chunk)。

突然间,我就有了采矿、机器、传送带、采石机、制作系统……我完全沉浸在咖啡因和动力的驱动下,整个原型仅花了两天就完成了。

期间,我和朋友们讨论了无数次采石机的设计:怎么设计升级,而不是“只是更快”?毕竟采石机是要挖掘世界并摧毁它的。这也让采石机成为单管道问题——常见资源会堵塞系统,稀有资源又会被饿死。

不过,这对原型来说无关紧要,因为当时物品只是沿着传送带移动的物理体。不知道当时为什么我还跑去想这些枝节问题。

总之,我准备好了。两天大约 32 小时的原型开发完成后,我去吃了点东西,回来后还安排了第二天的试玩。一切看起来都很完美——引擎能跑,游戏流畅——直到它卡住的那一刻。

天哪,一切都崩了

幸运的是,我有一台性能还不错的电脑,测试时帧率从未低于 144 FPS。可问题是,我从没尝试过堆更多采石机,因为我原以为性能瓶颈会出现在物理体上——毕竟那是最耗费计算的部分。

为了理解这个问题,我得先讲讲 Godot 中如何生成区块(chunk)。

一个区块就是由三角形构成的网格,而三角形又由顶点组成。在最简单的体素引擎里,每个区块生成一个四边形——速度慢,但简单。为了做这个游戏原型,我还加了个很初级的剔除算法,让它不必生成被隐藏的面。

这一切我都是用 Rust 实现的,速度快得惊人。

当然,Godot 也有一个 SurfaceTool 类型,从技术上讲,这并不是官方提供的最底层的 API,但几乎是将单个顶点推送到列表中的直接替代品。

首先,Rust 调用 Godot 画一个四边形的代码大概长这样:

fn draw_quad(st: &mut SurfaceTool, k: &mut i32, quad: Quad) { let normal = match quad.direction { ... }; st.set_color(quad.color); st.set_normal(normal); for corner in quad.corners { st.add_vertex(corner); } st.add_index(*k); st.add_index(*k + 1); st.add_index(*k + 2); st.add_index(*k + 3); st.add_index(*k); *k += 4;}

而“直接推入列表”的做法是这样的:

fn draw_quad(vertices: &mut Vec let normal = match quad.direction { ... }; for corner in quad.corners { vertices.push(corner); } indices.push(*k); indices.push(*k + 1); indices.push(*k + 2); indices.push(*k + 3); indices.push(*k); *k += 4;}

看起来差别不大,对吧?但是 SurfaceTool 有多糟糕呢?——每区块 8 毫秒!

感觉还行?可是你要知道,我自己手动做只要 300 微秒,这就整整慢了 27 倍!而且,这还没算上 Godot 的网格转换时间,算上更是慢得要死。

所以,你大概可以猜到后果了:当朋友试玩这个原型时,帧率在一小时内暴跌到 10 FPS。

为什么我自己测试时没发现?因为我做得很高效,知道要建什么,基本没有浪费;而他,几乎把整个星球都铺满了采石机(差不多五台)。FPS 掉到 10 后,他停止了游戏,这也很合理。

不过,我还是把一小时的试玩算作胜利,只是他停下来的原因……真是令人震惊。

重新出发

这个原型,我觉得还是勉强算成功吧。于是,是时候做正式的游戏了。

Godot 事件之后,我决定用 Rust 全部重写。但光有代码还不够,你还需要引擎!而且性能必须超高,因为我再也不想看到有人因为性能问题停下来了。Godot 的惨痛经历让我彻底走向另一个极端——甚至考虑自己做一个完整的游戏引擎。

当然,这对我来说不是完全不可能……但真的完全没必要。最终,我突然意识到:我只需要做游戏,不需要引擎、不需要框架,只要裸 OpenGL 加一个 while 循环就够了。

当时,对我来说,这算是一个巨大发现。我打开了 LearnOpenGL,把 Godot 体素引擎逐步搬过来,几乎一模一样。效果惊人,速度比 Godot 快了不止一倍。

现在,我需要做真正的游戏功能了——首先是机器。像 Factorio 这样的游戏,很多机器并不需要每帧运作。它们会接受输入,处理N帧,然后产出结果。这种系统非常适合用工作队列(WorkQueue)实现。机器需要在 20 个 tick 后再次运作,就告诉工作队列:“20 个 tick 后提醒我”,然后工作队列处理它。这样,即便有上百万台机器,也只更新需要更新的那部分。

接下来是传送带。在 Godot 里我用物理体来表示传送带上的物品,因为简单,但缺点很明显:传送带没有吞吐限制,自动化游戏 80% 的难点都被掐掉了。Factorio 风格的传送带需要从末端更新到起点,我得搞清楚每条传送带依赖哪些其他传送带——一个小型依赖图!

经历 Godot 后,我对性能的敏感度极高,担心这会成为性能瓶颈。好在,我写不出高效缓存算法,只能每帧重建整个图,所以性能问题完全没有出现。

随后,我花了三周时间优化体素引擎,从 OpenGL 换到 WGPU(因为 MacOS 上的 OpenGL 不支持 SSBO)。最终,我的体素引擎支持异步区块和网格生成,在 M2 MacBook 上用 3GB 内存、渲染距离 3072 块,并稳定在 144 FPS+。

总结

这个游戏没做完(主要是因为 3D 体素沙盒里的工厂玩法体验感不是太好),所以故事还没有完整结局。

但也多亏了这次经历,每当我有想要寻求抽象的冲动时,我都会更加谨慎。因为老实说,在没有臃肿抽象的代码库里工作,要愉快得多。

至于体素引擎的下一步?也许,未来我会用自己的编程语言重做它吧。

来源:承光君

相关推荐