添加交互性
我们将继续探索 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;// 这个函数与之前的一样, 除了把`requestAnimationFrame`的结果// 分配到 `animationId`.const renderLoop = () => {universe.tick();drawCells();drawGrid();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 文件中添加 toggle 函数到 Cell。
impl Cell {fn toggle(&mut self) {*self = match *self {Cell::Dead => Cell::Alive,Cell::Alive => Cell::Dead,};}}
要在给定的行和列上切换单元格的状态,我们将行和列对转换为单元格向量的索引,并对该索引处的单元格调用toggle方法:
/// 公共方法,导出为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();}}
这个方法是在 impl 中定义的,impl 用 #[wasm#u bindgen] 注释,以便JavaScript调用它。
在 wasm-game-of-life/www/index.js 中我们会监听 <canvas> 的点击事件,
将 click 事件的相对坐标转换为画布相对坐标,然后转换为行和列,调用 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);drawCells();drawGrid();});
在 wasm-game-of-life 下使用 wasm-pack build 重新编译,并刷新 http://localhost:8080/。
现在可以通过点击单元格并切换它们的状态来绘制自己的模式。
练习
- 使用一个
<input type="range">小部件来控制每个动画帧出现多少个tick。
- 添加一个按钮,在单击时将宇宙重置为随机初始状态。另一个按钮将宇宙重置为所有死细胞。
- Ctrl + Click,在目标细胞的中心插入一个滑翔机(glider) 。 Shift + Click,插入一个脉冲星(pulsar) 。
