0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブロックゲームでRustキャッチアップ

Posted at

目的

「ブラウザのしくみ」が気になって仕方がないので購入したのだが、そもそもRust書いたことないわ...という状況だったので前段階としてRustのキャッチアップをする。

せっかくなのでWebAssemblyもキャッチアップしたい...という欲が出たのでブロックゲーム的なものを作る。

購入したものはこちら

注意

Rust初心者目線で気になった点を書いているので、同じようなシチュエーションの方の読み物になれば嬉しいなと思っています。(wasmについても何もわかっていないです)

rustインストール

さくっとインストール。

WebAssemblyビルドツールをインストール

ビルドツールは別途インストールが必要っぽい。
rustupはツールチェインマネージャー。
cargoはパッケージマネージャー。

rustup target add wasm32-unknown-unknown
cargo install wasm-pack

VSCode 拡張

こちらの記事を参考に導入

Rustセットアップ

適当なプロジェクトディレクトリを作成して以下のコマンド実行。
最小のプロジェクトが生成された。

cargo init

必要なパッケージをインストール

cargo add wasm-bindgen
cargo add js-sys
cargo add web-sys --features "CanvasRenderingContext2d,Document,Element,HtmlCanvasElement,Window"

cargo.tomlに以下を追加。
shared libraryとして出力するための設定。

[lib]
crate-type = ["cdylib"]

src/lib.rsにWebAssemblyとして出力する内容を記述

lib.rsはライブラリクレートのエントリーポイントらしいので、WebAssembly出力時にはこっちを使う。
main.rsはバイナリクレートらしい。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

最後にwasm用のビルドをする。
ビルド結果はpkgに生成される。

wasm-pack build --target web

ここまででRustのセットアップは完了
検証用にwasmをコールするためのHTMLなどを準備する

呼び出し側のHTML準備

rootにこんな感じのファイルを作成

mkdir www
touch www/index.html
touch www/index.js

htmlでは所定のjsを読み込む

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Rust Block Game</title>
  </head>
  <body>
    <script type="module" src="./index.js"></script>
  </body>
</html>

Rustのビルド成果物はpkgに生成する想定。

import init, { greet } from '../pkg/rust_block_game.js';

async function run() {
    await init();
    const result = greet("Rust");
    console.log(result);
}

run();

