← 返回文章列表
Rust + WASM 贪食蛇开发手记
用 WASM 写贪食蛇是个经典练手项目,看起来简单,但从 Setup 到跑通 Canvas 渲染,每一步对 Rust 新手都是坑。
技术选型
核心依赖只有三个:
wasm-pack— 编译 Rust → WASM + JS 胶水代码wasm-bindgen— Rust ↔ JS 互操作桥web-sys— 浏览器 API 的 Rust 绑定(Canvas / requestAnimationFrame / KeyboardEvent)
没有用 yew 或 seed 之类的高层框架——只是想把 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.toml 后 wasm-pack build 偶尔不生效。确认是 target/wasm32-unknown-unknown 缓存没清。加 --release 前先 rm -rf target 最稳。
从 Go 到 Rust 的思维转变
Go 里写游戏循环惯用的 time.Ticker + goroutine 模式,在 Rust 里变成 requestAnimationFrame 回调 + Closure。最不习惯的是 Closure 的所有权——必须显式 .forget() 告诉 Rust "这个闭包的生命周期请忽略,浏览器会管"。
写完这个小项目,对 Box、Rc、RefCell 的理解一下子通了。下个目标是用 bevy 引擎做个俯视角射击游戏。