起因
事情的起因是在 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,就可以在浏览器里面玩游戏了。