這是關於使用 Rust 和 WebAssembly 的未發布文件,已發布的文件可於 Rust 和 WebAssembly 文件主網站上找到 。在此記錄的功能可能不適用於 Rust 和 WebAssembly 工具的已發佈版本。

增加互動性

我們將透過為康威的生命遊戲實作中新增一些互動功能,進一步探索 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,
        };
    }
}
#}

切換特定行和欄的儲存格狀態,我們將行和欄的配對轉換為儲存格向量中的索引,然後在該索引的儲存格上呼叫 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> 元素上聆聽 click 事件,將 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);

  drawGrid();
  drawCells();
});

wasm-game-of-life 中以 wasm-pack build 重新建置,接著再重新整理 https://127.0.0.1:8080/,現在我們可以按一下儲存格並切換其狀態,就能繪製自己的模式了。

練習

  • 引入一個 <input type="range"> 小工具,用以控制每個動畫畫面的刻度數量。

  • 新增一個按鈕,在按一下按鈕時將宇宙重設為隨機的初始狀態。再新增一個按鈕,在按一下按鈕時將宇宙重設為所有的死儲存格。

  • Ctrl + 按一下,插入以目標單元格為中心的 滑翔機。在 Shift + 按一下,插入脈衝星。