用Rust写个游戏——糯米团子历代级(the) Gnorp Apologue
白炎拉力
2024年11月01日 11:44
收录于文集
共1篇

该文章源自Writing a game in Rust — (the) Gnorp Apologue : r/rust (reddit.com): https://www.reddit.com/r/rust/comments/18ul125/writing_a_game_in_rust_the_gnorp_apologue/,由白炎拉力翻译。

三年前,我打算学习Rust来自娱自乐。这个决定让我完成了学习一个全新编程语言的一整个历程,并且最后将游戏发布在了Steam上。你可以在(Reddit)上阅读这个帖子,或者在我的网站上阅读:gnorp.dev: https://gnorp.dev/news/10-Hats-And-Rust/

开端

我热爱游戏,并且已经考虑好一段时间来做一个出来。我的文档系统中留存着未完成的项目、艺术画、点子和半成品原型。这段时间,我在大量的不同框架、引擎与编程语言之间流转。Unity,Godot和Unreal因为它们以编辑器为中心的特性,不是很吸引我。我更倾向于遨游在文本编辑器中。所以我开始使用LÖVE,一款2D游戏引擎。我用LÖVE来制作原型,LÖVE也没有让我失望。然而,我对于(这个用LÖVE制作的)原型的兴趣却渐渐变淡。一部分是源自游戏理念,另一部分是我脑海中有声音一直说:“Lua不够快”。

时间穿梭,而我仍想要做游戏。有一天我决定拿Rust尝尝鲜。Rust是我听说过但是从来没有了解过的编程语言。稍微了解之后,我被它能舒适地写很快的代码的想法吸引住了。我年年都从Stack Overflow的排行榜中听闻Rust,也通过Reddit上关于一个用C++编写项目的AMA了解到它。在这些地方有一个问题是“你觉得Rust怎么样”。在问题的回复中,开发者说只要C++能够干相同甚至更多的事情,他就没有想过要换另外一门语言。在这句后面跟着一句“我用C++很少有问题”。这句话激起我的兴趣,因为我知道根本不可能“用C++又很少问题”。那这只能说明他的观点有问题了。

结果证明,即便在我笨笨的、有缺陷的逻辑下,尝试使用Rust也是有意思的想法。我超爱这门语言,还有它的文档和工具链。我花了几个小时来探索了一下Rust,然后就坚定地决定使用它来做游戏了。通过读了Rust的书,我的确做到了。

在读了12分钟Rust的书之后,是时候让一无所知的我投入实践了。我调研了游戏框架和引擎。因为Rust是一门相对新的语言所以框架和引擎的选择面并没有其他成熟语言这么广。不过还好,我很乐意用我的激情去解决这个问题。我找到了GGEZ,Coffee,Amethyt,Bevy(当时刚刚发布),还有其他很多。最后我选择了Joe Clay搞的Tetra。这是一个文档齐全,基于SDL2构建的超棒框架。它绘制图片很快,而且有所有我需要的功能。

那么是时候回到Test Project 1了。它可以帮助我熟悉Rust构建软件的流程。Test Project 1是基于一款我在12小时内给我女朋友(现在是妻子)做的一款小游戏。这游戏的特点是有一堆糯米团子,和一个被团子攻击就会产生小石头的岩石。看着团子们干活是如此享受,看着屏幕上大量小石头,石堆也是如此愉悦。我女朋友(现在是妻子)玩了之后很享受这个游戏,我的朋友们也是如此。

是时候基于Test Project 1来做一个真正的游戏了。最后它就成为了糯米团子历代级(以下简称糯米团子,译者注)。

我最喜欢Rust的点是什么?

我最喜欢的是enumerators,和pattern matching带来的各种眼神。真是不可思议的强大系统,让我能够在需要引入新行为的时候写出很容易拓展的可靠代码。

编译器也是很棒的工具。当设计游戏的时候,重构是必然的流程。你做了点雏形,你想要拓展,然后就需要修改点什么属性,亦或者这个单位现在能够干这个,诸如此类。你抽出代码,然后把它改成别的,引入新的系统,把他们放在一起亦或是分开——无所谓怎么折腾,编译器会出手。IT JUST WORKS.我发现能够专注于只写游戏相关内容真的会生产力爆棚。毕竟做context switching的代价很高(指从游戏内容中分心关心其他内容,译者注)。而我仅仅有几次被语言分心或者神秘报错信息的情况。

