【Rust】Tauri + WebAssemblyでゲームを作る

## はじめに

Tauri上で動くFlappy Birdライクなゲームを作りました。
スペースキーでジャンプしてブロックを回避します。
~~画像を用意するのが面倒くさいので~~自機もブロックも全て矩形で表現しています。

![tauri-bird.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/3c36d763-9e74-0c04-9fc8-fd436d4b72fc.gif)

https://github.com/yt2b/Tauri-bird/tree/7dc243e1efe7bc05f1a51aed3ffb22f8996f1f3a

RustのコードをWebAssemblyにコンパイルして`Canvas`状に描画しています。
よろしければ以下の記事もどうぞ。

https://qiita.com/k-yaina60/items/2d1b458a87aa8d5d8be3

## コア部分

画面の描画、入力の検知、ゲームのループ等の処理はDOM操作やJavaScript関数の呼び出しが必要です。
ですが、これらの処理をRustで呼び出すのは結構大変です。

なので面倒な処理をラップするengineモジュールを作りました。
ゲームのロジック部分はこれらのモジュールを呼び出すことで関心を分離させます。

https://github.com/yt2b/Tauri-bird/tree/7dc243e1efe7bc05f1a51aed3ffb22f8996f1f3a/src/engine

## 自機

毎フレーム`update`関数が呼び出され地面に落下していきます。
またスペースキーが押された瞬間に`fly`関数が呼び出されて上にジャンプすることができます。

```rust
pub struct Player {
    pub rect: Rect,
    velocity: f32,
}

impl Player {
    pub fn new() -> Self {
        Self {
            rect: Rect::new(Vector2::new(300.0, 150.0), Vector2::new(50.0, 50.0)),
            velocity: 0.0,
        }
    }

    pub fn fly(&mut self) {
        self.velocity = -12.0;
    }

    pub fn update(&mut self) {
        self.velocity += 0.6;
        self.rect.pos.y += self.velocity;
        if self.rect.pos.y <= 0.0 {
            self.rect.pos.y = 0.0;
            self.velocity = 0.0;
        }
    }

    pub fn draw(&self, renderer: &Renderer) {
        renderer.set_rgb(235, 145, 0);
        renderer.draw_rect(&self.rect);
    }
}
```

## ブロック

ブロックにはいくつか種類があるので、共通処理をまとめたBlockトレイトを定義します。

```rust:block.rs
pub trait Block {
    fn update(&mut self, speed: f32);
    fn is_alive(&self) -> bool;
    fn get_rect(&self) -> &[Rect];
    fn draw(&self, renderer: &Renderer);
}
```

各関数の説明は以下の通りです。

* `update`関数
ブロックの座標を更新する関数です。
引数に移動量を取ります。
* `is_alive`関数
ブロックが画面内にあるかを判定する関数です。
画面外に移動した場合、`false`を返します。
`false`を返したブロックは管理リスト内から除外されます。
* `get_rect`関数
ブロックが持つ矩形を返す関数です。
複数の矩形を持つ場合があるので配列の参照を返します。
* `draw`関数
ブロックを画面上に描画する関数です。

### 地面

地面は画面下に存在するブロックです。
移動はしないので`update`関数内で座標は更新せず、`is_alive`関数は常に`true`を返します。

```rust:block.rs
impl Block for Ground {
    fn update(&mut self, _: f32) {}

    fn is_alive(&self) -> bool {
        true
    }

    fn get_rect(&self) -> &[Rect] {
        &self.rects
    }

    fn draw(&self, renderer: &Renderer) {
        renderer.set_rgb(116, 80, 48);
        renderer.draw_rect(&self.rects[0]);
    }
}
```

![ground.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/2fc19200-2257-9aed-9577-ac5b22c8f9b0.png)

### 通常のブロック

画面上を右から左へ移動するブロックです。
`update`関数内で全てのブロックのx座標を更新します。
ブロックの右端のx座標が0未満になったら、`is_alive`は`false`を返します。

```rust:block.rs
impl Block for Normal {
    fn update(&mut self, speed: f32) {
        for r in &mut self.rects {
            r.pos.x -= speed;
        }
    }

    fn is_alive(&self) -> bool {
        let rect = &self.rects[0];
        rect.pos.x + rect.size.x >= 0.0
    }

    fn get_rect(&self) -> &[Rect] {
        &self.rects
    }

    fn draw(&self, renderer: &Renderer) {
        renderer.set_rgb(0, 180, 0);
        for r in &self.rects {
            renderer.draw_rect(r);
        }
    }
}
```

![normal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/096a39a8-71f2-4b41-09f6-38f2843574b7.gif)


### スライドするブロック

画面上を右から左へ移動して、さらにブロック間の隙間が上下にスライドします。
`update`関数内でブロックのx座標と形を更新します。
その他の実装は通常のブロックと同じです。

```rust:block.rs
impl Block for Slide {
    fn update(&mut self, speed: f32) {
        for r in &mut self.rects {
            r.pos.x -= speed;
        }
        self.rects[0].size.y += self.v_y;
        self.rects[1].pos.y += self.v_y;
        self.rects[1].size.y -= self.v_y;
    }
    ...
}
```

![slide.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/1f44d52a-741d-494d-fcf0-136d7f3a319a.gif)


### ブロックの生成

一定の間隔ごとにブロックを追加します。
その際に乱数を使用することで、追加するブロックの種類や
ブロック間の隙間の座標をランダム化しています。

```rust:block.rs
const GROUND_Y: f32 = 550.0;
...
pub fn get_block() -> Box<dyn Block> {
    let mut rng = rand::thread_rng();
    let center_y = GROUND_Y / 2.0 + rng.gen_range(-120.0, 120.0);
    if rng.gen_range(0, 2) == 0 {
        Box::new(Normal::new(center_y, 185.0))
    } else {
        let v_y = if center_y < GROUND_Y / 2.0 { 0.5 } else { -0.5 };
        Box::new(Slide::new(center_y, 190.0, v_y))
    }
}
```

## 当たり判定

自機の矩形とブロックの矩形を比較して当たり判定を行います。
1つでも当たっていたらゲームオーバーフラグを`true`にします。

```rust
impl Game for Bird {
    fn update(&mut self, key_state: &KeyState) {
            ...
            for block in &mut self.blocks {
                block.update(SPEED);
                for r in block.get_rect() {
                    if self.player.rect.is_hit(r) {
                        self.is_game_over = true;
                    }
                }
            }
            ...
    }
}
```

ゲームオーバーの場合は画面に文字を表示させます。
スペースキーを押すことで再挑戦できます。

![gameover.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/e855f7d6-bf0e-5099-0bf3-ce8b3a1986a8.png)

## おわりに

メインのゲームロジックは比較的簡単でしたが
低レイヤー部分をRustで実装するのはかなり大変でした。
ブラウザ上のゲームはJavaScriptかTypeScriptで書いた方が楽だと思います。

## 参考文献

https://ja.wikipedia.org/wiki/Flappy_Bird

https://github.com/rustwasm/wasm-bindgen