添加交互性

我们将继续探索 JavaScript 和 WebAssembly 界面,为我们的游戏实现添加一些交互功能。 我们将允许用户通过点击来切换一个单元格是活的还是死的,并允许暂停游戏,这使得绘制单元格模式变得更加容易。

暂停和恢复游戏

我们添加一个按钮来切换游戏的运行或暂停状态。要访问 wasm-game-of-life/www/index.html,请在 <canvas> 上方添加按钮:

  1. <button id="play-pause"></button>

wasm-game-of-life/www/index.js JavaScript 中,我们将进行以下更改:

  • 跟踪最新调用 requestAnimationFrame 返回的标识符, 以便我们可以以此,调用 cancelAnimationFrame 来取消那个标识符动画。
  • 单击播放/暂停按钮时,检查是否有排队动画帧的标识符。 如果我们这样做,那么游戏当前正在运行,我们想取消动画帧,这样 renderLoop 就不会被再次调用,从而有效地暂停游戏。 如果队列中的动画帧没有标识符,则当前已暂停,并希望调用 requestAnimationFrame 来恢复游戏。

因为 JavaScript 驱动了 Rust 和 WebAssembly,所以我们只需要这样做,而不需要更改 Rust 的源代码。

我们引入 animationId 变量来跟踪 requestAnimationFrame 返回的标识符。 当运行动画帧时,我们将此变量设置为 null

  1. let animationId = null;
  2. // 这个函数与之前的一样, 除了把`requestAnimationFrame`的结果
  3. // 分配到 `animationId`.
  4. const renderLoop = () => {
  5. universe.tick();
  6. drawCells();
  7. drawGrid();
  8. animationId = requestAnimationFrame(renderLoop);
  9. };

我们可以在任何时候通过检查 animationId 的值来判断游戏是否暂停:

  1. const isPaused = () => {
  2. return animationId === null;
  3. };

现在,当单击“播放/暂停”按钮时,我们将检查游戏当前是否暂停或正在播放,并分别恢复 renderLoop 动画或取消下一个动画帧。 此外,我们还更新按钮的文本图标,以反映下一步单击按钮时将执行的操作。

  1. const playPauseButton = document.getElementById("play-pause");
  2. const play = () => {
  3. playPauseButton.textContent = "⏸";
  4. renderLoop();
  5. };
  6. const pause = () => {
  7. playPauseButton.textContent = "▶";
  8. cancelAnimationFrame(animationId);
  9. animationId = null;
  10. };
  11. playPauseButton.addEventListener("click", event => {
  12. if (isPaused()) {
  13. play();
  14. } else {
  15. pause();
  16. }
  17. });

最后,我们以前是通过直接调用 requestAnimationFrame(renderLoop) 来启动游戏及其动画的,但是我们希望用调用 play 来替换它,以便按钮获得正确的初始文本图标。

  1. // This used to be `requestAnimationFrame(renderLoop)`.
  2. play();

刷新 http://localhost:8080 我们现在应该可以通过点击按钮暂停和运行游戏了!

Click 事件中切换单元格状态

现在我们可以暂停游戏了,是时候通过点击细胞来增加变异细胞的能力了。

变异细胞就是把它的状态从活转死或从死转活。在 wasm-game-of-life/src/lib.rs 文件中添加 toggle 函数到 Cell

  1. impl Cell {
  2. fn toggle(&mut self) {
  3. *self = match *self {
  4. Cell::Dead => Cell::Alive,
  5. Cell::Alive => Cell::Dead,
  6. };
  7. }
  8. }

要在给定的行和列上切换单元格的状态,我们将行和列对转换为单元格向量的索引,并对该索引处的单元格调用toggle方法:

  1. /// 公共方法,导出为JavaScript。
  2. #[wasm_bindgen]
  3. impl Universe {
  4. // ...
  5. pub fn toggle_cell(&mut self, row: u32, column: u32) {
  6. let idx = self.get_index(row, column);
  7. self.cells[idx].toggle();
  8. }
  9. }

这个方法是在 impl 中定义的,impl#[wasm#u bindgen] 注释,以便JavaScript调用它。

wasm-game-of-life/www/index.js 中我们会监听 <canvas> 的点击事件, 将 click 事件的相对坐标转换为画布相对坐标,然后转换为行和列,调用 toggle_cell 方法,最后重绘场景。

  1. canvas.addEventListener('click', event => {
  2. const boundingRect = canvas.getBoundingClientRect();
  3. const scaleX = canvas.width / boundingRect.width;
  4. const scaleY = canvas.height / boundingRect.height;
  5. const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
  6. const canvasTop = (event.clientY - boundingRect.top) * scaleY;
  7. const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
  8. const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);
  9. universe.toggle_cell(row, col);
  10. drawCells();
  11. drawGrid();
  12. });

wasm-game-of-life 下使用 wasm-pack build 重新编译,并刷新 http://localhost:8080/。 现在可以通过点击单元格并切换它们的状态来绘制自己的模式。

练习

  • 使用一个 <input type="range"> 小部件来控制每个动画帧出现多少个 tick
  • 添加一个按钮,在单击时将宇宙重置为随机初始状态。另一个按钮将宇宙重置为所有死细胞。