19
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

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

tauri-bird.gif

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

コア部分

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

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

自機

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

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トレイトを定義します。

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を返します。

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

通常のブロック

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

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

スライドするブロック

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

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

ブロックの生成

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

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にします。

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

おわりに

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

参考文献

19
12
0

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
19
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?