WebAssemblyの練習に、セル・オートマトンの有名な問題ライフゲーム(Game of Life)を実装してみました。


ライフゲームのルール

 Wikipediaより、典型的と思われるルールを採用しました。


誕生

 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。

生存

 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。

過疎

 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。

過密

 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。



結果

まず出力結果から。ここで、nはキャンバス1辺に含まれるセルの数。初期条件は、市松模様からスタートしています。デモは https://cellular-automaton-webassembly.herokuapp.com/ にあげてあります。


n = 20

cell20


n = 50

cell50


n = 100

cell100



Cargo.toml

主に3つのcrateを読ませます。



  • wasm-bindgen - JavaScript連携

  • web-sys - HtmlElementの操作に使用

  • lazy_static - セルの状態等を格納するベクトルをグローバル変数化し、クロージャー内で使用するために使用


[package]

name = "wasm"
version = "0.1.0"
authors = ["snst.lab <snst.lab@gmail.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
lazy_static = "0.2.1"

[dependencies.wasm-bindgen]
version = "0.2.45"

[dependencies.web-sys]
version = "0.3.4"
features = [
'Document',
'Window',
'Element',
'Node',
'HtmlElement',
'DocumentFragment',
'CssStyleDeclaration',
'Event',
'EventTarget',
'MouseEvent',
]



Rust側

とりあえず主要箇所のみ抜粋。ソースはGitHubにあげています。


ヘッダー部分

 ポイントとして、RcRefCellrequest_animation_frameの呼び出しに、RwLocklazy_staticに使用します。

extern crate wasm_bindgen;

use std::rc::Rc;
use std::cell::RefCell;
use web_sys::{HtmlElement, DocumentFragment};
use wasm_bindgen::{prelude::*, JsCast};

#[macro_use]
extern crate lazy_static;
use std::sync::RwLock;


グローバル変数の定義

 Vec<HtmlElement>RwLockに渡すと、以下のエラーとなります。


error[E0277]: *mut u8 cannot be sent between threads safely

help: within web::web_sys::HtmlElement, the trait std::marker::Send is not implemented for *mut u8


回避策として、Vec<String>RwLockに渡して各セル要素のクラス名を保持しています。

lazy_static! {

static ref CELLS: RwLock<Vec<String>> = RwLock::new(Vec::new()); //セル要素のクラス名を保持するベクトル
static ref LIFE: RwLock<Vec<u16>> = RwLock::new(Vec::new()); //セルの状態を保持するベクトル
static ref LIFE_TEMP: RwLock<Vec<u16>> = RwLock::new(Vec::new()); //セルの状態を一時保管するベクトル
static ref RUNNING: RwLock<bool> = RwLock::new(false); //アニメーションの稼働状態を保持
}


構造体の定義

 wasm-bindgenの作法で書いていきます。ちなみに、Document::query_selector()Window::request_animation_frame()web-sysで用意されている関数をラップした関数です。


#[wasm_bindgen]
pub struct CellularAutomaton{
canvas: HtmlElement, //描画範囲のHTML要素。HtmlElementはCopy Treatを実装していないため、pubは外す
pub n: isize, //描画範囲1辺に含まれるセルの数
pub N: isize, //全セル数
pub size_of_cell: f64, //セルの幅(px)
}

#[wasm_bindgen]
impl CellularAutomaton{
/**
コンストラクタで各プロパティを初期化。N, size_of_cellは計算で求めるため、とりあえずの初期値
*/

#[wasm_bindgen(constructor)]
pub fn new() -> CellularAutomaton {
CellularAutomaton {
canvas: Document::query_selector(".canvas"),
n : 20,
N : 400,
size_of_cell:10.0
}
}
/**
N, size_of_cellの計算、各メソッドの呼び出し
*/

pub fn start(&mut self) -> Result<(), JsValue>{
self.N = self.n.pow(2);
self.size_of_cell = self.canvas.client_width() as f64 /self.n as f64;
CellularAutomaton::draw_canvas(self).expect("failed to draw canvas");
CellularAutomaton::initialize(self).expect("failed to initialize");
Ok(())
}
/**
Canvasの描画。DocumentFragmentを使って一気に追加。
*/

fn draw_canvas(&mut self) -> Result<(), JsValue> {
self.canvas.style().set_property("height", &(self.canvas.client_width().to_string()+"px"))?;
let fragment : DocumentFragment = DocumentFragment::new().unwrap();
/**
各グローバル変数を書き込み用に呼び出し。
*/

let mut life = LIFE.write().unwrap();
let mut life_temp = LIFE_TEMP.write().unwrap();
let mut cells = CELLS.write().unwrap();

for i in 0..(self.N as usize){
let cell:HtmlElement = Document::create_element("div");
let class_name:String = "cell".to_owned() + &i.to_string();
cell.set_class_name(&("cell ".to_owned()+&class_name));
cell.style().set_property("width", &(self.size_of_cell.to_string() + "px"))?;
cell.style().set_property("height", &(self.size_of_cell.to_string() + "px"))?;
fragment.append_child(&cell)?;
/**
まず全セルを0で初期化。
*/

(*life).push(0);
(*life_temp).push(0);
(*cells).push(".".to_owned() + &class_name);
}
self.canvas.append_child(&fragment);
Ok(())
}
/**
初期状態として市松模様を与え、アニメーションスタート
*/

fn initialize(&self) -> Result<(), JsValue>{
let mut life = LIFE.write().unwrap();
let mut life_temp = LIFE_TEMP.write().unwrap();
let cells = CELLS.write().unwrap();

for i in 0..(self.N as usize){
if ((i/(self.n as usize))%2==0 && i % 2 == 0) || ((i/(self.n as usize))%2==1 && i % 2 == 1){
(*life)[i] = 1;
(*life_temp)[i] = 1;
let cell:HtmlElement = Document::query_selector(&(*cells)[i as usize]);
cell.style().set_property("background-color", "deeppink").expect("failed to set property");
}
}

let mut running = RUNNING.write().unwrap();
(*running) = true;
CellularAutomaton::run(self.n,self.N);
Ok(())
}

/**
request_animation_frameで15フレーム毎(1秒に約4回)に世代遷移のメソッドevaluateを呼び出す。
   ブール値runningは中断や再開をコントロールするためのフラグとして使用。
*/

fn run(n:isize, N:isize) -> Result<(), JsValue>{
let f = Rc::new(RefCell::new(None));
let g = f.clone();

let mut frame = 0;
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
let runnning = RUNNING.read().unwrap();
if *runnning {
if frame % 15 == 0 {CellularAutomaton::evaluate(n,N).expect("failed to evaluate");}
frame += 1;
Window::request_animation_frame(f.borrow().as_ref().unwrap());
}
}) as Box<FnMut()>));
Window::request_animation_frame(g.borrow().as_ref().unwrap());
Ok(())
}
/**
世代遷移のメソッド
*/

