LoginSignup
54
38

More than 3 years have passed since last update.

Rust のゲームエンジン Piston で矩形波を描く

Last updated at Posted at 2020-04-21

動機

正弦波の組み合わせで矩形波(くけいは)ができることを4歳の娘に視覚的に理解させようと思い、下に示すアニメーションを作ってみました。Rust で。

N=1
n1.gif
N=3
n3.gif
N=10
n10.gif
眺めてるだけでも楽しいですね。娘のウケはイマイチでした。

Piston

Piston は Rust 製のゲームエンジンです。Piston は初めて使うので、Rustでスネークゲームを作る【pistonチュートリアル】 の記事がとても参考になりました。

ウィンドウの表示にはpiston_windowcrate を使います。

main.rs
use piston_window::*;
const FPS: u64 = 60;

fn main() {
    let mut window: PistonWindow = WindowSettings::new("Hello, Wave!", [1000, 480])
        .exit_on_esc(true)
        .samples(4)
        .build()
        .unwrap();
    window.set_ups(FPS);

    while let Some(e) = window.next() {
        if let Some(_) = e.render_args() {
            // 描画処理
        }
        if let Some(_) = e.update_args() {
            // 状態の更新処理
        }
    }
}

データ構造

正弦波を組み合わせるということは、円盤の端っこに円盤が乗っていて、その円盤の端っこにまた円盤が乗っていて、それぞれがぐるぐると回っているということです。上のアニメーションの青い○が円盤です。なのでそれぞれの円盤は親となる円盤を持っていて、ある円盤自身の中心位置は親円盤の状態に依存していることになります。Rust でこのようなデータ構造を表してみましょう。

失敗例

まずは円盤構造体の定義。

struct Disk {
    center: (f64, f64), // 中心位置
    radius: f64,        // 半径
    degree: f64,        // 角度
}

続いてApp構造体を定義します。メンバとしてVec<Disk>を持たせます。そのVecに円盤を詰め込んでいって、自身の1個前の円盤が親円盤という状態です。updateメソッドで状態の更新を行います。しかしこれはコンパイルエラー。なぜだか分かりますよね。

struct App {
    disks: Vec<Disk>,
}

impl App {
    fn update(&mut self) {
        for (i, d) in self.disks.iter_mut().enumerate() {
            // 自身の円盤角度を進める
            d.degree += 1.0;
            // 2個目以降の円盤なら
            if i > 0 {
                // 1個前の円盤(親円盤)の参照を得て...
                let parent = self.disks.get(i - 1).unwrap();
                // 自身の中心位置を更新
                d.center = (
                    parent.center.0 + parent.radius * (parent.degree / 180.0 * PI).cos(),
                    parent.center.1 + parent.radius * (parent.degree / 180.0 * PI).sin()                    
                );
            }
        }
    }
}

let disks = vec![];
for _ in 0..N {
    let disk = // 省略
    disks.push(disk);
}
let mut app = App { disks };
app.update();

成功例

Rust でこういうツリー構造を作るときはRcRefCellを使うのが一般的(?)みたいです。では円盤構造体を書き直します。(Rc参照カウント方式のスマートポインタRefCell"実行時に"借用規則を強制する型なんですって。)親円盤は無い場合もあるのでOptionで包んでおきます。

use std::rc::Rc;
use std::cell::RefCell;

struct Disk {
    parent: Option<Rc<RefCell<Disk>>>, // 親円盤への参照
    center: (f64, f64), // 中心位置
    radius: f64,        // 半径
    degree: f64,        // 角度
}

App構造体も書き直します。VecにはDisk構造体ではなく、Diskへのポインタを持たせます。updateメソッドも以下のようになります。これで親円盤の状態によって自身の中心位置を更新できるようになりました。

struct App {
    // disks: Vec<Disk>,
    disks: Vec<Rc<RefCell<Disk>>>,
}

impl App {
    fn update(&mut self) {
        for d in self.disks.iter() {
            // 自身への可変の参照を得る
            let mut disk = d.borrow_mut();
            // 自身の円盤角度を進める
            disk.degree += 1.0;
            // 親円盤があるなら
            if let Some(parent) = disk.parent.clone() {
                // 親円盤への不変の参照を得る
                let parent = parent.borrow();
                // 自身の中心位置を更新
                disk.center = (
                    parent.center.0 + parent.radius * (parent.degree / 180.0 * PI).cos(),
                    parent.center.1 + parent.radius * (parent.degree / 180.0 * PI).sin(),
                );
            }
        }
    }
}

let disks = vec![];
// 最初の円盤には親円盤はない
let mut parent = None;
for _ in 0..N {
    let disk = Disk {
        parent,
        // 省略
    };
    // 円盤へのポインタを作る
    let disk_rc = Rc::new(RefCell::new(disk));
    // ポインタを複製して Vec に追加
    disks.push(disk_rc.clone());
    // 親円盤へのポインタ
    parent = Some(disk_rc);
}
let mut app = App { disks };
app.update();

描画

App構造体にrenderメソッドを書きます。実際にはもう少し工夫が必要になりますが簡単のために省略します。是非ご自身で実装を考えてみてください。

impl App {
    fn update(&mut self) {
        // 省略
    }

    fn render<E: GenericEvent>(&self, e: &E, window: &mut PistonWindow) {
        window.draw_2d(e, |c, g, _| {
            clear(color::WHITE, g);
            self.disks.iter().for_each(|d| {
                let disk = d.borrow();
                // 省略
                // piston の line 関数とか circle_arc 関数とか使って
                // ゴリゴリと描画処理を書いていく
            });
        });
    }
}

main関数はこんな感じになります。

fn main() {
    let mut disks = vec![];
    for _ 0..N {
        // disks の初期化処理は省略
        // 矩形波になるように Disk の半径は (2k-1) ずつ小さくして
        // 周波数は (2k-1) ずつ大きくしないといけません
    }
    let mut app = App { disks };

    let mut window: PistonWindow = WindowSettings::new("Hello, Wave!", [1000, 480])
        .exit_on_esc(true)
        .samples(4)
        .build()
        .unwrap();
    window.set_ups(FPS);

    while let Some(e) = window.next() {
        if let Some(_) = e.render_args() {
            // 描画処理
            app.render(&e, &mut window);
        }
        if let Some(_) = e.update_args() {
            // 状態の更新処理
            app.update();
        }
    }
}

まとめ

  • Piston を使ってみたよ
  • RcRefCell の使いどころが分かったよ
  • 動くものができるとたのしい:blush:
54
38
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
38