| 网站首页 | 游戏新闻 | 游戏资讯 | 游戏信息 | 游戏攻略 | 游戏大全 | 资讯中心 | 文章 | 

您现在的位置: 游戏信息资讯网www.yxnetw.com >> 游戏资讯 >> 正文

  没有公告

  Rust 生态纯属炒作?3 年写了 10 万行代码的开发者吐槽:当初用 Rust 是被忽悠了法拉利haobc           ★★★ 【字体:  
Rust 生态纯属炒作?3 年写了 10 万行代码的开发者吐槽:当初用 Rust 是被忽悠了法拉利haobc
作者:佚名    游戏资讯来源:本站原创    点击数:    更新时间:2024/5/15    

  嘻哈4重奏第1季编者按:本文作者是国外一位用 Rust 编程语言开发游戏的开发者,这位作者和他的朋友两人成立了一家小型独立游戏开发工作室,在过去几年中他们致力于开发跨不同引擎的各种游戏。

  他们热爱游戏,并在编程和创建各种应用程序(网络或桌面应用程序)方面拥有丰富的经验。他们用 Rust 构建了自己的引擎,称为 Comfy Engine,用于他们的游戏。本文就讲述了他们这三年来使用 Rust 编程语言开发游戏的心路历程。下列内容为 InfoQ 翻译并整理。

  免责声明:这篇文章是我个人对于多年来感受与困境的总结,也验证了我听到过的某些被反复强调的所谓“真理”。我使用 Rust 进行了数千个小时的游戏开发,期间也完成过多款游戏作品。本文不是想向大家炫耀我有多牛、有多成功,最主要的目的在于破除“你觉得 Rust 不好用,是因为你的经验还不够”这一广泛存在的荒谬论点。

  本文也绝不是科学评估或者严谨的 A/B 研究。我们是一支由两人组成的小型独立游戏开发团队,单纯是想用 Rust 做游戏并赚取持续发展的必要收益。所以如果大家能从投资者那边拿到几无上限的资助、一个项目就要做上好几年,而且并不介意不断开发各种必要系统,那我们的经验对您并不适用。我们的诉求很简单:最多在 3 到 12 个月的周期之内完成游戏的开发和发布,并借此赚取回报。这就是本文的立论基础,跟快乐学习、探索 Rust 没有半毛钱关系。并不是说后一种目标不好,只是我们这里讨论的是能不能用 Rust 养家糊口、能不能在商业层面找到可以自给自足可行路径的问题。

  我们已经在 Rust、Godot、Unity 和虚幻引擎上开发过一些游戏,各位没准在 Steam 上游玩过了。我们还从头开始使用简单的渲染器制作出了自己的 2D 游戏引擎。多年以来,我们也在多个项目中使用了 Bevy 和 Macroquad,包括一些相当重要的项目。我本人还有一份全职工作,负责的是后端 Rust 开发。另外需要强调,本文内容并非源自经验浅薄的新手——我们在这三年多时间里已经编写过超 10 万行 Rust 代码。

  我写下这篇文章的目的,就是破除一个曾被反复提及、误导了无数新手的荒谬观点。希望在读了这篇文章之后,大家能理解我们为什么要放弃 Rust 作为游戏开发工具。我们不会停止游戏开发,只是不再继续使用 Rust。

  如果各位的目标是学习 Rust,能感受到它的优秀之处而且乐于接受技术挑战,那完全没有问题。作为有经验的开发者,我会在文章中把应用场景明确区分开来,而不像很多所谓 Rust 老鸟那样不问你是单纯需要技术演示、还是想认真推出一款游戏,就盲目鼓吹 Rust 语言。我发现整个 Rust 社区的注意力都主要集中在技术身上,反而对游戏开发中“游戏”的部分熟视无睹。举个例子,我曾参加过一场 Rust 游戏开发的线下聚会,结果在提案中居然看到了“有人想在会上展示一款游戏,当否请批示”的纸条……着实给我整无语了。

  学习 Rust 确实是种有趣的经历,因为人们常常发现“这肯定是个只有我遇到过的特殊问题”其实是正在困扰更多开发者的普遍模式,于是每位学习者都必须调整思路并消化这些“怪癖”才能提高生产力。而且这些问题往往出现在相当基础的层面,比如 &str 和 String 或者.iter 与o_iter 的区别等等。总而言之,我们潜意识里认为应该没区别的事物,在 Rust 这边往往边界森严。

  我承认,其中一些属于必要之痛,在积累到足够的经验之后,用户就可以不假思索地预见到潜在问题并提高工作效率。我非常享受用 Rust 编写各种实用程序和 CLI 工具的体验,而且在多数情况下效率也比用 Python 更高。

  可话虽如此,Rust 社区中的确存在一股压倒性的力量。每当有人提到自己在使用 Rust 语言时遇到的基础问题,他们就粗暴回应说“你觉得 Rust 不好用,是因为你的经验还不够”。当然,这不仅仅是 Rust 的问题。我们使用 ECS 时有这种现象,在使用 Bevy 时也有这种现象。甚至是在我们使用自己选定的任何框架(无论是响应式方案还是即时模式)制作 GUI 时,也都有类似的困扰。“你觉得 xx 不好用,是因为你的经验还不够”。

  多年以来我一直对此深信不疑,也一直在努力学习和尝试。我在多种语言中都遇到过类似的情况,并且发现自己在掌握诀窍之后效率确有提升,慢慢学会了预测语言和类型系统的“脾性”并能有效回避问题。

  但我想强调一点,在花掉了大约三年的时间,在 Rust 的整个框架 / 引擎生态系统中编写了超过 10 万行游戏相关代码之后,我发现很多(甚至是大多数)问题仍然存在。所以如果各位不想没完没了地重构代码并将编程视为一种不断挑战自我的趣事,而单纯想要安安静静地用它完成任务,那千万别用 Rust。

  最基本的问题就是借用检查器经常选个最让人难受的时机强制进行重构。Rust 的粉丝们觉得这事没问题,因为能让他们“编写出更好的代码”,但我花在这门语言上的时间越多,就越是怀疑这话到底靠不靠谱。好的代码确实是靠不断迭代思路并做出尝试来实现的,虽然借用检查器可以强制进行更多迭代,但并不代表这就是编写代码的理想方式。我经常发现自己被牢牢卡死,根本没办法稍后再做修复——这导致我根本没办法用轻松愉快的心情把脑袋里的灵感丝滑顺畅地表达成代码。

  在其他语言中,人们可以在写完代码之后就把它抛在脑后,我觉得这才是实现良好代码的最佳途径。举个例子,我正在编写一个角色控制器,唯一的目标就是用它操纵角色移动和执行操作。完成之后,我就可以开始构建关卡和敌人了。我不需要这个控制器有多好,能起效就足够了。如果有了更好的点子,我当然可以稍后把它删掉再换上个更好的。但在 Rust 中,万事万物之间都有联系,导致我们经常遇到没办法一次只做一件事的情况。于是我们的每一个开发目标都变得极其复杂,并且最终会被编译器重构,哪怕那些一次性代码也是如此。

  人们常说 Rust 最大的优势之一就是易于重构。这话没错,而且我也有切身体会,比如可以无所畏惧地重构代码库中的重要部分,不必担心运行起来出什么问题。但事情真这么简单美好吗?

  事实上,Rust 也是一种比其他语言更频繁迫使用户进行重构的语言。每当我开发一段时间,就会突然被借用检查器当头棒喝,并意识到“好吧,我添加的这项新功能无法编译,而且除了代码重构之后没有其他解决办法”。

  有经验的人们常常会说,“你觉得 Rust 不好用,是因为你的经验还不够”。虽然这话原则上没错,但游戏是种复杂的状态机,其需求一直在变化。用 Rust 编写 CLI 或者服务器 API,跟用它编写独立游戏是完全不同的两种体验。毕竟我们开发游戏的目标是为玩家提供良好体验,而不是一组僵化死板的通用系统,所以必须考虑人们在游玩过程中随时发生的需求变化,特别是那些需要从根本上做出调整的变更。Rust 的高度静态特性与过度检查的倾向,明显有违游戏软件的天然需求。

  很多人可能会反驳说,借用检查器和代码重构并不是坏事,它们能有效提高代码的质量。确实,对于那些强调代码的项目来说,Rust 的特性有其积极的一面。但至少在游戏开发这边,多数情况下我需要的不是“高质量的代码”,而是“能早点试玩的游戏”,这样我才能快速测试自己的玩法设计思路。Rust 的很多坚持,其实就是逼着我在“要不要打破流程并花上整整 2 个小时进行重构”和“在客观上让代码质量变得更糟”之间二选其一。我真的快要崩溃了……

  这里我还要放句大不敬的话厥词:至少对于独立游戏来说,可维护性根本就是个伪诉求,我们更应该追求迭代速度。其他语言可以更简单地解决眼下的问题,又不必过度牺牲代码质量。而在 Rust 中,我们永远需要选择要不要向函数中添加第 11 个函数,要不要添加另一个 LazyAtomicRefCell ,要不要将其放入另一个对象,要不要添加间接(函数指针)并恶化迭代体验,或者干脆花点时间重新设计这部分代码。

  Rust 所主张、而且特别有效的一种基本解决思路,就是添加一个间接层。我们以 Bevy 事件为典型案例,对于“需要配合 17 个参数来完成任务”这类问题,Bevy 事件都是首选的解决办法。我也很努力地想从好的方面理解这种情况,但 Bevy 确实高度倚重事件、而且总想把所有内容都塞进单一系统。

  借用检查器的很多问题,都可以通过间接执行某些操作来解决。或者也可以复制 / 移出某些内容,执行该操作,然后再把其转移回来。又或者将其存储在命令缓冲区内并稍后执行。这通常会在设计模式上引发一些神奇的现象,比如我就发现可以通过提前保留 entity id 并结合命令缓冲区来解决很大一部分问题(例如 hecs 中的 World::reserve,请注意是 &world 而不是 &mut world)。这些模式有时候确实效果不错,而且足以解决种种极其困难的问题。另一个例子则是在 Thunderdome 中提到的 get2_mut,乍看之下没什么道理,但经验多了之后人们发现它能解决很多意想不到的问题。

  关于 Rust 那陡峭的学习曲线,我其实不想争论太多,毕竟每种语言都各有特性。但我想提醒各位的是,哪怕是积累到相当丰富的经验之后,Rust 中也仍然存在很多基本问题。

  回到正题,虽然前面提到的一些方法可以解决特定问题,但也有不少情况根本无法通过专门的、精心策划的库函数来解决。也正因为如此,很多人才建议直接使用命令缓冲区或者事件队列“将问题延后”,从而切实有效地解决问题。

  游戏开发的具体问题在于,我们经常需要关注相互关联的多个事件与特定的时间安排,而且得同时管理大量状态。跨事件屏障进行数据移动,意味着事物的代码逻辑会被割裂成两个部分——就是说哪怕业务逻辑本身仍是一个整体,在认知意义上也应被视为彼此独立。

  经常混迹 Rust 社区的朋友肯定知道,那边的老人儿们会说这是件好事,能保证关注点分离并让代码“更干净”等等。在他们看来,Rust 的设计者是最聪明的,所以如果有什么正常功能难以实现——那不是设计有误,而是他们想强迫你采取正确的实现方法……

  于是乎,在 C# 中 3 行代码能搞定的功能在 Rust 这边需要 30 行代码,还得一分为二。我给大家举个典型例子:“在迭代当前查询时,我想检查另一功能上的组件并涉及一堆相关系统”(比如生成粒子、播放音频等)。不用问就知道,Rust 社区上的那帮铁粉会说,“这明显是个 Event,所以你不应该内联编写代码”。

  可想象一下,在这样的规则下功能实现起来将有多麻烦(以下为 Unity 版代码):

  这只是个相对简单的例子,但这样的需求随时可能出现。特别是在实现新机制或者测试某项新功能时,我们最需要的是可以直接编写,而暂时不想考虑什么可维护性。我们要做的是很简单的东西,只想它在正确的位置上运行。我不需要 MobHitEvent,因为我还打算同时检查 raycast 等其他相关功能。

  我也不想检查“Mob 上存不存在 Transform”,因为我是在开发游戏,所以每个 entity 当然都有 transform。但 Rust 不允许我使用.transform,而且一旦我不小心让查询发生了原型重合,由此引发的双重借用就会立刻导致崩溃。

  我也不想检查音频源是否存在。我当然可以用.unwrap.unwrap,但细心的 Rust 会注意到这里没有传递 world。在 Rust 看来,运行场景是全局 world 吗?不是应该用依赖注入将查询写成系统中的中车个参数,并将所有内容都预先安排就绪吗?.Choose 是不是假设存在一个全局随机数生成器?线程呢?

  我知道,很多粉丝都会说什么“但这不利于未来扩展”、“后续可能引发崩溃”、“你不能假设全局 world,因为 blabla”、“你没考虑过多人游戏的问题吗”或者“这种代码质量敢用吗”之类……我都知道。但就在各位忙于挑错的同时,我已经完成了功能实现并继续前进。很多代码其实就是一次性的产物,我在编码过程中实际是在考虑当前实现的游戏功能会如何影响玩家体验。我并不在乎“这里应该使用哪种正确的随机生成器”、“能不能假设单线程场景”或者“嵌套查询当中的原型重合该怎么处理”之类的技术问题,而且后续也没有出现编译器错误或者运行时借用检查器崩溃。我只想在傻瓜引擎里用点傻瓜语言,保证自己能在编写代码的时候只考虑游戏逻辑,行吗?

  由于 Rust 类型系统和借用检查器的天然特性,ECS 自然而然成了帮且我们解决“如何让某个东西引用其他东西”的方案。遗憾的是,我认为其中存在大量术语混淆,不单是不同的人对其有不同的定义,而且社区中有不少人会把本不属于 ECS 的东西硬安在它头上。下面咱们就做一点明确的区分和阐述。

  首先,让我们先聊聊因为各种原因而导致开发者无法实现的东西(为了控制篇幅,这里不做过多的细节区分和讨论):

  具有实际指针的 pointer-y 数据。这里的问题很简单,如果字符 A 跟随 B,且 B 被删除(并取消分配),则该指针将无效。

  RcRefCellT与弱指针相结合。虽然可以实现,但在游戏中性能往往非常重要,而且受内存局部性的影响,这样的资源开销确实会带来可感知的影响。

  Entity 数组的索引。在第一种情况下出现了一个无效指针,这时候如果我们拥有一个索引并删除了一个元素,则该索引可能仍然有效但指向其他内容。

  这时出现了一个神奇的解决方案,能帮助我们摆脱所有问题——这就是我个人强烈推荐的 generational arenas——它又小又轻巧,而且能在保持代码库可读性的同时实现既定功能。这种稳定实现既定功能的能力,在 Rust 生态系统中其实相当罕见。

  Generational arena 在本质上就是一个数组,只不过我们的 id 不再是一个索引,而是一个(index, generation)元组。该数组本身存储的是(generation, value)元组。为了简单起见,我们可以想象每次在索引处删除某些内容时,只需增加该索引处的生成计数器即可。之后只需要确保对 arena 进行索引时,始终检查提供索引的 generation 是否与数组中的 generation 相匹配。如果该条目被删除,则 slot 将拥有更高的 generation,而索引也将“无效”、就如同该条目不存在一样。这种方法还能解决其他一些非常简单的问题,比如保留一个空闲的 slot 列表,以便在必要时向这里插入以加快操作速度——当然,这些都跟用户无关。

  关键在于,这种方式终于让 Rust 这类语言能够完全避开借用检查器,允许我们“使用 arenas 进行手动内存管理”,从而在保证 100% 安全的前提下无需接触任何指针。如果非要说 Rust 有什么让人喜欢的优点,那就是它了。特别是对于像 thunderdome 这样的库,二者确实结合得很好,而且这种数据结构也非常符合语言的设计思路。

  当然,这种定义方式并不能体现 ECS 的全部优势。但我想强调的是,我们想在使用 Rust 的同时尽量回避 Rc RefCell返回搜狐,查看更多

