本文最后更新于2 年前,文中所描述的信息可能已发生改变。
这是一篇水文。
为什么写了这个么东西呢?
这是个好问题,动机永远是关键。第一点呢,我这个人玩心很重,能不做任务就绝对不会做,用一切时间来玩(或者睡),在玩的过程中逐渐有了自己做个游戏的想法。正巧托室友的福,我发现了这个项目合集:Build your own x,里面有好几个小游戏的实现教程。至于第二点,我最近对 Rust 比较感兴趣,还心血来潮用它写了个课程实验。看到合集里有 Rust 的游戏,我觉得有点惊喜,就选了一个跟着做一下。
就是这个,用 tcod-rs 来做的 Roguelike。之前玩 Hades 很上头,发现了 Roguelike 的吸引力,就它了。
那么,一知半解的 Rust + Roguelike 游戏之旅就这么拉开了序幕。
当然不会贴具体的代码和过程,毕竟基本上是按教程做的,不会有太多差异。
什么是 Roguelike?
根据维基百科的描述,Roguelike 游戏有一些明确的特点,包括随机生成地牢、使用永久死亡机制和回合制等等(柏林准则)。在最直观的元素——画面上,早期的 Roguelike 游戏往往使用一连串的 ASCII 或者 ANSI 字符来表示。其他的也没必要说太多,知道这些就够用了。
不过看了才知道,像 Hades 这样的游戏已经比传统的 Roguelike 简单了很多。这算是降低了一点门槛吧。
游戏的逻辑
真正上手做游戏之前最大的困惑就是游戏的运行逻辑该如何实现,各种元素应该如何放到代码里面组成想要的那个东西。趁着现在还没忘完,来梳理一下。
游戏里的各种物体——组装!
游戏里最直观的就是能看到、能互动的各种物体了。它们会有很多不同,比如一个敌人单位和一个友好的 NPC 的属性、行为等当然会不一样。但是它们也会有很多相同之处,比方说敌人和 NPC 都可能会移动,这部分代码很可能一样,不太可能把它们分别写好几遍。怎么在代码层面描述这些不同和共同之处?
之前尝试用 Java 写的时候,可以利用面向对象的特点,把一些共同的部分——例如 move ——抽象成接口,不同的物体就可以分别实现这些接口,实现各种功能自由组合的直观效果。然而 Rust 似乎没有一般意义上的面向对象这个概念(它甚至都不是个面向对象的语言),所幸通过结构体的一些操作能实现一样的效果。利用特性可以实现一些接口,在子类中定义父类的实例可以变相实现继承。
比方说有一个具有战斗相关属性的类型 Fighter
,如果是 Java 就可以把它写成接口,放在子类里面实现,而 Rust 要直接写出实现,在 Object
里定义一个 Option<Fighter>
,当这个 Fighter
有确定的实例时它就是一个战斗单位(玩家、敌人等)。像是用各种零件组装出一个完整的机器一样。
这样一来就清楚了,可以把各种功能属性单独包装起来,用这种组合的方式实现具有复杂属性的类型。
游戏循环——性能黑洞?
看过一些有关游戏实现的文章之后,我悟了:游戏可以在一个循环中进行,尤其是这种玩家行动-怪物行动的模式,实在是再合适不过了。
开始游戏其实就是进入程序的主循环,我按照教程做下来,主循环会依次完成这些任务:
- 清除前一帧
- 检测、记录鼠标键盘输入事件
- 重新绘制画面
- 计算经验值
- 根据玩家行动退出游戏或使怪物行动
具体每一步怎么实现就不赘述了,展示信息、修改属性数值什么的还算比较常规。
按照这样的思路,游戏的每一次循环都会对所有数据来一次检查,如果是比较复杂的游戏,计算量应该会很大。游戏吃性能的原因之一?
关于 Rust 本身
在整个实现过程中我又把之前忘掉的 Rust 相关的注意事项稍微拾起了一点,见识到了之前没来得及看的更高级操作,果然要把工具用得更好就是要多用。
derive
按照比较官方的说法,derive 应该是用来自动实现一些 trait (特性?),比方说用了 #[derive(Debug)]
,就不用再为结构体手动写 std::fmt::Debug
的实现,算是非常方便了。还可以自己写 derive 宏。
备注:Macros
所有权,所有权,还是所有权
Rust 对变量的生命周期控制得很严格,之前没怎么写 Rust 的我刚上手写这个游戏时总是碰一鼻子灰。回头又看了看 Rust “圣经”里面关于所有权和生命周期的章节,Rust 中变量与数据的交互包括移动和克隆,值得注意。不过这方面的错误应该过不了编译器那一关,多写写自然也就不会犯了。
关于所有权,比较有意思的还有 Copy
trait、返回值转移所有权以及引用借用等。
话说为什么对所有权有这么多规定呢?可能和内存管理有关?先按下不表,之后再说(懒癌发作)。
那么就先到这里,写这篇水文距离我跟着教程做完已经有一段时间了,也忘了很多想写进来的东西,有一点遗憾。