← 返回文章列表

Rust + WASM 贪食蛇开发手记

2026-07-13 · Rust · WASM · 游戏

用 WASM 写贪食蛇是个经典练手项目,看起来简单,但从 Setup 到跑通 Canvas 渲染,每一步对 Rust 新手都是坑。

技术选型

核心依赖只有三个:

  • wasm-pack — 编译 Rust → WASM + JS 胶水代码
  • wasm-bindgen — Rust ↔ JS 互操作桥
  • web-sys — 浏览器 API 的 Rust 绑定(Canvas / requestAnimationFrame / KeyboardEvent)

没有用 yewseed 之类的高层框架——只是想把 Rust 和原生浏览器 API 之间的边界摸透。

架构

// snake.rs — 纯 Rust 逻辑,不依赖任何浏览器 API
pub struct SnakeGame {
    snake: VecDeque<(i32, i32)>,
    food: (i32, i32),
    direction: Direction,
    grid_w: i32,
    grid_h: i32,
    state: GameState,
}

impl SnakeGame {
    pub fn tick(&mut self) { /* … */ }
    pub fn change_direction(&mut self, d: Direction) { /* … */ }
    pub fn grid(&self) -> &[[Cell; GRID_W]; GRID_H] { /* … */ }
}

游戏逻辑层和渲染层完全分离。所有 pub 方法通过 #[wasm_bindgen] 暴露给 JavaScript:

#[wasm_bindgen]
impl SnakeGame {
    pub fn new(w: u8, h: u8) -> SnakeGame { /* … */ }
    pub fn tick(&mut self) -> bool { /* … */ }
    pub fn turn(&mut self, key: u8) { /* … */ }
    pub fn render_buffer(&mut self) -> Vec<u8> { /* … */ }
}

踩坑记录

WASM 堆内存与 Vec<u8>

最开始想法是每帧返回一个 Vec<Vec<Cell>>,结果发现 WASM 跨越 JS ↔ Rust 边界时,嵌套 Vec 的序列化开销大到离谱。改用一维 Vec<u8> 拍平,JS 侧用 Uint8Array 读,帧时间从 8ms 降到 0.3ms。

wasm-pack 构建缓存

改完 Cargo.tomlwasm-pack build 偶尔不生效。确认是 target/wasm32-unknown-unknown 缓存没清。加 --release 前先 rm -rf target 最稳。

从 Go 到 Rust 的思维转变

Go 里写游戏循环惯用的 time.Ticker + goroutine 模式,在 Rust 里变成 requestAnimationFrame 回调 + Closure。最不习惯的是 Closure 的所有权——必须显式 .forget() 告诉 Rust "这个闭包的生命周期请忽略,浏览器会管"。

写完这个小项目,对 BoxRcRefCell 的理解一下子通了。下个目标是用 bevy 引擎做个俯视角射击游戏。