fn evaluate(n:isize, N:isize) -> Result<(), JsValue>{
/**
cellsを読み取り専用で、lifeとlife_tempは書き込み用に呼び出す。
*/

let cells = CELLS.read().unwrap();
let mut life = LIFE.write().unwrap();
let mut life_temp = LIFE_TEMP.write().unwrap();
/**
 セルiの周囲の生きたセルの数から、上記ルールに従って次世代のセルの生死を判定
*/

for i in 0..N {
let top_right: isize = i - n + 1;
let top: isize = i - n;
let top_left: isize = i - n - 1;
let left: isize = i - 1;
let bottom_left: isize = i + n - 1;
let bottom: isize = i + n;
let bottom_right: isize = i + n + 1;
let right: isize = i + 1;
/**
 around = セルiの周囲の生きたセルの数
*/

let around : u16 =
(if top_right < 0 { 0 } else { (*life)[top_right as usize] } ) +
(if top < 0 { 0 } else { (*life)[top as usize] } ) +
(if top_left < 0 { 0 } else { (*life)[top_left as usize] } ) +
(if left < 0 { 0 } else { (*life)[left as usize] } ) +
(if bottom_left >= N { 0 } else { (*life)[bottom_left as usize] } ) +
(if bottom >= N { 0 } else { (*life)[bottom as usize] } ) +
(if bottom_right >= N { 0 } else { (*life)[bottom_right as usize] } ) +
(if right >= N { 0 } else { (*life)[right as usize] } );

(*life_temp)[i as usize] = (*life)[i as usize];

if (*life)[i as usize] == 0 && around == 3 {
let cell:HtmlElement = Document::query_selector(&(*cells)[i as usize]);
cell.style().set_property("background-color", "deeppink").expect("failed to set property");
(*life_temp)[i as usize] = 1;

} else if (*life)[i as usize] == 1 && (around == 2 || around == 3) {
continue;

} else if (*life)[i as usize] == 1 && (around <= 1 || around >= 4) {
let cell:HtmlElement = Document::query_selector(&(*cells)[i as usize]);
cell.style().set_property("background-color", "lightgray").expect("failed to set property");
(*life_temp)[i as usize] = 0;
}
}
/**
 上記の判定が全て終わったら、life_tempの状態をlifeにコピーする。
*/

for i in 0..(N as usize){
(*life)[i] = (*life_temp)[i];
}
Ok(())
}
}




JavaScript側

WebAssemblyは非同期呼び出しする必要があるため、async即時関数でラップします。

init関数で初期化してから上記CellularAutomaton構造体を呼び出します。

wasm.jswasm_bg.wasmのパスは環境によって変えて下さい。

import { CellularAutomaton as CA , default as init } from '../wasm/pkg/wasm.js';

(async() =>{
await init('./src/wasm/pkg/wasm_bg.wasm');
const ca: CA = new CA();
ca.start();

})().catch(() => 'Failed to load wasm.');


参考にさせていただいたサイト

https://rustwasm.github.io/wasm-bindgen/examples/request-animation-frame.html

https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.HtmlElement.html

https://qiita.com/nacika_ins/items/cf3782bd371da79def74

https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0