添加交互性

我们将继续探索 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 + 点击 时,插入一个脉冲星。