添加交互性
我们将继续探索 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) 。