WebAssemblyの練習に、セル・オートマトンの有名な問題ライフゲーム(Game of Life)を実装してみました。
ライフゲームのルール
Wikipediaより、典型的と思われるルールを採用しました。
誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
結果
まず出力結果から。ここで、nはキャンバス1辺に含まれるセルの数。初期条件は、市松模様からスタートしています。デモは https://cellular-automaton-webassembly.herokuapp.com/ にあげてあります。
n = 20
n = 50
n = 100
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にあげています。
ヘッダー部分
ポイントとして、Rc
とRefCell
をrequest_animation_frame
の呼び出しに、RwLock
をlazy_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: withinweb::web_sys::HtmlElement
, the traitstd::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.js
、wasm_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