はじめに
Tauri上で動くFlappy Birdライクなゲームを作りました。
スペースキーでジャンプしてブロックを回避します。
画像を用意するのが面倒くさいので自機もブロックも全て矩形で表現しています。
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トレイトを定義します。
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
を返します。
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]);
}
}
通常のブロック
画面上を右から左へ移動するブロックです。
update
関数内で全てのブロックのx座標を更新します。
ブロックの右端のx座標が0未満になったら、is_alive
はfalse
を返します。
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);
}
}
}
スライドするブロック
画面上を右から左へ移動して、さらにブロック間の隙間が上下にスライドします。
update
関数内でブロックのx座標と形を更新します。
その他の実装は通常のブロックと同じです。
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;
}
...
}
ブロックの生成
一定の間隔ごとにブロックを追加します。
その際に乱数を使用することで、追加するブロックの種類や
ブロック間の隙間の座標をランダム化しています。
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;
}
}
}
...
}
}
ゲームオーバーの場合は画面に文字を表示させます。
スペースキーを押すことで再挑戦できます。
おわりに
メインのゲームロジックは比較的簡単でしたが
低レイヤー部分をRustで実装するのはかなり大変でした。
ブラウザ上のゲームはJavaScriptかTypeScriptで書いた方が楽だと思います。
参考文献