← レッスン一覧に戻る レッスン 24 / 28
上級 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の描画ループに接続します。
試してみる
チャプタークイズ
すべての問題に正解してレッスンを完了しましょう