WebAssembly(2)-康威生命游戏案例

通过案例来展示WebAssembly的应用

1. 游戏规则和设计

  1. 规则:在一个二维方格中,每个方格的状态都为“生”或者“死”。每个方格对应的就是一个细胞,每个细胞和它的周围的八个方格相邻。在每个时间推移过程中,都会发生以下转换(就是说,下一秒的状态,根据前一秒的状态来判断生死):
    • 任何少于两个活邻居的活细胞都会死亡。
    • 任何有两个或三个活邻居的活细胞都能存活到下一代。
    • 任何一个有三个以上邻居的活细胞都会死亡。
    • 任何一个有三个活邻居的死细胞都会变成一个活细胞。
  2. 设计:创建一个固定大小的宇宙(二维方格),但是左边的尽头就是右边。

2. 基本实现

以正常思路完成以上功能

2.1 rust实现

计算和打印,都由rust来实现,这个只是简单的设计逻辑,具体注释都在代码中标注了
lib.rs文件中

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
#[repr(u8)] //表示下方枚举只占用8个bit
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}

#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}

#[wasm_bindgen]
impl Universe {
//初始化
pub fn new() -> Universe {
let w = 64;
let h = 64;
let cells = (0..w * h).map(|i| {
if i % 2 == 0 || i % 7 == 0 {
Cell::Alive
} else {
Cell::Dead
}
}).collect();
Universe {
width: w,
height: h,
cells,
}
}

pub fn render(&self) -> String {
self.to_string()
}

//获取到一维数组中的索引
fn get_index(&self, row: u32, col: u32) -> usize {
(row * self.width + col) as usize
}

//获取活着的邻居的个数
//相邻的步进是-1,0,1
fn live_neighbor_count(&self, row: u32, col: 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 = (col + delta_col) % self.width;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}

//下一个时间点,宇宙的变化
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;
use std::fmt::write;

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(())
}
}

2.2 web页面处理

用于调用rust的函数
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">
<script src="./bootstrap.js"></script>
</body>
</html>

index.js

1
2
3
4
5
6
7
8
9
import {Universe} from "wasm-game-of-life";
const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
function renderLoop(){
pre.textContent = universe.render();
universe.tick();
window.requestAnimationFrame(renderLoop);
}
window.requestAnimationFrame(renderLoop);

2.3 启动

1
npm run start

浏览器访问:http://localhost:8080]

3. 进一步优化

上面实现的问题:
在Rust中实现了填充的内容,然后让wasm bindgen转换为一个有效的js字符串,这样就产生了不必要的副本。因为js代码已经知道宇宙的宽度和高度,因此可以直接读取构成cell的web assembly内存。另外我们将切换到Canvas API,而不是直接使用unicode文本。

3.1 rsut中将宽度、高度和数组指针返回给js

1
2
3
4
5
6
7
8
9
10
11
12
//返回宽度
pub fn width(&self) -> u32 {
self.width
}
//返回高度
pub fn height(&self) -> u32 {
self.height
}
//数组指针返回给js
pub fn cells(&self) -> *const Cell {
self.cells.as_ptr()
}

3.2 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>
<canvas id="game-of-life-canvas"></canvas>
<script src="./bootstrap.js"></script>
</body>
</html>

3.3 index.js

原先在rust中生成画面的逻辑,迁移到了js中,减少了一次数据的复制

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
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');

function renderLoop() {
universe.tick();
drawGrid();
drawCells();
window.requestAnimationFrame(renderLoop);
}


function drawGrid() {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;

// Vertical lines.
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);
}

// Horizontal lines.
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();
}

function getIndex(row, column) {
return row * width + column;
}

function 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();
}
window.requestAnimationFrame(renderLoop);

4. 测试

需要先在rust中设置一些方法,用于测试调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Universe的wasm中实现方法:
pub fn set_width(&mut self,width:u32){
self.width = width;
self.cells = (0..width*self.height).map(|_i| Cell::Dead).collect();
}
pub fn set_height(&mut self,height:u32){
self.height = height;
self.cells = (0..height*self.width).map(|_i| Cell::Dead).collect();
}

非wasm实现方法
//没有#[wasm_bindgen]
impl Universe{
pub fn get_cells(&self)->&[Cell]{
&self.cells
}
pub fn set_cells(&mut self,cells:&[(u32,u32)]){
for (row,col) in cells.iter().cloned(){
let idx = self.get_index(row,col);
self.cells[idx] = Cell::Alive;
}
}
}

在tests中的web.rs中:

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
//! Test suite for the Web and headless browsers.

#![cfg(target_arch = "wasm32")]

extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
extern crate wasm_game_of_life;
use wasm_game_of_life::Universe;

wasm_bindgen_test_configure!(run_in_browser);

#[cfg(test)]
pub fn input_spaceship() -> Universe {
let mut universe = Universe::new();
universe.set_width(6);
universe.set_height(6);
universe.set_cells(&[(1, 2), (2, 3), (3, 1), (3, 2), (3, 3)]);
universe
}

#[cfg(test)]
pub fn excepted_spaceship() -> Universe {
let mut universe = Universe::new();
universe.set_width(6);
universe.set_height(6);
universe.set_cells(&[(2, 1), (2, 3), (3, 2), (3, 3), (4, 2)]);
universe
}

#[wasm_bindgen_test]
pub fn test_tick() {
let mut input_universe = input_spaceship();
let excepted_universe = excepted_spaceship();
input_universe.tick();
assert_eq!(&input_universe.get_cells(), &excepted_universe.get_cells());
}

执行测试:

1
wasm-pack test --firefox --headless

5. 调试代码

要在浏览器控制台打印日志,需要:
wasm-pack-template附带了一个可选的,默认情况下启用的依赖,该依赖属于console_error_panic_hook包,在wasm-game-of-life/src/utils中配置,需要在初始化函数活着公共代码路径中安装钩子,在本项目中,可以如下调用:

1
2
3
4
pub fn new() ->Universe{
utils::set_panic_hook();
//....
}

Cargo.toml依赖:

1
web-sys = {version="0.3",features=["console"]}

日志宏:
lib.rs :

1
2
3
4
5
6
extern crate web_sys;
macro_rules! log {
($($t:tt)*)=>{
web_sys::console::log_1(&format!($($t)*).into())
}
}

rust中设置需要打印的内容:

1
2
3
4
5
6
7
8
//变化前打印情况
log!(
"cell [{},{}] is initally {:?} and has {} live neighbors",
row,
col,
cell,
live_neighbors
);

index.js中开启debug:

1
2
3
4
5
6
7
function renderLoop() {
debugger;//添加调试
universe.tick();
drawGrid();
drawCells();
window.requestAnimationFrame(renderLoop);
}

启动后,即可在浏览器控制台看到打印的信息

总结

本文已编辑完毕

参考

[1] Wasm官方文档

  • Copyrights © 2017-2023 Jason
  • Visitors: | Views:

谢谢打赏~

微信