上級 graphicssimulation

Wasmゲームの構築

ゲームアーキテクチャ

Wasmゲームは責任をRustとJavaScriptに分担します:

Rust(ゲームロジック)             JavaScript(描画 + 入力)
┌────────────────────┐         ┌────────────────────────┐
│ ゲーム状態          │         │ Canvas 2D描画          │
│ 物理/衝突判定       │◀───────▶│ キーボード/マウス入力    │
│ エンティティ管理     │  data   │ requestAnimationFrame  │
│ スコア/ルール       │         │ オーディオ再生          │
└────────────────────┘         └────────────────────────┘

Rustが状態を管理し、JSがI/Oを担当します。

ゲームループ

import init, { Game } from './pkg/game.js';

await init();

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const game = new Game(canvas.width, canvas.height);

// 押されているキーを追跡
const keys = new Set();
document.addEventListener('keydown', (e) => keys.add(e.key));
document.addEventListener('keyup', (e) => keys.delete(e.key));

function gameLoop() {
    // 1. 入力
    const speed = 4;
    if (keys.has('ArrowLeft'))  game.move_player(-speed, 0);
    if (keys.has('ArrowRight')) game.move_player(speed, 0);
    if (keys.has('ArrowUp'))    game.move_player(0, -speed);
    if (keys.has('ArrowDown'))  game.move_player(0, speed);

    // 2. 更新(Rust)
    game.update();

    // 3. 描画(JS)
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // プレイヤーを描画
    ctx.fillStyle = '#654ff0';
    ctx.beginPath();
    ctx.arc(game.player_x(), game.player_y(), game.player_size(), 0, Math.PI * 2);
    ctx.fill();

    // ターゲットを描画
    const positions = game.target_positions();
    ctx.fillStyle = '#ce422b';
    for (let i = 0; i < positions.length; i += 2) {
        ctx.beginPath();
        ctx.arc(positions[i], positions[i + 1], 15, 0, Math.PI * 2);
        ctx.fill();
    }

    // スコアを描画
    ctx.fillStyle = '#f1f5f9';
    ctx.font = '20px monospace';
    ctx.fillText(`Score: ${game.score()}`, 10, 30);

    requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

衝突判定

円同士の衝突

fn circles_collide(x1: f64, y1: f64, r1: f64, x2: f64, y2: f64, r2: f64) -> bool {
    let dx = x1 - x2;
    let dy = y1 - y2;
    let dist_sq = dx * dx + dy * dy;
    let radii = r1 + r2;
    dist_sq < radii * radii  // パフォーマンスのためsqrtを回避
}

AABB(矩形)衝突

fn aabb_collide(
    x1: f64, y1: f64, w1: f64, h1: f64,
    x2: f64, y2: f64, w2: f64, h2: f64,
) -> bool {
    x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2
}

エンティティ・コンポーネント・システム(ECS)パターン

複雑なゲームにはECSパターンを使います:

#[wasm_bindgen]
pub struct World {
    positions: Vec<f64>,    // [x0, y0, x1, y1, ...]
    velocities: Vec<f64>,   // [vx0, vy0, vx1, vy1, ...]
    sizes: Vec<f64>,        // [r0, r1, ...]
    alive: Vec<bool>,
    count: usize,
}

#[wasm_bindgen]
impl World {
    pub fn update(&mut self, dt: f64) {
        for i in 0..self.count {
            if !self.alive[i] { continue; }
            let idx = i * 2;
            self.positions[idx] += self.velocities[idx] * dt;
            self.positions[idx + 1] += self.velocities[idx + 1] * dt;
        }
    }

    pub fn positions(&self) -> Vec<f64> {
        self.positions.clone()
    }
}

ゲームのパフォーマンスヒント

ヒント 理由
バルクデータにはフラット配列(Vec<f64>)を使う Wasm↔JS間の境界越えを最小化
フレームごとのアロケーションを避ける .clear() でバッファを再利用
JS向けコードでは f32 ではなく f64 を使う JSの数値はf64
空間分割で衝突判定する グリッドや四分木でO(n²)をO(n)に
ゲームロジックはRust、描画はJSで Canvas APIはWasmからアクセスできない

試してみよう

スターターコードはシンプルな収集ゲームを実装しています — プレイヤーが移動してターゲットを集めます。実際のプロジェクトでは、キーボード入力とCanvasの描画ループに接続します。

試してみる

チャプタークイズ

すべての問題に正解してレッスンを完了しましょう