目的
「ブラウザのしくみ」が気になって仕方がないので購入したのだが、そもそも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に興味がない場合は普通にバックエンドを書くとかの方がわかりやすそう。
書き心地良すぎてヤヴァイ。これはハマる。