前言 本文為「Rust and WebAssembly 」教學指南的學習筆記。
介紹 康威生命遊戲(Conway’s Game of Life),又稱康威生命棋,是英國數學家康威在 1970 年發明的細胞自動機。
規則一:任何活細胞周圍有低於兩個活細胞時,將因人口稀少而死亡。
規則二:任何活細胞周圍有兩至三個活細胞時,將存活至下個世代。
規則二:任何活細胞周圍有高於三個活細胞時,將因人口過剩而死亡。
規則四:任何死細胞周圍有剛好三個活細胞時,將因繁衍而成為活細胞。
例如,以下方世界(universe)為例:
1 2 3 4 5 6 🟦🟦🟦🟦🟦 🟦🟦🟧🟦🟦 🟦🟦🟧🟦🟦 🟦🟦🟧🟦🟦 🟦🟦🟦🟦🟦 1 2 3 4 5
座標 3-2
和 3-4
的活細胞,將因規則一死去;座標 3-3
的活細胞,將因規則二繼續存活;座標 2-3
和 4-3
的活細胞,將因規則四成為活細胞。
到了下個世代,細胞將形成以下狀態。
1 2 3 4 5 6 🟦🟦🟦🟦🟦 🟦🟦🟦🟦🟦 🟦🟧🟧🟧🟦 🟦🟦🟦🟦🟦 🟦🟦🟦🟦🟦 1 2 3 4 5
建立專案 建立專案。
1 cargo generate --git https://github.com/rustwasm/wasm-pack-template --name wasm-game-of-life
進入專案。
建立前端專案。
進入前端專案。
修改 package.json
檔。
1 2 3 4 5 6 7 { "dependencies" : { "wasm-game-of-life" : "file:../pkg" } , }
安裝依賴套件。
啟動前端專案。
架構設計 在程式中盡量最佳化以下兩件事情:
最小化從 WebAssembly 線性記憶體當中資料的存取。
最小化資料的序列化與反序列化。
因此,避免在每個世代把整個世界(universe)的物件複製進或複製出 WebAssembly 線性記憶體,而是使用扁平的陣列來表達當前世界的狀態,並使用 0
來表示死細胞,使用 1
來表示活細胞。
以下是一個高度為 4
且寬度為 4
的世界存在於記憶體中的樣子。
1 2 3 0 4 8 12 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 🔲 row1 | row2 | row3 | row4
為了找出指定行列的細胞陣列索引,可以使用以下公式:
1 index(row, column, universe) = row * width(universe) + column
實作後端 首先,修改 src/lib.rs
檔,定義一個 Cell
枚舉。這裡使用 #[repr(u8)]
屬性,用來表示每一個細胞都是一個位元組,並使用 0
來表示死細胞,使用 1
來表示活細胞,如此一來就可以使用加法來計算一個細胞的周圍存在多少活細胞。
1 2 3 4 5 6 7 8 9 use wasm_bindgen::prelude::*;#[wasm_bindgen] #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Cell { Dead = 0 , Alive = 1 , }
再來,定義一個 Universe
結構體,包含了其寬度、高度,和一組細胞陣列。
1 2 3 4 5 6 #[wasm_bindgen] pub struct Universe { width: u32 , height: u32 , cells: Vec <Cell>, }
接著為 Universe
結構體建立一個 get_index
方法,用來取得指定行列的細胞陣列索引。
1 2 3 4 5 6 7 impl Universe { fn get_index (&self , row: u32 , column: u32 ) -> usize { (row * self .width + column) as usize } }
再建立一個 live_neighbor_count
方法,用來取得一個細胞的周圍有多少活細胞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 impl Universe { fn live_neighbor_count (&self , row: u32 , column: u32 ) -> u8 { let mut count = 0 ; for delta_row in [self .height - 1 , 0 , 1 ].iter ().cloned () { for delta_col in [self .width - 1 , 0 , 1 ].iter ().cloned () { if delta_row == 0 && delta_col == 0 { continue ; } let neighbor_row = (row + delta_row) % self .height; let neighbor_col = (column + delta_col) % self .width; let idx = self .get_index (neighbor_row, neighbor_col); count += self .cells[idx] as u8 ; } } count } }
建立一個帶有 #[wasm_bindgen]
屬性的 Universe
實作,將方法暴露給前端。
1 2 3 4 #[wasm_bindgen] impl Universe { }
建立一個公開的 tick
方法,用來記算在下一個世代的細胞狀態。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #[wasm_bindgen] impl Universe { pub fn tick (&mut self ) { let mut next = self .cells.clone (); for row in 0 ..self .height { for col in 0 ..self .width { let idx = self .get_index (row, col); let cell = self .cells[idx]; let live_neighbors = self .live_neighbor_count (row, col); let next_cell = match (cell, live_neighbors) { (Cell::Alive, x) if x < 2 => Cell::Dead, (Cell::Alive, 2 ) | (Cell::Alive, 3 ) => Cell::Alive, (Cell::Alive, x) if x > 3 => Cell::Dead, (Cell::Dead, 3 ) => Cell::Alive, (otherwise, _) => otherwise, }; next[idx] = next_cell; } } self .cells = next; } }
為 Universe
結構體實作一個 fmt
方法,用來渲染出人類可讀的方塊圖形,並且可以使用 to_string
方法呼叫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::fmt;impl fmt ::Display for Universe { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { for line in self .cells.as_slice ().chunks (self .width as usize ) { for &cell in line { let symbol = if cell == Cell::Dead { '◻' } else { '◼' }; write! (f, "{}" , symbol)?; } write! (f, "\n" )?; } Ok (()) } }
再為 Universe
結構體建立一個公開的 new
方法當作建構子,用來初始化一個新的世界。並建立一個 render
方法,用來渲染方塊圖形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #[wasm_bindgen] impl Universe { pub fn new () -> Universe { let width = 64 ; let height = 64 ; let cells = (0 ..width * height) .map (|i| { if i % 2 == 0 || i % 7 == 0 { Cell::Alive } else { Cell::Dead } }) .collect (); Universe { width, height, cells, } } pub fn render (&self ) -> String { self .to_string () } }
執行編譯。
實作前端 修改 www/index.html
檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <title > Hello wasm-pack!</title > <style > body { position : absolute; top : 0 ; left : 0 ; width : 100% ; height : 100% ; display : flex; flex-direction : column; align-items : center; justify-content : center; } </style > </head > <body > <pre id ="game-of-life-canvas" > </pre > <script src ="./bootstrap.js" > </script > </body > </html >
修改 www/index.js
檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { Universe } from "wasm-game-of-life" ;const pre = document .getElementById ("game-of-life-canvas" );const universe = Universe .new ();const renderLoop = ( ) => { pre.textContent = universe.render (); universe.tick (); requestAnimationFrame (renderLoop); }; requestAnimationFrame (renderLoop);
啟動服務。
前往 http://localhost:8080 瀏覽。
重構 修改後端的 src/lib.rs
檔,建立以下公開方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #[wasm_bindgen] impl Universe { pub fn width (&self ) -> u32 { self .width } pub fn height (&self ) -> u32 { self .height } pub fn cells (&self ) -> *const Cell { self .cells.as_ptr () } }
修改 www/index.html
檔,將渲染的節點改為畫布。
1 2 3 4 <body > <canvas id ="game-of-life-canvas" > </canvas > <script src ="./bootstrap.js" > </script > </body >
修改 www/index.js
檔,引入 wasm_game_of_life_bg
檔的 memory
模組,直接存取指向細胞的指針,並寫入 Uint8Array
陣列使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import { Universe , Cell } from "wasm-game-of-life" ;import { memory } from "wasm-game-of-life/wasm_game_of_life_bg" ;const CELL_SIZE = 5 ;const GRID_COLOR = "#CCCCCC" ;const DEAD_COLOR = "#FFFFFF" ;const ALIVE_COLOR = "#000000" ;const universe = Universe .new ();const width = universe.width ();const height = universe.height ();const canvas = document .getElementById ("game-of-life-canvas" );canvas.height = (CELL_SIZE + 1 ) * height + 1 ; canvas.width = (CELL_SIZE + 1 ) * width + 1 ; const ctx = canvas.getContext ('2d' );const renderLoop = ( ) => { universe.tick (); drawGrid (); drawCells (); requestAnimationFrame (renderLoop); }; const drawGrid = ( ) => { ctx.beginPath (); ctx.strokeStyle = GRID_COLOR ; for (let i = 0 ; i <= width; i++) { ctx.moveTo (i * (CELL_SIZE + 1 ) + 1 , 0 ); ctx.lineTo (i * (CELL_SIZE + 1 ) + 1 , (CELL_SIZE + 1 ) * height + 1 ); } for (let j = 0 ; j <= height; j++) { ctx.moveTo (0 , j * (CELL_SIZE + 1 ) + 1 ); ctx.lineTo ((CELL_SIZE + 1 ) * width + 1 , j * (CELL_SIZE + 1 ) + 1 ); } ctx.stroke (); }; const getIndex = (row, column ) => { return row * width + column; }; const drawCells = ( ) => { const cellsPtr = universe.cells (); const cells = new Uint8Array (memory.buffer , cellsPtr, width * height); ctx.beginPath (); for (let row = 0 ; row < height; row++) { for (let col = 0 ; col < width; col++) { const idx = getIndex (row, col); ctx.fillStyle = cells[idx] === Cell .Dead ? DEAD_COLOR : ALIVE_COLOR ; ctx.fillRect ( col * (CELL_SIZE + 1 ) + 1 , row * (CELL_SIZE + 1 ) + 1 , CELL_SIZE , CELL_SIZE ); } } ctx.stroke (); }; drawGrid ();drawCells ();requestAnimationFrame (renderLoop);
重新執行編譯。
重新啟動服務。
前往 http://localhost:8080 瀏覽。
程式碼
參考資料