commit/a3f3f4b36a34b12580015063bd548eda03577dd5
实现康威生命游戏
设计
在我们深入之前,我们有一些设计选择供考虑。
无限宇宙
生命游戏在一个无限宇宙中进行,但是我们没有无限的内存和计算能力。 解决这个烦人的限制通常使用三种方法之一:
保持跟踪正在发生有趣事情的宇宙的子集,然后按需要扩展这个区域。 最差的情况是扩展是没有边界的,并且实现将会越来越慢并最终耗尽内存。
创建固定尺寸的宇宙,这样位于边缘的细胞会比在中间的细胞拥有更少的邻居。 这个方法的缺点是像滑翔机那样的无穷模式在到达宇宙边界后就被消灭了。
创建一个固定尺寸的周期行宇宙,这样位于边缘的细胞拥有环绕到宇宙另一侧的邻居。 因为邻居环绕宇宙的边缘,滑翔机可以永远运行。
我们将实现第三个选项。
Rust和JavaScript接口技术
⚡ 这是需要从理解并从本教程中学会的最重要的概念之一。
JavaScript的垃圾收集堆内存不同于WebAssembly的线性内存空间,
前者是分配Object
,Array
和DOM节点的地方,后者存放我们Rust的值。
WebAssembly目前没有直接访问垃圾回收堆内存
(截至2018年四月,期望这将随着"host bindings"提案改变)。
换句话说JavaScript可以读写WebAssembly的线性内存空间,
但只能作为标量值( u8
, i32
, f64
等)的ArrayBuffer
。
WebAssembly函数也获取和返回标量值。
这些是构成WebAssembly和JavaScript通信的所有组件。
wasm_bindgen
定义了如何在这个边界上使用复合结构的共识。
它涉及包装Rust结构并将指针包装在JavaScript类中以便于使用,或者从Rust中索引JavaScript对象表。
wasm_bindgen
非常方便,但是它没有移除考虑我们的数据表示以及什么值和结构被传递到这个边界的需求。
相反,将它视为一个用于实现你选择的界面设计的工具。
当你在WebAssembly和JavaScript之间设计接口的时候,我们想要优化下面的性能:
最小化复制进出WebAssembly的线性内存。 不必要的复制会产生不必要的开销。
最小化序列化和反序列化。 类似于复制,序列化和反序列化也会产生开销,并且通常也进行复制。 如果我们可以传递不透明的句柄到数据结构, 而不用在一端序列化然后将其拷贝到WebAssembly线性内存的某个已知位置再从另一端反序列化, 这样我们通常能减少大量开销。
wasm_bindgen
帮助我们定义和处理JavaScript的Object
或封装的Rust结构的不透明句柄。
作为一般经验法则,一个好的JavaScript↔WebAssembly接口设计通常是将大型,长寿命的数据结构实现为存在 WebAssembly线性内存中的Rust类型,并作为不透明句柄暴露给JavaScript。 JavaScript调用导出的WebAssembly函数,这个函数获取不透明句柄, 传递他们的数据,执行繁重的计算,查询数据,并最终返回小的,可复制的结果。 只有通过返回计算的小结果,我们才能避免拷贝和序列化所有在 JavaScript垃圾收集堆内存和WebAssembly线性内存之间来回传递的所有内容。
我们的生命游戏中的Rust和JavaScript的接口技术
我们先来列举一些要避开的危险。我们不想要每一个tick都将整个宇宙拷贝进出到WebAssembly的线性内存。 我们不想要为宇宙中的每个单元分配对象,也不想强加一个跨边界调用来读写每个单元。
这给我们留下了什么?
我们可以将宇宙表示为一个平面数组,它存储在WebAssembly的线性内存中,每个单元格有一个字节。
0
是死细胞,1
是活细胞。
这是一个4*4宇宙在内存中的样子:
为了找到宇宙中给定行列单元格的数组索引,我们可以使用这个公式:
index(row, column, universe) = row * width(universe) + column
我们有几个方法将宇宙中的单元格暴露给JavaScript。
首先我们将为Universe
实现std::fmt::Display
,
这样我们可以用来生成一个表现为文本字符的单元格的RustString
。
然后这个Rust String被从WebAssembly的线性内存拷贝到JavaScript的垃圾收集堆内存上的JavaScript字符串,
然后通过设置HTML的textContent
来展示。
在本节的后面,我们将改变这个实现以避免在堆内存之间拷贝宇宙单元格和渲染到<canvas>
.
另一个可行的设计是Rust在每个tick后返回一个改变状态的单元格的列表,而不用将这个宇宙暴露给JavaScript。 这样JavaScript在渲染的时候不需要迭代整个宇宙,只需要一个对应的子集。 这种增量的设计实现起来稍微有些困难。
Rust实现
在上一个节中,我们克隆了一个初始的模板项目。我们现在将要修改那个模板项目。
我们首先从wasm-game-of-life/src/lib.rs
中移除alert
导入和greet
函数,
然后用一个对单元格的类型定义取代他们:
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
这里有一个#[repr(u8)]
是很重要的,这样每个单元格被表示为单个字节。
Dead
表示为0
而Alive
表示为1
也很重要,这样我们能够轻松的用加计算一个单元格的存活邻居数。
接下来让我们定义宇宙。宇宙有一个长度和宽度以及一个具有width * height
长度的向量。
#[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
}
// ...
}
为了计算单元格的下一个状态,我们需要得到一个有多少邻居存活的计数。
让我们写一个live_neighbor_count
方法来做这个。
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
}
}
live_neighbor_count
方法使用增加和模来避免使用if对宇宙边缘进行特殊的覆盖。
当应用 -1
的增量时,我们加self.height - 1
并让模数做它的事而不是尝试去减1
。
row
和column
可以是0
,如果我们试图对他们减1
,将会有一个无符号整数下溢。
现在我们有了从当前一代计算出下一代所需要的全部内容了。每一条游戏规则都被直接翻译为一个match
表达式的条件。
另外,由于我们希望当ticks发生的时候JavaScript进行控制,所以我们将把这个方法放到一个#[wasm_bindgen]
块中,
这样它就才能暴露给JavaScript。
/// 公共函数,暴露给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) {
// 规则 1:任何存活细胞周围少于两个存活邻居则死亡,仿佛是人口过少造成的。
(Cell::Alive, x) if x < 2 => Cell::Dead,
// 规则 2: 任何存活细胞周围有两个或三个存活邻居则活到下一代。
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// 规则 3: 任何存活细胞周围有超过三个存活邻居则死亡,仿佛是人口过剩造成的。
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: 任何死亡细胞周围有正好三个存活细胞则变成存活细胞,仿佛是生殖。
(Cell::Dead, 3) => Cell::Alive,
// 所有其他单元格保持同样的状态。
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
// ...
}
到目前为止,宇宙的状态被表示为一个单元格向量。为了是人可读,让我们实现一个基本的文本渲染器。
方法是将宇宙一行行以文本形式写下来,对于每个存活细胞,打印Unicode字符◼
(“黑色中方框”)。
对于每个死细胞,我们打印◻
(一个“白色中方框”)。
通过实现Rust标准库中的Display
trait,我们可以添加一个方法来以面向用户的方式格式化一个结构。
这将会自动的给我们一个to_string
方法。
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(())
}
}
最后我们定义一个构造函数,它使用有意思的生死细胞模式来初始化宇宙,也有一个render
方法:
/// 公共方法,暴露给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-game-of-life
目录中运行wasm-pack build
将它编译为WebAssembly。
使用JavaScript渲染
首先,我们天加一个<pre>
元素到wasm-game-of-life/www/index.html中,
宇宙就在其中渲染,添加的位置就在