ここまででindex.htmlのコンソール上にHello, Rust!が表示される。
こういう感じなのか。なるほど(´・ω・`)

wasmはjsからロードする模様。
wasmからDOMを操作できなかったりするらしいので、適材適所で使うという理解。

ざっくりグリッド実装

めちゃくちゃざっくり実装。
グリッドの生成と、ブロックの回転と移動まで。

use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};

#[derive(Clone, Copy, PartialEq)]
pub enum Cell {
    Empty,
    Filled(u8),
}
// テトリミノの形状を定義
#[derive(Clone, Copy, PartialEq)]
pub enum BlockType {
    I,
    O,
    T,
    S,
    Z,
    J,
    L,
}

#[derive(Clone)]
pub struct BlockShape {
    block_type: BlockType,
    cells: Vec<Vec<bool>>, // trueなら埋まっているセル
    color: u8,
}

impl BlockShape {
    pub fn new(block_type: BlockType) -> Self {
        let (cells, color) = match block_type {
            BlockType::I => (
                vec![
                    vec![false, false, false, false],
                    vec![true, true, true, true],
                    vec![false, false, false, false],
                    vec![false, false, false, false],
                ],
                0, // cyan
            ),
            BlockType::O => (
                vec![vec![true, true], vec![true, true]],
                1, // yellow
            ),
            BlockType::T => (
                vec![
                    vec![false, true, false],
                    vec![true, true, true],
                    vec![false, false, false],
                ],
                2, // purple
            ),
            BlockType::S => (
                vec![
                    vec![false, true, true],
                    vec![true, true, false],
                    vec![false, false, false],
                ],
                5, // green
            ),
            BlockType::Z => (
                vec![
                    vec![true, true, false],
                    vec![false, true, true],
                    vec![false, false, false],
                ],
                6, // red
            ),
            BlockType::J => (
                vec![
                    vec![true, false, false],
                    vec![true, true, true],
                    vec![false, false, false],
                ],
                3, // blue
            ),
            BlockType::L => (
                vec![
                    vec![false, false, true],
                    vec![true, true, true],
                    vec![false, false, false],
                ],
                4, // orange
            ),
        };

        BlockShape {
            block_type,
            cells,
            color,
        }
    }

    // 時計回りに90度回転
    pub fn rotate(&mut self) {
        let n = self.cells.len();
        let mut rotated = vec![vec![false; n]; n];

        for i in 0..n {
            for j in 0..n {
                rotated[j][n - 1 - i] = self.cells[i][j];
            }
        }

        self.cells = rotated;
    }
}

#[wasm_bindgen]
pub struct Game {
    context: CanvasRenderingContext2d,
    canvas: HtmlCanvasElement,
    board: Vec<Vec<Cell>>,
    board_width: usize,
    board_height: usize,
    cell_size: f64,

    current_block: Option<BlockShape>,
    current_pos: (usize, usize), // (x, y)
}

#[wasm_bindgen]
impl Game {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Result<Game, JsValue> {
        let document = web_sys::window().unwrap().document().unwrap();
        let canvas = document.get_element_by_id("game-canvas").unwrap();
        let canvas: HtmlCanvasElement = canvas
            .dyn_into::<HtmlCanvasElement>()
            .map_err(|_| ())
            .unwrap();

        let canvas_width = 300.0;
        let canvas_height = 600.0;
        canvas.set_width(canvas_width as u32);
        canvas.set_height(canvas_height as u32);

        let context = canvas
            .get_context("2d")?
            .unwrap()
            .dyn_into::<CanvasRenderingContext2d>()
            .unwrap();

        let board_width = 10;
        let board_height = 20;
        let cell_size = canvas_width / board_width as f64;

        let board = vec![vec![Cell::Empty; board_width]; board_height];

        Ok(Game {
            context,
            canvas,
            board,
            board_width,
            board_height,
            cell_size,
            current_block: None,
            current_pos: (0, 0),
        })
    }

    pub fn draw(&self) {
        // 背景を黒に
        self.context.set_fill_style_str("black");
        self.context.fill_rect(
            0.0,
            0.0,
            self.canvas.width() as f64,
            self.canvas.height() as f64,
        );

        // グリッドとセルを描画
        for (y, row) in self.board.iter().enumerate() {
            for (x, cell) in row.iter().enumerate() {
                match cell {
                    Cell::Empty => {
                        // 空のセルは灰色の枠線のみ
                        self.context.set_stroke_style_str("gray");
                        self.context.stroke_rect(
                            x as f64 * self.cell_size,
                            y as f64 * self.cell_size,
                            self.cell_size,
                            self.cell_size,
                        );
                    }
                    Cell::Filled(color) => {
                        // 塗りつぶされたセルは色付きで描画
                        let color_str = match color {
                            0 => "cyan",
                            1 => "yellow",
                            2 => "purple",
                            3 => "blue",
                            4 => "orange",
                            5 => "green",
                            6 => "red",
                            _ => "white",
                        };
                        self.context.set_fill_style_str(color_str);
                        self.context.fill_rect(
                            x as f64 * self.cell_size,
                            y as f64 * self.cell_size,
                            self.cell_size,
                            self.cell_size,
                        );
                    }
                }
            }
        }

        if let Some(block) = &self.current_block {
            for (i, row) in block.cells.iter().enumerate() {
                for (j, &is_filled) in row.iter().enumerate() {
                    if is_filled {
                        let x = self.current_pos.0 + j;
                        let y = self.current_pos.1 + i;

                        self.context.set_fill_style_str(match block.color {
                            0 => "cyan",
                            1 => "yellow",
                            2 => "purple",
                            3 => "blue",
                            4 => "orange",
                            5 => "green",
                            6 => "red",
                            _ => "white",
                        });

                        self.context.fill_rect(
                            x as f64 * self.cell_size,
                            y as f64 * self.cell_size,
                            self.cell_size,
                            self.cell_size,
                        );
                    }
                }
            }
        }

        // デバッグ用
        web_sys::console::log_1(
            &format!(
                "Debug Info:\nBoard: {}x{}\nCell Size: {}\nCanvas: {}x{}",
                self.board_width,
                self.board_height,
                self.cell_size,
                self.canvas.width(),
                self.canvas.height()
            )
            .into(),
        );
    }

    // 左に移動
    pub fn move_left(&mut self) {
        if let Some(_) = &self.current_block {
            if self.current_pos.0 > 0 {
                self.current_pos.0 -= 1;
                self.draw();
            }
        }
    }

    // 右に移動
    pub fn move_right(&mut self) {
        if let Some(block) = &self.current_block {
            if self.current_pos.0 + block.cells.len() < self.board_width {
                self.current_pos.0 += 1;
                self.draw();
            }
        }
    }

    // 下に移動
    pub fn move_down(&mut self) {
        if let Some(block) = &self.current_block {
            if self.current_pos.1 + block.cells.len() < self.board_height {
                self.current_pos.1 += 1;
                self.draw();
            }
        }
    }

    // ミノを回転
    pub fn rotate(&mut self) {
        if let Some(block) = &mut self.current_block {
            block.rotate();
            self.draw();
        }
    }

    // テスト用:テノを生成して表示
    pub fn spawn_test_mino(&mut self) {
        let block = BlockShape::new(BlockType::T); // Tミノをテスト用に生成
        self.current_block = Some(block);
        self.current_pos = (self.board_width / 2 - 2, 0); // 上端中央に配置
        self.draw();
    }

    pub fn test_fill(&mut self) {
        self.board[0][0] = Cell::Filled(0); // cyan
        self.board[1][1] = Cell::Filled(1); // yellow
        self.board[2][2] = Cell::Filled(2); // purple
        self.draw();
    }
}
import init, { Game } from '../pkg/rust_block_game.js';

async function run() {
    await init();
    const game = new Game();
    // game.draw_test();
    // game.test_fill();
    game.spawn_test_mino();


    // キーボードイベントの処理
    document.addEventListener('keydown', (event) => {
        switch (event.key) {
            case 'ArrowLeft':
                game.move_left();
                break;
            case 'ArrowRight':
                game.move_right();
                break;
            case 'ArrowDown':
                game.move_down();
                break;
            case 'ArrowUp':    // 上キーで回転
                game.rotate();
                break;
        }
    });

}

run();

結論

最低限の記述はなんとなくわかったので、Rustを書いたことがない人の取っ掛かりとしてはいいかも?と思いました。
業務アプリからは少し外れてしまうので、wasmに興味がない場合は普通にバックエンドを書くとかの方がわかりやすそう。

書き心地良すぎてヤヴァイ。これはハマる。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?