信息化管理系统 | 数字孪生 · 智慧园区 · 数字大屏 | App · 微信 · 小程序 | 元宇宙 · 区块链 · 3D展厅 | 虚拟仿真系统 | 新零售电商

三年全职 Rust 游戏开发,真要放弃 Rust 吗?(2)

从 Rust 游戏开发中学到的教训

 

作者罗列了他在三年 Rust 游戏开发者中总结的几条教训,我认为非常有见地。这几条教训也适合给想在生产环境引入 Rust 的团队作为技术选型参考

 

“一旦你精通 Rust,所有这些问题都会消失”

作者提到了一个他在社区里遇到的问题,就是当他遇到各种问题时,Rust 社区很多人都会说:“一旦你精通 Rust,所有这些问题都会消失”。

作者提到,“这句话好像是在 Rust 社区中存在一股压倒性的力量,当有人提到他们在 Rust 语言的基本层面上遇到问题时,答案是“你只是还没理解,我保证一旦你变得足够好,一切都会变得清晰明了”。这不仅仅是在 Rust 中如此,如果你尝试使用 ECS,你会得到同样的回答。如果你尝试使用 Bevy,你也会得到同样的回答。如果你尝试使用任何框架来制作 GUI(无论是响应式解决方案还是即时模式),你也会得到同样的回答”。

 

这句话的言外之意是指,“你遇到这种问题,是因为你学艺不精。”这在其他语言社区,可能听上去好像不太礼貌。但是在 Rust 社区,这样说是有原因的。

因为 Rust 编译器类型检查和所有权借用检查等机制的存在,会强迫开发者在遇到这类问题时,去反思自己的代码架构。Rust 不像其他语言那般让开发者随心所欲,这是一种限制。所以开发者经常可能会遇到「编译器强制重构」的时刻。

编译器强制重构,对于提升代码质量和系统安全来说,是一个优点。这个优点使得 Rust 非常适合开发大型的、安全关键的、想长期稳定发展的软件,比如基础设施类软件,以及一些想长期稳定发展的大型应用。这意味着,开发者必须得对他写的每一行代码负责

 

但是对于小型游戏来说,特点是,“代码写完即扔“,因为以后还可以写个更好的,不会长期维护。只要功能达到要求就行了,玩家能玩就行了,代码质量就算是一坨翔也无所谓。所以,Rust 编译器强制重构的特性,在这种场景下,对开发者来说就很难受了。因为你明明知道“那坨翔”后面没啥用,你还不得不重构它。

所以,这其实是个技术选型问题,而非一个语言之争问题

 

“Rust 在大规模重构方面表现出色,解决了借用检查器中的大部分 self 造成的问题”

所以,综上所述, Rust 最大的优势之一是易于重构。这是大家都认可的优势。

然而事物总是蕴含两面性的。语言特性好不好,得结合具体应用场景。这就是选型的本质。

应用和游戏有很大的不同。引用网友一句话,“游戏引擎需要管理极大量的状态和状态变化(这是需求,不是设计)”。

作者也说了,“游戏作为复杂的状态机存在一个根本性问题,要求经常变化。在 Rust 中编写 CLI 或服务器 API 与编写独立游戏完全不同。假设目标是为玩家构建良好的体验,而不是一组惰性的通用系统,需求可能会在人们玩游戏后的每一天都发生变化,你会意识到一些事情需要从根本上改变。Rust 静态和过度检查的特性直接与此相抗衡”。

很多时候,我们用 Rust 编写应用代码时,如果遇到借用检查问题,就说明我们的代码中存在「悬垂指针」的风险。这时候确实是需要重构或修复。这是 Rust 的安全保证。

然而,作者说,他就是“不想要好的代码”,他只想要“更快的游戏”,“更快的测试他的想法”。但是编译器借用检查强迫他重构代码。作者认为,对于独立游戏来说,可维护性并不是一个正确的价值观。因为独立游戏开发者应该追求的是游戏迭代的速度。而其他编程语言可以更轻松地解决这类问题,而不必牺牲代码质量。

我虽然认同他这个观点,但是独立游戏也分种类吧。如果是那种区块链游戏呢?涉及金钱利益的场景,代码质量真的没关系吗

 

“间接性只能解决一些问题,并且总是以开发人员的舒适度为代价”

作者说,Rust 非常喜欢并且经常有效的一种基本解决方案是添加一层间接性

我认为这不应该算是 Rust 特有的吧?不是有句计算机名言吗 :“计算机科学中的每个问题都可以用一间接层解决”。

