← Back to Lessons Lesson 24 of 28
Advanced graphicssimulation
Building a Wasm Game
Game Architecture
A Wasm game splits responsibility between Rust and JavaScript:
Rust (game logic) JavaScript (rendering + input)
┌────────────────────┐ ┌────────────────────────┐
│ Game state │ │ Canvas 2D rendering │
│ Physics/collision │◀───────▶│ Keyboard/mouse input │
│ Entity management │ data │ requestAnimationFrame │
│ Score/rules │ │ Audio playback │
└────────────────────┘ └────────────────────────┘Rust owns the state, JS handles I/O.
The Game Loop
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);
// Track pressed keys
const keys = new Set();
document.addEventListener('keydown', (e) => keys.add(e.key));
document.addEventListener('keyup', (e) => keys.delete(e.key));
function gameLoop() {
// 1. Input
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. Update (Rust)
game.update();
// 3. Render (JS)
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw player
ctx.fillStyle = '#654ff0';
ctx.beginPath();
ctx.arc(game.player_x(), game.player_y(), game.player_size(), 0, Math.PI * 2);
ctx.fill();
// Draw targets
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();
}
// Draw score
ctx.fillStyle = '#f1f5f9';
ctx.font = '20px monospace';
ctx.fillText(`Score: ${game.score()}`, 10, 30);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);Collision Detection
Circle-circle collision
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 // Avoid sqrt for performance
}AABB (rectangle) collision
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
}Entity Component System (ECS) Pattern
For complex games, use an ECS pattern:
#[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()
}
}Performance Tips for Games
| Tip | Why |
|---|---|
Use flat arrays (Vec<f64>) for bulk data |
Minimizes Wasm↔JS boundary crossing |
| Avoid allocating per frame | Reuse buffers with .clear() |
Use f64 not f32 in JS-facing code |
JS numbers are f64 |
| Check collisions with spatial partitioning | Grid or quadtree for O(n) instead of O(n²) |
| Keep game logic in Rust, rendering in JS | Canvas API isn't accessible from Wasm |
Try It
The starter code implements a simple collectible game — a player that moves and collects targets. In a real project, you'd wire this to keyboard input and a Canvas rendering loop.
Try It
Chapter Quiz
Pass all questions to complete this lesson