rustでライフゲームを作る#3-nannou編-

nannou

前回、前々回とライフゲームの骨格、アルゴリズムを実装してきました。

今回は、そのアルゴリズムに従って、生成されるライフゲームを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()でライフゲームのフィールドを生成します。これで初期状態のものができます。

ライフゲームの描画の下準備をする

ライフゲームの描画の下準備をします。

手順を確認してそれをプログラムにしていきます。

  1. 現在の全セルを取り出す。
  2. セルの位置を特定する
  3. 生きているセルを描画する

この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)

rustでライフゲームシリーズ

コメント

タイトルとURLをコピーしました