增加互動性

我們將透過為生命遊戲實作新增一些互動功能,繼續探索 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 + 按下 時,加入一個脈衝星。