添加交互性
我们将继续探索 JavaScript 和 WebAssembly 接口,为我们的生命游戏实现添加一些交互功能。我们将允许用户通过点击来切换单元格的生死状态,并允许暂停游戏,这使得绘制单元格图案变得容易得多。
暂停和恢复游戏
让我们添加一个按钮来切换游戏是否正在播放或暂停。在 wasm-game-of-life/www/index.html
中,在 <canvas>
之前添加按钮
<button id="play-pause"></button>
在 wasm-game-of-life/www/index.js
JavaScript 中,我们将进行以下更改
-
跟踪最新调用
requestAnimationFrame
返回的标识符,以便我们可以通过使用该标识符调用cancelAnimationFrame
来取消动画。 -
当点击播放/暂停按钮时,检查我们是否拥有排队动画帧的标识符。如果我们有,那么游戏当前正在播放,我们希望取消动画帧,以便不再调用
renderLoop
,从而有效地暂停游戏。如果我们没有排队动画帧的标识符,那么我们当前处于暂停状态,我们希望调用requestAnimationFrame
来恢复游戏。
由于 JavaScript 驱动 Rust 和 WebAssembly,所以我们只需要做这些,不需要更改 Rust 源代码。
我们引入 animationId
变量来跟踪 requestAnimationFrame
返回的标识符。当没有排队动画帧时,我们将此变量设置为 null
。
let animationId = null;
// This function is the same as before, except the
// result of `requestAnimationFrame` is assigned to
// `animationId`.
const renderLoop = () => {
drawGrid();
drawCells();
universe.tick();
animationId = requestAnimationFrame(renderLoop);
};
在任何时刻,我们都可以通过检查 animationId
的值来判断游戏是否处于暂停状态。
const isPaused = () => {
return animationId === null;
};
现在,当点击播放/暂停按钮时,我们检查游戏当前是否处于暂停或播放状态,并分别恢复 renderLoop
动画或取消下一个动画帧。此外,我们更新按钮的文本图标以反映按钮在下次点击时将执行的操作。
const playPauseButton = document.getElementById("play-pause");
const play = () => {
playPauseButton.textContent = "⏸";
renderLoop();
};
const pause = () => {
playPauseButton.textContent = "▶";
cancelAnimationFrame(animationId);
animationId = null;
};
playPauseButton.addEventListener("click", event => {
if (isPaused()) {
play();
} else {
pause();
}
});
最后,我们之前通过直接调用 requestAnimationFrame(renderLoop)
来启动游戏及其动画,但我们希望用调用 play
来替换它,以便按钮获得正确的初始文本图标。
// This used to be `requestAnimationFrame(renderLoop)`.
play();
刷新 http://localhost:8080/,我们现在应该可以通过点击按钮来暂停和恢复游戏了!
在 "click"
事件上切换单元格的状态
现在我们可以暂停游戏了,是时候添加通过点击来改变单元格的能力了。
切换单元格是指将它的状态从存活切换到死亡,或从死亡切换到存活。在 wasm-game-of-life/src/lib.rs
中的 Cell
中添加一个 toggle
方法
# #![allow(unused_variables)] #fn main() { impl Cell { fn toggle(&mut self) { *self = match *self { Cell::Dead => Cell::Alive, Cell::Alive => Cell::Dead, }; } } #}
要切换给定行和列的单元格状态,我们将行和列对转换为单元格向量中的索引,并调用该索引处单元格的 toggle
方法
# #![allow(unused_variables)] #fn main() { /// Public methods, exported to JavaScript. #[wasm_bindgen] impl Universe { // ... pub fn toggle_cell(&mut self, row: u32, column: u32) { let idx = self.get_index(row, column); self.cells[idx].toggle(); } } #}
此方法定义在用 #[wasm_bindgen]
注释的 impl
块中,以便 JavaScript 可以调用它。
在 wasm-game-of-life/www/index.js
中,我们监听 <canvas>
元素上的点击事件,将点击事件的页面相对坐标转换为画布相对坐标,然后转换为行和列,调用 toggle_cell
方法,最后重新绘制场景。
canvas.addEventListener("click", event => {
const boundingRect = canvas.getBoundingClientRect();
const scaleX = canvas.width / boundingRect.width;
const scaleY = canvas.height / boundingRect.height;
const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
const canvasTop = (event.clientY - boundingRect.top) * scaleY;
const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);
universe.toggle_cell(row, col);
drawGrid();
drawCells();
});
在 wasm-game-of-life
中使用 wasm-pack build
重新构建,然后再次刷新 http://localhost:8080/,我们现在可以通过点击单元格并切换其状态来绘制自己的图案了。
练习
-
引入一个
<input type="range">
小部件来控制每个动画帧发生的滴答次数。 -
添加一个按钮,当点击时将宇宙重置为随机初始状态。另一个按钮将宇宙重置为所有死亡的单元格。
-
在
Ctrl + 点击
时,在目标单元格中心插入一个 滑翔机。在Shift + 点击
时,插入一个脉冲星。