Rust 借用检查器的许多问题可以通过间接地做一些事情来简单地解决。比如通过 Copy/Move某些内容,或者通过将其存储在命令缓冲区中,然后稍后执行。

作者举了两个例子用来说明 Rust 为了解决这类问题引入了一些有趣的设计模式:

  • World::reserve in `hecs`[5],hecs 库中的 reserve_entitiesreserve_entity 函数允许在 ECS (Entity Component System) 框架中预分配实体 ID,这种设计可以很好地与 Rust 的生命周期和并发模型协作。只是预留了实体的 ID,并没有立即创建实体。这意味着这些实体在预留阶段不会参与任何查询或世界迭代,直到它们通过如 insertdespawn 等操作显式地转换为“实际”实体。这种延迟初始化的模式有助于减少生命周期冲突,因为它允许更灵活的控制何时将数据(如组件)与实体 ID 关联。
  • get2_mut in `thunderdome`[6] ,可以从同一个集合中一次获取两个可变借用。这在 Rust 基本规则里是违反借用规则的操作,但是这个库用设计模式巧妙实现了。Thunderdome 库的设计灵感来自于 generational-arenaslotmapslab 等库,它是一个 Arena(竞技场)数据结构的实现。你可以理解为 Arean 算是一种类 “GC”的实现。

 

虽然通过这种设计模式也能解决问题,但这个门槛确实比较高,这也是 Rust 学习曲线高的原因。但是,这并不是作者想要强调的重点。

作者认为,很多时候会遇到无法用专门设计和深思熟虑的库函数解决的情况。这就是为什么很多人会建议用命令缓冲区或事件队列来解决问题,这种方法确实有效。

游戏特别之处在于我们经常关心相互关联的事件、特定的时间点,以及整体上同时管理大量的状态。将数据在事件边界之间传递意味着代码逻辑会突然分成两部分,即使业务逻辑可能是“一个块”,但在认知上必须将其视为两个部分。足够长时间在社区中待过的人都有过这样的经历,被告知这其实是一件好事,关注点分离,代码更加"干净"等等。你看,Rust 的设计非常聪明,如果某件事情做不到,那是因为设计有问题,它只是想强迫你走上正确的道路...对吗?在 C# 中只需要 3 行代码的事情,在 Rust 中突然变成了分散在两个地方的 30 行代码。最典型的例子就是像这样的情况:"当我遍历这个查询时,我想要检查另一个对象上的一个组件,并且触发一系列相关的系统"(生成粒子、播放音频等)。我已经听到有人告诉我,嗯,这显然是一个 Event ,你不应该将那段代码写在一行内。如果你在想“但这不会扩展”或“它可能在后面崩溃”或“你不能假设全球世界因为 XYZ”或“如果是多人游戏怎么办”或“这只是糟糕的代码”...我明白你的意思。但是在你向我解释我错了的时候,我已经完成了我的功能实现并继续前进。我一次性编写代码而不考虑代码本身,当我编写代码时,我在思考我正在实现的游戏功能以及它对玩家的影响。我没有考虑“在这里获取一个随机生成器的正确方法是什么”或“我可以假设这是单线程的吗”或“我是否在嵌套查询中,如果我的原型重叠会怎样”,而且之后我也没有得到编译器错误,也没有运行时借用检查器崩溃。我在一个愚蠢的语言和愚蠢的引擎中使用,并且在编写代码的整个过程中只考虑游戏本身。

 

作者想表达的其实很简单,就是 Rust 限制了他在游戏开发者中的自由发挥,因为他不需要代码质量(前提是使用 Rust)。如果换成其他语言,比如C/Cpp/ Go/Java / Python /Ruby 等他就不会担心这种问题,因为他可以随心所欲。

他说的很有道理,如果你的场景跟他一样,那确实不用 Rust 最好。应该快速用现有的成熟框架和脚本语言推出游戏或产品,验证想法,收获用户,而非和 Rust 编译器做斗争。

 

欢迎咨询,九影专注软件定制15年,提供:游戏定制、元宇宙、线上展厅、虚拟仿真、AR/VR、小程序/公众号、网站/App、全景展示/3D建模、CRM/OA/ERP系统定制等。

热线:021-50309719  电话/微信15000568602

 

微信“扫一扫”免费获取 方案+报价!

 

 

如有侵权请通知删除,谢谢!

本文转自;三年全职 Rust 游戏开发,真要放弃 Rust 吗?-腾讯云开发者社区-腾讯云 (tencent.com)