游戏资讯录入:admin    责任编辑:admin 
  • 上一个游戏资讯:

  • 下一个游戏资讯: 没有了
  • 最新热点 最新推荐 相关文章
    天娱数科01月05日涨停分析路…
    《火箭骑士历险记:重闪合集…
    腾讯发布GiiNEX AI游戏引擎:…
    手游开发福利 ShareSDK Coco…
    厉害了我的哥!手游画质要赶…
    谷歌发布基础世界模型Genie …
    粉丝用虚幻引擎打造HD-2D版《…
    Cocos2d之父加盟触控 v30版本…
    Stencyl(2D游戏制作软件)v34…
    从像素游戏到 3A 大作的游戏…
     最新文章
    普通游戏资讯 Rust 生态纯属炒作?3 年写了 10 万行代码的开发者吐槽:当…
    普通游戏资讯 天娱数科01月05日涨停分析路径抠图
    普通游戏资讯 《火箭骑士历险记:重闪合集》公开新要素预告 将带来全新的…
    普通游戏资讯 腾讯发布GiiNEX AI游戏引擎:面向AINPC、场景制作与内容生…
    普通游戏资讯 手机游戏程序员培训学校蔡默网xvt9yy神雕侠侣之作恶
    普通游戏资讯 安卓角色扮演单机游戏分享 最好玩的单机游戏排行2023破灭魔…
    普通游戏资讯 2023好玩的安卓单机手游推荐 超级好玩的单机游戏排行为忍师…
    普通游戏资讯 报:两芯片大师获2017图灵奖戴姆勒谷歌合作研究量子计算武…
    普通游戏资讯 安卓单机射击游戏有哪些 安卓单机射击游戏分享杨光的夏天演…
    普通游戏资讯 玩家爆料《炉石传说》破解版导致手机硬件损坏恶汉5200
    普通游戏资讯 破解版游戏下载app 破解游戏app最新排行榜龙仕旭强上天子
    普通游戏资讯 2024手机安卓游戏下载分享 好玩的安卓游戏推荐浙江废旧物资…
    普通游戏资讯 泸州:暑假来了如何管理孩子们的“屏幕时间”?钮汪人
    普通游戏资讯 类似饥荒的手机游戏有哪些 流行的生存游戏合集2024网游之武…
    普通游戏资讯 Microsoft将于2024年7月推出手机游戏商店凤血长歌黖沢爱
    普通游戏资讯 微软发力手机游戏!Xbox移动商店要来了:和大厂硬刚?少女…

    游戏信息资讯网声明:本站部分资源来源于网络,版权归原作者或者来源机构所有,如作者或来源机构不同意本站转载采用,请通知我们,我们将第一时间删除内容!