使用Rust开发WebAssembly,你需要有一定的编程基础。需要了解Rust,了解JavaScript,HTML,和CSS。https://www.rust-lang.org/;rustup target add wasm32-unknown-unknownhttps://rustwasm.github.io/wasm-pack/installer/cargo install cargo-generate
https://www.npmjs.com/get-npm使用模板初始化项目,项目名为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
fn main() {
mod utils;
use wasm_bindgen::prelude::*;
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
extern {
fn alert(s: &str);
}
pub fn greet() {
alert("Hello, wasm-game-of-life!");
}
}
当编译成功后,会生成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项目也有模板,在项目的根目录中,使用如下命令生成模板:wasm-game-of-life/www/
├── bootstrap.js
├── index.html
├── index.js
├── LICENSE-APACHE
├── LICENSE-MIT
├── package.json
├── README.md
└── webpack.config.js
npm install
npm run start
生命游戏的游戏规则:生命游戏的宇宙是一个由正方形细胞组成的无限二维正交网格,每个细胞都处于两种可能的状态之一,活着或死了。每个细胞与其八个相邻细胞相互作用,即水平、垂直或对角相邻的细胞。在每个时间周期内,都会发生以下转换:活细胞的相邻活细胞少于两个的会死亡,就好像是由人口不足引起的。活细胞的相邻活细胞有两个或三个会存活到下一代。活细胞的相邻活细胞有三个以上的会死亡,就好像是由人口过多引起的。死细胞的相邻活细胞只有三个的会复活,就好像是通过繁殖一样。最初的模式构成了系统的种子。第一代是种子中的每个细胞通过上述规则而产生的,死亡和生存会同时发生,直到达到平衡。
实现生命游戏的制作,生命的游戏是在一个无限的宇宙中进行的,但我们没有无限的存储和计算能力。解决这个相当烦人的限制通常有三种方式,1,跟踪宇宙的哪个子集发生了有趣的事情,并根据需要扩展这个区域。在最坏的情况下,这种扩展是无限制的,实现将变得越来越慢,最终耗尽内存。2,创建一个固定大小的宇宙,其中边缘的细胞比在中间的细胞有更少的邻居。这种方法的缺点是,像滑翔机一样,到达宇宙尽头的无限模式会被扼杀。3,创建一个固定大小的周期性宇宙,其中边缘的细胞具有环绕到宇宙另一侧的邻居。因为邻居环绕着宇宙的边缘,滑翔机可以永远运行。我们将实施第三种选择。
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代表活细胞。
要找到宇宙中的给定行和列的单元格的数组索引,我们可以使用下面的公式:
index(row, column, universe) = row * width(universe) + column
我们有几种方法将宇宙中的单元细胞内容暴漏给JavaScript,先从简单的说起,我们呈现为文本字符,然后通过WebAssembly线性内存复制到JavaScript中,然后通过HTML 文本对象来显示。我们也可以改进,避免在堆之间复制宇宙的单元,直接通过HTML Canvas进行渲染。另一种复杂的方法,Rust每次生命细胞迭代后,将变化的单元格组成列表,暴漏给JavaScript中,这样JavaScript渲染时就不需要在整个宇宙中迭代,只需要在相关的子集中迭代。这种方案比较难实现,我们暂不考虑。我们要开始真正的编码了,打开src/lib.rs文件,定义细胞
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中。
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;
}
}
到目前为止,宇宙的状态表示为细胞的矢量。为了使这个文本可读,我们实现一个文本呈现器。这个想法就是将宇宙一行一行写成文本,并为每个活细胞打印为◼,每个死细胞,打印为◻。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.
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 id="game-of-life-canvas"></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>
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);
还可以进行优化,使用Canvas进行渲染!下节吧。