游玩部分

Tetra,加上我的一点点小改动,可以让游戏循环是固定的time step loop。什么意思呢?这意味着游戏是运行离散的帧上面,而不用考虑每次Update的delta time。我发现这事写游戏最好的方式,因为它可以让你用很少的bug来写基于物理的行为。而游戏的绘制则会在不同步数之间做插值(interpolate),所以不管游戏(物理)帧数多少帧,游戏总能看起来比较丝滑。

游戏中有很多不同的单位,它们都有不同的行为。用Rust写这些行为真的非常丝滑,多亏了强大的enumerators。

当我创建一个新的单位,我会使用组合的方式来写:

代码块
Rust
自动换行
复制代码
//from another module
struct VelPos {
    position: Position, 
    velocity: Vec2<f32>,
}

struct Slammer {
    velpos: VelPos, 
    state: State, 
}

enum State {
    Attacking, 
    FlyingAway, 
}

impl Slammer {
    fn update(&mut self, rock: &mut Rock) {
        self.velpos.position.apply_velocity(); 

        match state {
             State::Attacking => {
                ...
             }
             State::FlyingAway => {
                ...
             }
        }
    }
}
复制成功

我从来就不是继承模式的粉丝,然后发现Rust中的组合模式相比之下可以更佳灵活。

用户界面 User interface

写糯米团子体验上最糟糕的部分毫无疑问是UI。我决定不用UI库,而是自己完全掌握UI的逻辑与外观。这可能是个错误,或许我应该用类似Egui或者别的可用的UI库。

有一部分UI遵循着立刻重绘的范式,而另一些则是在需要的时候重绘。绘制文本代价昂贵,而我试图最小化这个代价,但这是我在更新和完善这个游戏的时候要做的事情了。

说回Rust上用UI,很难说Rust是在帮忙还是在帮倒忙。根据软件架构的不同,处理UI的时候Rust的borrow checker可能还是有点麻烦的。好在尝试多次之后,我还是解决了这个问题。

性能

作为热爱游戏的玩家,我始终对性能感兴趣。当我是的小孩,没有非常强大的电脑的时候,我玩游戏时总是会关注性能。能够单纯在什么东西上运行的游戏总会让我印象深刻。因此这事我在糯米团子中设定的一个目标——性能。

以下是我做的一些让我达成目标的事情,有一些事情我超额完成了:

  • 我尝试写健壮的代码,少用临时方案

  • 不使用dynamic dispatch

  • 不使用reference counting

  • 让单位在内存上保持轻量

  • 减少UI渲染绘制次数(尽管还是很高)

整个游戏逻辑都是运行在单线程上的,我们也没啥必要把游戏搞成多线程,尽管游戏中会有上千个单位和物体。我没有用任何unsafe代码,除了一处完全安全且没有必要的unchecked unwrap操作。

游戏运行很好,无须多言。SDL2让游戏绘制很快,Rust让游戏逻辑又快又可靠。

编译

这里能说的不多。Rust编译时间相对较慢,但与此同时我发现持续编译挺不高效,因此我努力只在需要测试的时候才进行编译。

总结

我的Rust旅程还不错。我在这个语言中学到很多,不过仍有大量Rust特性我没有充分利用,这也是因为我用不上。我应该向游戏开发者们推荐Rust吗?是的,肯定。设计游戏最难的部分不是写代码,而是一个好点子。Rust能让你舒适地实现好点子,正如你预期的那样,而且很快。

Aftermath

12月14日我发布了我的第一款游戏,编译发行版本花了我20分钟。游戏到目前已经收获了1500个评论(现在是5600个,译者注)。这远比我期望的成功。尽管我尝试不去查看,但是游戏还是卖了数万份。这便是我投靠Rust,和它的社区,还有Joe Clay的Tera,带来的巨大成功。

感谢阅读!