前回、前々回とライフゲームの骨格、アルゴリズムを実装してきました。
今回は、そのアルゴリズムに従って、生成されるライフゲームをnannouクレートを使ってビジュアルに描画していきます。
ライフゲームのアルゴリズム実装については、#1、#2をご覧ください。
nannouの準備
今回はまず新しくプロジェクトを作りましょう。
そして、そこにnannouクレートを追加します。
手動でcargo.tomlを書いてもいいですが、
>cargo add nannou
とやれば、自動的に追加されます。
追加されたら、一度ビルドしておきましょう。
nannouの使い方とかは、過去の記事にあるので、気になる方は、ご覧ください。
今回は、過去に作ったテンプレートを利用して、作っていきます。
このテンプレートがあれば、大体の簡単なことはできるかな?って感じです。
main.rsにこのテンプレートをそのままコピペしてください。
nannou公式のリポジトリにもテンプレートがあるので、そちらでも構いません。
ライフゲームのアルゴリズムを持ってくる。
さて、#1、#2で作ったライフゲームを実装してものを持ってきます。
lifegame.rsていう名前のファイルを新しく作って、そこにコピペします。
外部から、呼び出したい関数や構造体には、”pub”を頭につけて、パブリック状態にしておきます。
get_indexとかnext_gen()とか外部から呼び出すのでpubつけましょう。get_alive_cells()とかは、内部でしか使わないので、プライベートのままでOKです。
#[derive(Debug,Clone, Copy,PartialEq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
#[derive(Debug)]
pub struct Field{
width:u32,
height:u32,
cells:Vec<Cell>
}
impl Field{
pub fn new()->Field{
let width = 10;
let height =10;
// let cells = (0..width*height)
// .map(|i|{
// if i%2==0||i%11==0{
// Cell::Alive
// }else{
// Cell::Dead
// }
// }).collect();
let cells = [
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,0,0,0,
0,0,0,1,1,1,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
].into_iter()
.map(|i|{
if i==1{
Cell::Alive
}else{
Cell::Dead
}
}).collect();
Field { width, height, cells}
}
pub fn get_index(&self,row:u32,column:u32)->usize{
((row-1)*self.width+column-1) as usize
}
pub fn get_cell_info(&self,row:u32,column:u32)->usize{
let idx = self.get_index(row, column);
self.cells[idx] as usize
}
fn get_alive_cells(&self,row:u32,column:u32)->usize{
let mut count:usize=0;
for y in [-1,0,1]{
for x in [-1,0,1]{
let y_idx = row as i32 + y;
let x_idx = column as i32 +x;
if y_idx >0 && x_idx>0 && y_idx<=self.height as i32 && x_idx<=self.width as i32{
// println!("True");
// println!("{}",self.get_cell_info(y_idx as u32, x_idx as u32));
// let idx = self.get_index(y_idx as u32,x_idx as u32);
// count += self.cells[idx] as usize
count += self.get_cell_info(y_idx as u32, x_idx as u32);
}
}
}
if self.get_cell_info(row, column)==1{
count -=1;
}
count
}
pub fn next_gen(&mut self){
let mut next_cells = self.cells.clone();
for row in 1..self.height+1{
for column in 1..self.width+1{
let idx = self.get_index(row, column);
let cell = self.cells[idx];
let alive_cells = self.get_alive_cells(row, column);
// println!("idx:{},cell:{:?},alive_cell:{}",idx,cell,alive_cells);
let next_cell = match (cell,alive_cells) {
(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_cells[idx]=next_cell;
}
}
self.cells = next_cells;
}
}
さらにここにセル一つの状態じゃなくて、セル全体の状態を取得する関数get_cells()、そして、新しくセルの状態をセットするための関数set_cell()を追加します。
pub fn get_cells(&self)-> Vec<Cell>{
self.cells.clone()
}
pub fn set_cell(&mut self,cell:Cell,row:u32,col:u32){
let idx= self.get_index(row,col);
self.cells[idx]=cell;
}
ここまでが、lifegame.rsの全体です。
では次にmain.rsを作っていきましょう。
描画の本体
僕が作ったnannouのテンプレートを使って構築していきます。
テンプレートは、これを見てください。
テンプレートの構造
軽くテンプレートの構造を解説します。
use nannou::prelude::*;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
_window: window::Id,
}
fn model(app: &App) -> Model {
let _window = app
.new_window()
.view(view)
.key_pressed(key_pressed)
.mouse_pressed(mouse_pressed)
.build()
.unwrap();
Model { _window }
}
fn update(_app: &App, _model: &mut Model, _update: Update) {}
fn view(app: &App, _model: &Model, frame: Frame) {
let draw = app.draw();
//todo
draw.to_frame(app, &frame).unwrap();
}
fn key_pressed(app: &App, _model: &mut Model, key: Key) {
match key {
Key::S => {
app.main_window()
.capture_frame(app.exe_name().unwrap() + &format!("{:03}", app.time) + ".png");
}
_other_key => {}
}
}
fn mouse_pressed(app: &App, _model: &mut Model, button: MouseButton) {
match button {
MouseButton::Middle => {}
MouseButton::Left => {}
MouseButton::Right => {}
=> {}
}
}
main関数はおまじないです。
このテンプレートは大きく5つの部分に分かれてます。
上から、model関数、update関数、view関数、key_pressed関数、mouse_pressed関数。
model関数は、初期状態を定義する関数。一番最初に1度だけ読み込んでほしいものを書きます。状態をModelっていう構造体に保持していくので、それを最初に初期化するものです。
update関数は、状態の更新をつかさどる部分。状態更新したい内容があれば、ここに書きます。
view関数は、その名の通り、描画にかかわる処理をここに書いていきます。
key_pressedとmouse_pressed関数は、キーボード入力とマウス入力の処理を書きます。
今回は、世代更新をマウスクリックでやっていきます。
ではどんどん実装していきましょう。
ライフゲームのアルゴリズムを読み込む
ライフゲームのアルゴリズムを実装したlifegame.rsを読み込みます
mod lifegame;
use crate::lifegame::*;
この2行を最初の方に追加します。
これで、lifegame.rsのすべてを読み込みました。
modelにlifegameを追加する
Model構造体の中にライフゲームの骨格を入れていきます。
Struct Modelにlifegameっていう要素を入れます。
そしてmodel関数でそれを生成します。
struct Model {
_window: window::Id,
lifegame:Field,
}
fn model(app: &App) -> Model {
let _window = app
...;
let lifegame = Field::new();
Model { _window,lifegame }
}
Field::new()でライフゲームのフィールドを生成します。これで初期状態のものができます。
ライフゲームの描画の下準備をする
ライフゲームの描画の下準備をします。
手順を確認してそれをプログラムにしていきます。
- 現在の全セルを取り出す。
- セルの位置を特定する
- 生きているセルを描画する
この3ステップです。
1.現在の全セルを取り出す
全部のセルを取り出します。
これは、最初にlifegame.rsにget_cellsっていう関数を作りましたね?これをつかって取り出します。
let cells = model.lifegame.get_cells();
こんな感じですね。
2.セルの位置を特定する
lifegame.rsの中には、セルの位置からインデックスを取得する関数がありますが、これからするのはこの逆です。インデックスから位置を特定します。
ここでは、剰余の考え方と整数部分の考え方を使って特定します。割り算の商と余りってやつだね。
let row = i/10;
let col = i%10;
こんな感じでやるんですけど
今回の場合は、幅10の格子です。
row:行は、10列ごとに変わっていくので、インデックスが33だったら、、10で割れば、整数部分は、3だよね。rustの場合、インデックスは0始まりなので、0,1,2、3・・・ってなて、3行目だよね。
col:列は、10列まであるので、33の場合、10で割った時の剰余は3だから、3列目。
これで、インデックス番号とそのインデックス番号の位置がわかりました。
3.生きているセルを描画する
描画をしていきましょう。
生きているセルってのを特定するので
if cells[i] == lifegame::Cell::Alive{
描画
}
って感じでやれば、生きているものだけ描画できます。
描画部分の完成
描画部分を完成させていきます。
まずは、画面を初期化するおまじない
if frame.nth() == 0 || app.keys.down.contains(&Key::R) {
draw.background().color(BLACK);
}
draw.background().color(BLACK);
最初は黒くしてね。画面更新ごとに毎回黒くしてね。っていうおまじない。
下準備したものを合わせていきます。
今回は白い丸に赤い縁取りをします。
let cells = model.lifegame.get_cells();
for i in 0..cells.len(){
let row = i/10;
let col = i%10;
let w =100.;
if cells[i]==lifegame::Cell::Alive{
draw.ellipse()
.xy(pt2(col as f32 * w-400.,row as f32 *(-w)+300.))
.radius(w*0.5)
.color(WHITE)
.stroke_weight(2.0)
.stroke(RED);
}
}
wってのは、幅です。
取り出したセル全体をforループで回して、全部のセルに対して、位置と状態を確認して、生きていれば描画するとしています。
これを実行すると
こんな感じに描画されます。
fieldをnewした時の初期状態は
let cells = [
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,0,0,0,
0,0,0,1,1,1,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
]
これなので、そのまま表示されてますね。
世代更新をする
世代更新をしてみましょう。
更新の関数はnext_gen()でした。
これをクリックするごとに世代更新していきたいので、mouse_pressedに書き込んでいきます。
fn mouse_pressed(_app: &App, model: &mut Model, button: MouseButton) {
match button {
MouseButton::Middle => {}
MouseButton::Left => {model.lifegame.next_gen()}
MouseButton::Right => {}
_ => {}
}
}
Leftボタンを押したときに、next_gen()が実行されるようにしました。
クリックするとこんな感じに更新されます。
まとめ
nannouを使って、ライフゲームの描画ができました。
もっとセルの数増やしたり、あとは、セルのセッターも作ったのでいろんな形を作ったりと幅は広がりますねー
参考資料
ライフゲームを作るうえで参考にしたサイトや書籍、資料など
書籍
岡瑞起、池上高志、ドミニク・チェン、青木竜太、丸山典宏『作って動かすALife』 オライリー・ジャパン(2018)
コメント