Rust 開發 WebAssembly 生命遊戲
使用 Rust 開發 WebAssembly,你需要有一定的編程基礎。需要了解 Rust,瞭解 JavaScript,HTML,和 CSS。
本節以生命遊戲爲例進行講解。
1,安裝開發環境
安裝 Rust 工具鏈,安裝 Rust 參考官網,
https://www.rust-lang.org/;
安裝 wasm 目標
rustup target add wasm32-unknown-unknown
安裝 wasm-pack 工具,下載地址:
https://rustwasm.github.io/wasm-pack/installer/
安裝 cargo-generate
cargo install cargo-generate
安裝 npm
https://www.npmjs.com/get-npm
2,初始化項目
使用模板初始化項目,項目名爲 wasm-game-of-life
cargo generate --git https://github.com/rustwasm/wasm-pack-template
項目生成的目錄結構如下:
wasm-game-of-life/
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
├── lib.rs
└── utils.rs
初始的 lib 模板代碼:
#![allow(unused_variables)]
fn main() {
mod utils;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, wasm-game-of-life!");
}
}
編譯項目,使用命令:
wasm-pack build
當編譯成功後,會生成 pkg 文件夾,裏面包含的內容:
pkg/
├── package.json
├── README.md
├── wasm_game_of_life_bg.wasm
├── wasm_game_of_life.d.ts
└── wasm_game_of_life.js
將生成的 wasm 文件放置在 Web 項目中,Web 項目也有模板,在項目的根目錄中,使用如下命令生成模板:
npm init wasm-app www
生成的 Web 項目模板目錄結構如下:
wasm-game-of-life/www/
├── bootstrap.js
├── index.html
├── index.js
├── LICENSE-APACHE
├── LICENSE-MIT
├── package.json
├── README.md
└── webpack.config.js
Web 項目的運行可以使用下面的命令:
npm install
npm run start
這樣就可以在本地進行開發調試了。
3,開始我們的項目編寫。
生命遊戲的遊戲規則:生命遊戲的宇宙是一個由正方形細胞組成的無限二維正交網格,每個細胞都處於兩種可能的狀態之一,活着或死了。每個細胞與其八個相鄰細胞相互作用,即水平、垂直或對角相鄰的細胞。在每個時間週期內,都會發生以下轉換:活細胞的相鄰活細胞少於兩個的會死亡,就好像是由人口不足引起的。活細胞的相鄰活細胞有兩個或三個會存活到下一代。活細胞的相鄰活細胞有三個以上的會死亡,就好像是由人口過多引起的。死細胞的相鄰活細胞只有三個的會復活,就好像是通過繁殖一樣。最初的模式構成了系統的種子。第一代是種子中的每個細胞通過上述規則而產生的,死亡和生存會同時發生,直到達到平衡。
實現生命遊戲的製作,生命的遊戲是在一個無限的宇宙中進行的,但我們沒有無限的存儲和計算能力。解決這個相當煩人的限制通常有三種方式,1,跟蹤宇宙的哪個子集發生了有趣的事情,並根據需要擴展這個區域。在最壞的情況下,這種擴展是無限制的,實現將變得越來越慢,最終耗盡內存。2,創建一個固定大小的宇宙,其中邊緣的細胞比在中間的細胞有更少的鄰居。這種方法的缺點是,像滑翔機一樣,到達宇宙盡頭的無限模式會被扼殺。3,創建一個固定大小的週期性宇宙,其中邊緣的細胞具有環繞到宇宙另一側的鄰居。因爲鄰居環繞着宇宙的邊緣,滑翔機可以永遠運行。我們將實施第三種選擇。
Rust 和 JavaScript 如何牽手?
JavaScript 的垃圾收集堆 - 存放對象、數組和 DOM 節點的地方,WebAssembly 的線性內存空間,存放 Rust 的值的地方。WebAssembly 目前無法直接訪問垃圾收集堆(截止 2018 年 4 月,這一點預計將隨着 “接口類型” 提案的提出而改變)。但是,JavaScript 可以讀取和寫入 WebAssembly 的線性內存空間,但只能使用標量值(u8,i32,f64)的 ArrayBuffer。WebAssembly 函數也可以獲取和返回標量值。這些是構成所有 WebAssembly 和 JavaScript 通信的基礎。
wasm_bindgen 定義了通用的方法,在兩種語言邊界如何解釋複雜結構體。它可以裝箱 Rust 結構體,在 JavaScript 類中封裝指針,或從 Rust 中索引到 JavaScript 對象表中。wasm_bindgen 非常方便,你可以選擇它實現接口設計。
在設計 WebAssembly 和 JavaScript 之間的接口時,我們希望優化以下屬性:最小化對 WebAssembly 線性內存的複製,不必要的拷貝會帶來不必要的開銷。最小序列化和反序列化,和複製相似,序列化和反序列化也會帶來開銷,而且通常附帶複製。如果我們可以將句柄傳遞給數據結構,而不是在一端進行序列化,將其複製到 WebAssembly 線性內存中的某個已知位置,然後在另一端對其進行反序列化,我們通常可以減少大量開銷。wasm_bindgen 幫助我們定義和使用 JavaScript 對象或裝箱 Rust 結構體的不透明句柄。根據經驗,一個好的 JavaScript<->WebAssembly 接口設計通常將大型、長生命週期的數據結構實現爲駐留在 WebAssembly 線性內存中的 Rust 類型,並作爲不透明句柄暴漏在 JavaScript 中。JavaScript 調用導出的 WebAssembly 函數,這些函數接受這些不透明的句柄,轉換其數據,執行繁重的計算,查詢數據,並最終返回一個小的,可複製的結果。通過只返回較小的計算結果,我們可以避免在 JavaScript 垃圾收集堆和 WebAssembly 線性內存之間來回複製和序列化這些數據。
讓我們從生命遊戲中的宇宙開始,我們不想每次都把整個宇宙複製到 WebAssembly 線性內存中。我們不想爲宇宙中的每個單元分配對象,也不想使用一個跨邊界的調用來讀取和寫入每個單元。我們可以將宇宙表示爲一個平面數組,該數組位於 WebAssembly 線性內存中,每個單元格都有一個字節,0 代表死細胞,1 代表活細胞。
下面代表一個 4*4 的宇宙的平面數組。
要找到宇宙中的給定行和列的單元格的數組索引,我們可以使用下面的公式:
index(row, column, universe) = row * width(universe) + column
我們有幾種方法將宇宙中的單元細胞內容暴漏給 JavaScript,先從簡單的說起,我們呈現爲文本字符,然後通過 WebAssembly 線性內存複製到 JavaScript 中,然後通過 HTML 文本對象來顯示。我們也可以改進,避免在堆之間複製宇宙的單元,直接通過 HTML Canvas 進行渲染。另一種複雜的方法,Rust 每次生命細胞迭代後,將變化的單元格組成列表,暴漏給 JavaScript 中,這樣 JavaScript 渲染時就不需要在整個宇宙中迭代,只需要在相關的子集中迭代。這種方案比較難實現,我們暫不考慮。
我們要開始真正的編碼了,打開 src/lib.rs 文件,定義細胞
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
這裏說明一下,#[repr(u8)] 表示每個單元格都是一個字節。並且 Dead 爲 0,Alive 爲 1,這樣我們可以通過加法計算周邊的活細胞鄰居。
下一步,我們定義宇宙,宇宙擁有寬和高,以及一個長度爲寬乘高的細胞數組。
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}
在宇宙中獲取某個細胞的方法,
impl Universe {
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
// ...
}
計算宇宙中某個細胞周邊的活鄰居數量的方法,
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
}
}
這個方法,巧妙的使用了 delta,當碰到邊界時,使用行或列加上變量對行或高求模,不會出現負數。否則,就得使用 if 條件進行判斷。
現在,我們有了從當前代計算下一代的所需的一切,遊戲的每一條規則都遵循一個簡單的翻譯,即匹配表達式上的條件。此外,因爲我們希望 JavaScript 控制 ticks 何時發生,所以我們將把這個方法放在 #[wasm_bindgen] 塊中,這樣它就可以暴漏在 JavaScript 中。
/// Public methods, exported to JavaScript.
#[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) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
// ...
}
到目前爲止,宇宙的狀態表示爲細胞的矢量。爲了使這個文本可讀,我們實現一個文本呈現器。這個想法就是將宇宙一行一行寫成文本,併爲每個活細胞打印爲◼,每個死細胞,打印爲◻。
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(())
}
}
是時候生成一個宇宙了,
/// Public methods, exported to JavaScript.
#[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()
}
}
現在 Rust 端的代碼已經編寫完成,我們使用 wasm-pack build 命令編譯。得到生命遊戲的 wasm 文件。
開始編寫 JavaScript 端的代碼。打開 www/index.html 文件,將內容調整爲:
<body>
<pre></pre>
<script src="./bootstrap.js"></script>
</body>
添加了 元素,用來顯示生命遊戲。
我們寫一些 CSS 樣式,將宇宙放在頁面的中間,在 index.html 頁面中的 head 標籤中添加如下代碼:
<style>
body {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
在 www/index.js 文件中,添加如下內容,
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);
正常運行 Web 項目後,界面如下:
還可以進行優化,使用 Canvas 進行渲染!下節吧。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/X9ooJKpl2TC6rPIrwDI97A