起因

事情的起因是在 Switch 上玩 Tetris99 游戏,由于不喜欢这种吃鸡的形式,只想玩小时候的那种掌机模式,于是想到可不可以自己做一个。

有了这个想法以后,打算使用 Rust + WASM,一方面是学习一下新技术,另一方面考虑到能直接在浏览器运行,可以跨平台,甚至可以在电视机上用浏览器打开网页就可以玩。

选定技术栈以后,在 Github 上搜了一下,发现早有人做了类似的工作,不过没关系,主要还是要自己实现一下。

几种技术方案

学习了一圈以后,理解了用 Rust + WASM 实现一个 web 游戏的大体思路。

首先,Rust 的 wasm-bindgen 库必不可少,这是连接 rust 代码和 wasm 之间的桥梁。

其次,既然是 web 游戏,那么免不了要画图,如何画图呢? 大家都不约而同的选择了 HTML 的 canvas,这是一种 html 标准自带的画图方式,比如用下面这样简单的代码,就能画一个矩形。

<html>
<body>

<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;">
</canvas>

</body>
</html>

所以,本质上我要做的就是用 Rust/WASM 代码 或者 JavaScript 代码,控制这个 <canvas id="myCanvas" ,并且定期刷新,这样就能显示动画效果了。 如果你是个 JavaScript 高手,并且打算全部用 JavaScript 实现,那么现在就可以开始动手了。

但如果是 Rust WASM 的方式,还需要考虑下是 纯 WASM 实现呢? 还是 WASM 实现核心算法逻辑,JavaScript 实现画图这样的组合方式?

纯 WASM 实现方式

参考 https://github.com/xuu/wasm-tetris

这种方式几乎不用写 HTML 和 JavaScript 代码,只需要 6 行 JavaScript 导入一个 WASM 文件即可。 见 examples/index.html

 <script type="module">
      import { make_tetris, default as init } from './pkg/wasm_tetris.js'
      async function run() {
        await init('./pkg/wasm_tetris_bg.wasm')
        document.getElementById('p-canvas').appendChild(make_tetris(15, 12, 25))
      }
      run()
    </script>

所有与 HTML Canvas 画图相关的部分也都是用 Rust 实现,当然,需要 Rust web-sys 库的支持,这个库封装了 Canvas 的 API, lib.rs 一开头就导入了相关函数。

use web_sys::{CanvasRenderingContext2d, FocusEvent, HtmlCanvasElement, KeyboardEvent};

具体的画图,以及接受键盘输入部分代码,可参考 lib.rs 的 setup() 函数。

这种方式的好处是存粹的 Rust 代码,3 年前的代码不用做任何改动,一次编译成功。

缺点是 Rust 控制 Canvas 的部分代码复杂,相比用 JavaScript 写麻烦了很多,而且 web_sys 的文档也不清楚,遇到问题也很少能搜到资料。

JavaScript + WASM 的方式

参考 https://github.com/liona24/wasm-tetris

这种方式比较适合初学者,即使我对 JavaScript 和 Rust 都不是特别熟悉,也很快看懂了 JS 和 Rust 各自的分工。

JavaScript 负责 web 页面上的所有操作,比如画图,键盘,游戏声音等。

Rust 负责计算游戏上 M*N 个方块在每时每刻的颜色,属于哪个 Block,该不该消失等等 游戏的核心逻辑。

最后把 M*N 的 matrix 序列化成一个 一维数组传递个 JavaScript,JS 负责画图。

这种方式的优点是 JavaScript 有很多成熟的 API 可以事半功倍,比如控制声音,一个声音文件截取其中不同的部分用于不同的游戏操作,我查了半天都不知道该如何用 Rust 实现。

缺点是要熟悉很多 JavaScript 的操作,特别是用 npm install modules,组织管理 package.json 文件,对于 JS 新手不友好,我花了很多时间了解和 debug npm 的这套流程。

后来,我才发现,这种方式来源于 rustwasm 的一个官方例子 https://github.com/rustwasm/wasm_game_of_life

在这个例子及其文档,有很多关于 Rust WASM 原理的解释,值得一看 https://rustwasm.github.io/docs/book/what-is-webassembly.html

我的实现

我选择了 JS + Rust 的实现方式,一方面因为纯 rust 实现 canvas 的部分太复杂,没看明白,另一方面是想学习一下 JS npm 这套东西。

最后,通过在 package.json 中添加 "NODE_ENV=production webpack, 生成了静态文件,这样就不需要像在开发过程中那样用必须运行 npm run 了。

Hash: 71da0bad7ab7f1e41732
Version: webpack 4.46.0
Time: 653ms
Built at: 03/19/2022 5:45:24 PM
                           Asset       Size  Chunks                         Chunk Names
                  0.bootstrap.js   18.8 KiB       0  [emitted]
126cf486be60b47a09e6.module.wasm   21.8 KiB       0  [emitted] [immutable]
                    bootstrap.js    338 KiB    main  [emitted]              main
                      index.html  547 bytes          [emitted]
/dist$ ls
0.bootstrap.js  126cf486be60b47a09e6.module.wasm  bootstrap.js  index.html  music.mp3

然后把整个 dist 文件夹拷贝到某个 web server 目录下,再用浏览器打开 URL,就可以在浏览器里面玩游戏了。

参考资料

  1. https://github.com/chvin/react-tetris
  2. https://github.com/xuu/wasm-tetris
  3. https://github.com/liona24/wasm-tetris
  4. https://github.com/rustwasm/wasm_game_of_life