前回、前々回とライフゲームの骨格、アルゴリズムを実装してきました。
今回は、そのアルゴリズムに従って、生成されるライフゲームを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)
 
 





コメント