增加互動性
我們將透過為生命遊戲實作新增一些互動功能,繼續探索 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();
重新整理 https://127.0.0.1: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, }; } } #}
要切換某個單元在特定列和行的狀態,我們可以將列和行對應到 cells 向量的索引,並呼叫那個索引的單元的 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
重新建置,然後再次重新整理 https://127.0.0.1:8080/,我們現在就可以透過按一下單元來切換它們的狀態,來繪製我們自己的圖形了。
練習
-
加入一個
<input type="range">
小工具,用來控制每次動畫畫面發生多少次刻度。 -
增加一個按鈕,當按下時將宇宙重置為隨機的初始狀態。另一個按鈕將宇宙重置為所有單元皆死亡的狀態。
-
在
Ctrl + 按下
時,將一個 滑翔機 置中於目標單元。在Shift + 按下
時,加入一個脈衝星。