docs: complete project research synthesis
Synthesized 4 parallel research efforts into comprehensive SUMMARY.md: - STACK.md: Vanilla Canvas 2D, fixed timestep, Web Audio, no dependencies - FEATURES.md: MVP features + v1.x enhancements + defer list - ARCHITECTURE.md: Game loop + state machine + entity patterns, 10-phase build order - PITFALLS.md: 8 critical pitfalls with prevention strategies Key recommendations: - Use fixed timestep accumulator (60 Hz physics, variable rendering) - Implement 10 phases from game loop foundation to cross-browser testing - Address critical pitfalls early (tunneling, timing, DPI, autoplay, AI, power-ups, lag, memory) - MVP ships with core Pong + AI + menus + basic polish - v1.x adds particles, trails, power-ups, arenas All research backed by official sources (MDN, web.dev, Chrome docs) and established patterns. Confidence: HIGH. Ready for requirements definition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
790
.planning/research/ARCHITECTURE.md
Normal file
790
.planning/research/ARCHITECTURE.md
Normal file
@@ -0,0 +1,790 @@
|
||||
# Architecture Research: HTML5 Canvas Arcade Games
|
||||
|
||||
**Domain:** HTML5 Canvas 2D arcade games (Pong-like)
|
||||
**Researched:** 2026-03-10
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Standard Architecture
|
||||
|
||||
### System Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Canvas View │ │ UI Layer │ │ Audio Out │ │
|
||||
│ │ (rendering) │ │ (DOM menus) │ │ (Web Audio) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
├─────────┴─────────────────┴─────────────────┴──────────────────┤
|
||||
│ Game Loop Manager │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ requestAnimationFrame → Update → Render → Loop │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ State Machine │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Menu │→ │ Game │→ │ Pause │→ │GameOver │ │
|
||||
│ │ State │ │ State │ │ State │ │ State │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Game Logic Layer │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Entity Mgr │ │ Physics/ │ │ Collision │ │
|
||||
│ │ (ball, │ │ Movement │ │ Detection │ │
|
||||
│ │ paddles) │ │ Update │ │ │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Particle │ │ Power-up │ │
|
||||
│ │ System │ │ Logic │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Input Layer │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Keyboard │ │ Input State │ │ Command │ │
|
||||
│ │ Events │ │ Manager │ │ Queue │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
| Component | Responsibility | Typical Implementation |
|
||||
|-----------|----------------|------------------------|
|
||||
| **Game Loop Manager** | Orchestrates frame timing, calls update/render in sequence, manages requestAnimationFrame | Single `gameLoop()` function that runs at ~60fps |
|
||||
| **State Machine** | Transitions between menu, playing, paused, game over states; isolates logic per scene | Object with `onEnter()`, `update()`, `render()`, `onExit()` methods per state |
|
||||
| **Input Manager** | Captures keyboard events, debounces, maintains current key state | Listens to keydown/keyup, maintains a `keysPressed` object |
|
||||
| **Entity Manager** | Owns and updates all game objects (paddles, ball, power-ups) | Array of entities with position, velocity, update/render methods |
|
||||
| **Physics/Movement** | Updates entity positions based on velocity, applies gravity if needed | `velocity.x`, `velocity.y` updated per frame, position += velocity |
|
||||
| **Collision Detection** | Detects overlaps between entities, triggers responses | AABB (Axis-Aligned Bounding Box) for rectangles; circle distance for round objects |
|
||||
| **Particle System** | Manages short-lived visual effects (impacts, explosions, trails) | Maintains particle array, updates lifetime/position, removes expired particles |
|
||||
| **Power-up System** | Spawns power-ups, detects collection, applies effects | Object with spawn logic, duration tracking, effect callbacks |
|
||||
| **Audio Manager** | Plays sound effects and music with pooling to avoid cutoff | Array of audio elements for each sound, round-robin playback |
|
||||
| **Renderer** | Draws all entities to canvas | Canvas 2D context calls in specific order (background, entities, UI) |
|
||||
|
||||
## Recommended Project Structure
|
||||
|
||||
For a vanilla JavaScript HTML5 Canvas game (no build tools), structure as a single HTML file or minimal multi-file approach:
|
||||
|
||||
```
|
||||
index.html # Single entry point (if single-file)
|
||||
├── <canvas>
|
||||
└── <script src="game.js">
|
||||
|
||||
OR multi-file (if modular):
|
||||
|
||||
game/
|
||||
├── index.html # Canvas element + script loading
|
||||
├── game.js # Main game loop and orchestration
|
||||
├── states/
|
||||
│ ├── MenuState.js # Title screen, mode selection
|
||||
│ ├── GameState.js # Active gameplay
|
||||
│ ├── PauseState.js # Pause overlay
|
||||
│ └── GameOverState.js # End screen
|
||||
├── entities/
|
||||
│ ├── Ball.js # Ball entity (position, velocity, radius)
|
||||
│ ├── Paddle.js # Paddle entity (position, score, AI logic)
|
||||
│ ├── PowerUp.js # Power-up entity
|
||||
│ └── EntityManager.js # Holds and updates all entities
|
||||
├── systems/
|
||||
│ ├── InputManager.js # Keyboard input handling
|
||||
│ ├── PhysicsSystem.js # Movement and velocity updates
|
||||
│ ├── CollisionSystem.js # Collision detection and response
|
||||
│ ├── ParticleSystem.js # Particle management
|
||||
│ └── AudioManager.js # Sound effects and music
|
||||
└── utils/
|
||||
├── constants.js # Game settings (canvas size, speeds)
|
||||
└── helpers.js # Utility functions
|
||||
```
|
||||
|
||||
### Structure Rationale
|
||||
|
||||
- **Single file vs. modular:** For a small game like Pong, a single HTML file works well for shareability. Use modular structure if the game grows (multiple levels, complex mechanics).
|
||||
- **States folder:** Each game scene (menu, gameplay, pause) is isolated, making it easy to change scene logic without affecting others.
|
||||
- **Entities folder:** Separates "what things exist" from "how the game works," making entity behavior updates straightforward.
|
||||
- **Systems folder:** Core game mechanics (input, physics, collision, audio) are independent modules that don't need to know about each other.
|
||||
- **Utils folder:** Shared constants and helpers keep the code DRY without creating unnecessary abstractions.
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Game Loop (requestAnimationFrame + Update/Render Separation)
|
||||
|
||||
**What:** The core loop calls `update()` to change game state, then `render()` to draw the result. Uses `requestAnimationFrame` to stay in sync with the browser's refresh cycle.
|
||||
|
||||
**When to use:** Every HTML5 Canvas game needs this. It's the heartbeat of the application.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pro:** Prevents visual glitches (draw happens after all logic updates)
|
||||
- **Pro:** Matches browser refresh rate (~60fps) automatically
|
||||
- **Pro:** Better battery life on mobile than raw setInterval
|
||||
- **Con:** Slightly more complex than a naive loop
|
||||
- **Con:** Frame rate is tied to display refresh (usually not an issue)
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
function gameLoop(timestamp) {
|
||||
// Calculate delta time for frame-rate independent movement
|
||||
const deltaTime = (timestamp - lastTimestamp) / 1000;
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// Update all game logic
|
||||
currentState.update(deltaTime);
|
||||
|
||||
// Clear and redraw
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
currentState.render(ctx);
|
||||
|
||||
// Continue loop
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
```
|
||||
|
||||
### Pattern 2: State Machine (Menu → Playing → Paused → GameOver)
|
||||
|
||||
**What:** The game has distinct scenes (states). Only one is active at a time. Each state has `onEnter()`, `update()`, `render()`, and `onExit()` hooks.
|
||||
|
||||
**When to use:** Any game with multiple screens (menu, gameplay, pause, game over).
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pro:** Clear separation of concerns — menu logic doesn't leak into gameplay
|
||||
- **Pro:** Easy to add new states without rewriting existing ones
|
||||
- **Pro:** Makes pause/resume trivial (just swap states)
|
||||
- **Con:** Requires discipline to avoid state-to-state coupling
|
||||
- **Con:** Small games might feel over-engineered with this pattern
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
class GameState {
|
||||
onEnter() {
|
||||
// Initialize game (reset ball, paddles, score)
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Update paddles, ball, check collisions, update score
|
||||
inputManager.poll();
|
||||
entityManager.updateAll(deltaTime);
|
||||
collisionSystem.check(entityManager.entities);
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
// Draw paddles, ball, particles, score
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
entityManager.renderAll(ctx);
|
||||
particleSystem.render(ctx);
|
||||
}
|
||||
|
||||
onExit() {
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
currentState = new MenuState();
|
||||
|
||||
update(deltaTime) {
|
||||
this.currentState.update(deltaTime);
|
||||
// Check for state transitions
|
||||
if (someCondition) {
|
||||
this.currentState.onExit();
|
||||
this.currentState = new GameState();
|
||||
this.currentState.onEnter();
|
||||
}
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
this.currentState.render(ctx);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Entity System (Ball, Paddles, Power-ups as Objects)
|
||||
|
||||
**What:** All dynamic game objects (ball, paddles, power-ups) inherit from a base `Entity` class or implement a common interface. Entities have position, velocity, and `update()`/`render()` methods.
|
||||
|
||||
**When to use:** When you have multiple types of objects that behave similarly (moveable, renderable).
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pro:** Adding new entity types (new power-up, new obstacle) is just another class
|
||||
- **Pro:** All entities update/render the same way in the loop
|
||||
- **Pro:** Easy to track all game objects in one collection
|
||||
- **Con:** Overkill for very simple games with only 3 objects
|
||||
- **Con:** Inheritance hierarchies can get messy if not carefully designed
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
class Entity {
|
||||
constructor(x, y, width, height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.velocity = { x: 0, y: 0 };
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.x += this.velocity.x * deltaTime;
|
||||
this.y += this.velocity.y * deltaTime;
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
// Override in subclass
|
||||
}
|
||||
}
|
||||
|
||||
class Ball extends Entity {
|
||||
constructor(x, y, radius) {
|
||||
super(x, y, radius * 2, radius * 2);
|
||||
this.radius = radius;
|
||||
this.velocity = { x: 300, y: 300 }; // pixels per second
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
class Paddle extends Entity {
|
||||
constructor(x, y, width, height) {
|
||||
super(x, y, width, height);
|
||||
this.score = 0;
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(this.x, this.y, this.width, this.height);
|
||||
}
|
||||
}
|
||||
|
||||
const entities = [];
|
||||
entities.push(new Ball(canvasWidth / 2, canvasHeight / 2, 5));
|
||||
entities.push(new Paddle(10, canvasHeight / 2 - 40, 10, 80));
|
||||
entities.push(new Paddle(canvasWidth - 20, canvasHeight / 2 - 40, 10, 80));
|
||||
|
||||
// In game loop:
|
||||
entities.forEach(e => e.update(deltaTime));
|
||||
entities.forEach(e => e.render(ctx));
|
||||
```
|
||||
|
||||
### Pattern 4: Collision Detection (AABB Bounding Box for Rectangles)
|
||||
|
||||
**What:** Check if two axis-aligned rectangles overlap using four boundary conditions. For the ball (circle), check distance between centers against sum of radii.
|
||||
|
||||
**When to use:** Every frame to detect ball-paddle hits, ball-boundaries, paddle-obstacles.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pro:** Fast and simple for rectangular objects
|
||||
- **Pro:** Good enough for arcade games (player won't notice slight imprecision)
|
||||
- **Con:** Not pixel-perfect — ball can clip through corners
|
||||
- **Con:** Doesn't handle rotating objects well
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Rectangle-Rectangle collision (paddle-to-paddle boundaries)
|
||||
function checkAABB(rectA, rectB) {
|
||||
return (
|
||||
rectA.x < rectB.x + rectB.width &&
|
||||
rectA.x + rectA.width > rectB.x &&
|
||||
rectA.y < rectB.y + rectB.height &&
|
||||
rectA.y + rectA.height > rectB.y
|
||||
);
|
||||
}
|
||||
|
||||
// Circle-Rectangle collision (ball-to-paddle)
|
||||
function checkCircleRect(circle, rect) {
|
||||
const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
|
||||
const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));
|
||||
|
||||
const distanceX = circle.x - closestX;
|
||||
const distanceY = circle.y - closestY;
|
||||
|
||||
return (distanceX * distanceX + distanceY * distanceY) < (circle.radius * circle.radius);
|
||||
}
|
||||
|
||||
// In collision system:
|
||||
collisionSystem.check = function(entities) {
|
||||
const ball = entities.find(e => e instanceof Ball);
|
||||
const paddles = entities.filter(e => e instanceof Paddle);
|
||||
|
||||
paddles.forEach(paddle => {
|
||||
if (checkCircleRect(ball, paddle)) {
|
||||
ball.velocity.x = -ball.velocity.x; // Bounce
|
||||
ball.x = ball.velocity.x > 0 ? paddle.x + paddle.width : paddle.x - ball.radius;
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Pattern 5: Particle System (Short-Lived Visual Effects)
|
||||
|
||||
**What:** Maintains an array of particles (small objects with position, velocity, lifetime). Each frame, particles move, fade out, and are removed when dead.
|
||||
|
||||
**When to use:** Impact effects, trail effects, explosions — anything visual that lasts a few frames.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pro:** Creates visual juice and feedback
|
||||
- **Pro:** Performant (particles are simple, deleted quickly)
|
||||
- **Con:** Particles can accumulate if not carefully pruned
|
||||
- **Con:** Requires tuning (count, lifetime, velocity) for good feel
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
class Particle {
|
||||
constructor(x, y, vx, vy, lifetime) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.lifetime = lifetime;
|
||||
this.maxLifetime = lifetime;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.x += this.vx * deltaTime;
|
||||
this.y += this.vy * deltaTime;
|
||||
this.lifetime -= deltaTime;
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const alpha = this.lifetime / this.maxLifetime;
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(this.x, this.y, 2, 2);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
isAlive() {
|
||||
return this.lifetime > 0;
|
||||
}
|
||||
}
|
||||
|
||||
class ParticleSystem {
|
||||
particles = [];
|
||||
|
||||
burst(x, y, count = 10) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = (Math.PI * 2 * i) / count;
|
||||
const vx = Math.cos(angle) * 200;
|
||||
const vy = Math.sin(angle) * 200;
|
||||
this.particles.push(new Particle(x, y, vx, vy, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.particles.forEach(p => p.update(deltaTime));
|
||||
this.particles = this.particles.filter(p => p.isAlive());
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
this.particles.forEach(p => p.render(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
// In game loop, on ball-paddle collision:
|
||||
particleSystem.burst(ball.x, ball.y, 8);
|
||||
```
|
||||
|
||||
### Pattern 6: Audio Manager with Sound Pooling
|
||||
|
||||
**What:** Maintain a pool of Audio objects for each sound effect. Cycle through them to avoid cutting off sounds when playing rapid-fire effects.
|
||||
|
||||
**When to use:** When you need to play the same sound multiple times in quick succession (paddle hits, power-up pickups).
|
||||
|
||||
**Trade-offs:**
|
||||
- **Pro:** Sounds don't cut each other off
|
||||
- **Pro:** No lag from creating new Audio objects
|
||||
- **Con:** Requires pre-loading and managing multiple copies of each sound
|
||||
- **Con:** Web Audio API is more complex but more powerful
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
class AudioManager {
|
||||
sounds = {};
|
||||
|
||||
registerSound(name, filename, poolSize = 3) {
|
||||
this.sounds[name] = [];
|
||||
for (let i = 0; i < poolSize; i++) {
|
||||
const audio = new Audio(filename);
|
||||
audio.poolName = name;
|
||||
audio.ready = false;
|
||||
audio.addEventListener('canplaythrough', () => {
|
||||
audio.ready = true;
|
||||
});
|
||||
this.sounds[name].push(audio);
|
||||
}
|
||||
}
|
||||
|
||||
play(name, volume = 1.0) {
|
||||
const pool = this.sounds[name];
|
||||
if (!pool) {
|
||||
console.warn(`Sound "${name}" not registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find next available sound in pool
|
||||
for (let audio of pool) {
|
||||
if (audio.paused) {
|
||||
audio.currentTime = 0;
|
||||
audio.volume = volume;
|
||||
audio.play().catch(() => {
|
||||
// Browser autoplay policy blocks it — ok to ignore
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
const audioManager = new AudioManager();
|
||||
audioManager.registerSound('paddle-hit', 'sounds/paddle.wav', 4);
|
||||
audioManager.registerSound('score', 'sounds/score.wav', 1);
|
||||
|
||||
// In collision system:
|
||||
audioManager.play('paddle-hit', 0.7);
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Game Loop Flow
|
||||
|
||||
```
|
||||
Frame Start
|
||||
↓
|
||||
Input Manager (poll keyboard)
|
||||
↓
|
||||
Current State.update(deltaTime)
|
||||
│
|
||||
├→ Entity Manager.update() [move ball, paddles]
|
||||
│
|
||||
├→ Collision System.check() [detect hits, trigger events]
|
||||
│
|
||||
├→ Particle System.update() [age and remove particles]
|
||||
│
|
||||
└→ Audio Manager [enqueue sounds from events]
|
||||
↓
|
||||
Canvas.clearRect() [blank the canvas]
|
||||
↓
|
||||
Current State.render(ctx)
|
||||
│
|
||||
├→ Background [draw arena]
|
||||
│
|
||||
├→ Entity Manager.render() [draw paddles, ball]
|
||||
│
|
||||
├→ Particle System.render() [draw particle effects]
|
||||
│
|
||||
└→ UI Layer [draw score, instructions]
|
||||
↓
|
||||
Request next frame
|
||||
```
|
||||
|
||||
### State Transition Flow
|
||||
|
||||
```
|
||||
Current State (e.g., MenuState)
|
||||
↓
|
||||
Check for exit conditions
|
||||
↓
|
||||
Call currentState.onExit()
|
||||
↓
|
||||
Create new state (e.g., GameState)
|
||||
↓
|
||||
Call newState.onEnter()
|
||||
↓
|
||||
Update state reference
|
||||
↓
|
||||
Next frame uses new state
|
||||
```
|
||||
|
||||
### Collision Response Flow
|
||||
|
||||
```
|
||||
Collision Detected (e.g., Ball ↔ Paddle)
|
||||
↓
|
||||
Update velocity [bounce ball]
|
||||
↓
|
||||
Trigger event [emit "paddle-hit"]
|
||||
↓
|
||||
Particle System.burst() [visual feedback]
|
||||
↓
|
||||
Audio Manager.play() [sound feedback]
|
||||
↓
|
||||
Power-up System [check if power-up triggered]
|
||||
```
|
||||
|
||||
## Build Order (Recommended Implementation Sequence)
|
||||
|
||||
The following order minimizes integration complexity and ensures each phase is testable:
|
||||
|
||||
### Phase 1: Game Loop + Single State
|
||||
1. Create canvas, get context
|
||||
2. Implement `requestAnimationFrame` loop
|
||||
3. Implement first state (GameState with basic ball + one paddle)
|
||||
4. Test: Ball moves, paddle can be manually updated
|
||||
|
||||
**Deliverable:** Ball bounces off canvas edges, stationary paddle on screen
|
||||
|
||||
### Phase 2: Input + Movement
|
||||
1. Implement InputManager (keyboard event listeners)
|
||||
2. Wire input to paddle 1 movement
|
||||
3. Test: Paddle responds to keys
|
||||
|
||||
**Deliverable:** Player can move left paddle up/down
|
||||
|
||||
### Phase 3: Collision + Physics
|
||||
1. Implement Collision System
|
||||
2. Ball bounces off paddles and walls
|
||||
3. Ball-paddle collision response (velocity reversal)
|
||||
|
||||
**Deliverable:** Ball bounces realistically off paddles and boundaries
|
||||
|
||||
### Phase 4: Second Paddle + Score
|
||||
1. Add paddle 2 (AI or second player)
|
||||
2. Implement score tracking (ball off-screen = point)
|
||||
3. Reset ball position on score
|
||||
|
||||
**Deliverable:** Game tracks score, one paddle controlled, one paddle AI or manual
|
||||
|
||||
### Phase 5: State Machine + Menu
|
||||
1. Implement StateMachine and multiple states (Menu, Game, GameOver)
|
||||
2. Add MenuState (start game, select mode)
|
||||
3. Add GameOverState (show winner, restart)
|
||||
4. Wire transitions
|
||||
|
||||
**Deliverable:** Full menu flow — start, play, game over, restart
|
||||
|
||||
### Phase 6: Audio
|
||||
1. Implement AudioManager with sound pooling
|
||||
2. Add sound effects for paddle hits, scoring, menu clicks
|
||||
3. Add background music to GameState
|
||||
|
||||
**Deliverable:** Game has audio feedback
|
||||
|
||||
### Phase 7: Particle System + Polish
|
||||
1. Implement ParticleSystem
|
||||
2. Emit particles on collisions, scoring
|
||||
3. Glow effects, trails (via particles)
|
||||
|
||||
**Deliverable:** Visually polished — particles on impact, trails on ball
|
||||
|
||||
### Phase 8: Power-ups
|
||||
1. Implement PowerUpSystem
|
||||
2. Power-up types: speed boost, paddle enlargement, ball split
|
||||
3. Spawn logic, collision detection, effect application
|
||||
|
||||
**Deliverable:** Power-ups spawn randomly, apply effects
|
||||
|
||||
### Phase 9: Multiple Arenas
|
||||
1. Add arena config (layout, obstacles, hazards)
|
||||
2. Arena selection in menu
|
||||
3. Level progression on wins
|
||||
|
||||
**Deliverable:** Multiple distinct arena layouts playable
|
||||
|
||||
### Phase 10: Polish + Refinement
|
||||
1. Tune difficulty curves (AI, ball speeds)
|
||||
2. Screen shake on impacts
|
||||
3. Pause state and pause key
|
||||
4. Settings menu (volume, difficulty)
|
||||
|
||||
**Deliverable:** Game feels complete and juicy
|
||||
|
||||
## Scaling Considerations
|
||||
|
||||
For an HTML5 Canvas Pong game, "scaling" is less about handling millions of users and more about complexity growth.
|
||||
|
||||
| Scale | Architecture Adjustments |
|
||||
|-------|--------------------------|
|
||||
| **Current (2-4 entities)** | Single state, simple collision checks, particle count < 100. No optimization needed. |
|
||||
| **Medium (10+ entities, complex arenas)** | Spatial partitioning for collision detection (divide canvas into grid cells, only check nearby entities). Consider renderer caching (re-use canvas for static backgrounds). |
|
||||
| **Large (many power-ups, obstacles, hazards)** | Separate rendering layer for UI (use DOM instead of canvas for menus/HUD). Consider offscreen canvas rendering for frequently-drawn elements. Profile particle system (may need pooling limits). |
|
||||
|
||||
**In practice:** For a Pong game, the "medium" complexity level is the realistic ceiling. Even with 50 particles and dozens of power-ups, a single-threaded JS game will run fine at 60fps.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Anti-Pattern 1: All Game Logic in the Render Function
|
||||
|
||||
**What people do:**
|
||||
```javascript
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// Move entities
|
||||
ball.x += ball.vx;
|
||||
ball.y += ball.vy;
|
||||
// Check collisions
|
||||
if (ball.x < 0) ball.vx = -ball.vx;
|
||||
// Update score
|
||||
if (ball.x < 0) score++;
|
||||
// Draw
|
||||
ctx.fillRect(ball.x, ball.y, 10, 10);
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's wrong:**
|
||||
- Mixing logic and rendering makes the frame rate dependent on rendering performance
|
||||
- Hard to test (can't verify logic without drawing)
|
||||
- Debugging is confusing (is the bug in movement or rendering?)
|
||||
- Impossible to pause (pause the render, logic stops too)
|
||||
|
||||
**Do this instead:** Separate `update()` and `render()` phases.
|
||||
|
||||
```javascript
|
||||
function gameLoop() {
|
||||
update(deltaTime); // All logic changes state
|
||||
render(); // Render displays state
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Pattern 2: No Input Debouncing / Polling Every Event
|
||||
|
||||
**What people do:**
|
||||
```javascript
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'w') paddle.y -= 10; // Move immediately on keydown
|
||||
if (e.key === 's') paddle.y += 10;
|
||||
});
|
||||
```
|
||||
|
||||
**Why it's wrong:**
|
||||
- Movement tied to event firing (non-deterministic)
|
||||
- Key repeat lag can cause jerky movement
|
||||
- Can't handle two keys pressed simultaneously
|
||||
- Frame rate dependency (holding key moves differently on 60fps vs 120fps)
|
||||
|
||||
**Do this instead:** Track key state in `update()`.
|
||||
|
||||
```javascript
|
||||
const keysPressed = {};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keysPressed[e.key] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keysPressed[e.key] = false;
|
||||
});
|
||||
|
||||
function update(deltaTime) {
|
||||
if (keysPressed['w']) paddle.y -= paddle.speed * deltaTime;
|
||||
if (keysPressed['s']) paddle.y += paddle.speed * deltaTime;
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Pattern 3: Collision Response Couples Ball and Paddle
|
||||
|
||||
**What people do:**
|
||||
```javascript
|
||||
class Ball {
|
||||
checkCollision(paddle) {
|
||||
if (this.overlaps(paddle)) {
|
||||
paddle.score++; // Ball knows about paddle state
|
||||
this.velocity.x = -this.velocity.x;
|
||||
this.y = this.y < paddle.y ? paddle.y - this.radius : paddle.y + paddle.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's wrong:**
|
||||
- Ball is tightly coupled to Paddle (hard to add obstacles, other bounceable objects)
|
||||
- Score logic is buried in physics code
|
||||
- Hard to test ball movement independently
|
||||
|
||||
**Do this instead:** Separate collision detection from response.
|
||||
|
||||
```javascript
|
||||
function handleCollision(ball, paddle) {
|
||||
ball.velocity.x = -ball.velocity.x;
|
||||
ball.x = ball.velocity.x > 0 ? paddle.x + paddle.width : paddle.x - ball.radius;
|
||||
|
||||
// Event-driven responses
|
||||
emitEvent('collision', { object: 'ball-paddle', paddle });
|
||||
}
|
||||
|
||||
// Listeners respond to event:
|
||||
on('collision', (event) => {
|
||||
if (event.object === 'ball-paddle') {
|
||||
audioManager.play('paddle-hit');
|
||||
particleSystem.burst(ball.x, ball.y);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Anti-Pattern 4: Drawing Every Frame Without Clearing
|
||||
|
||||
**What people do:**
|
||||
```javascript
|
||||
function draw() {
|
||||
// ctx.clearRect(...) is commented out to create "trails"
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(ball.x, ball.y, 10, 10);
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's wrong:**
|
||||
- Performance degrades over time (entire canvas accumulates garbage)
|
||||
- Can't pause or reset without flickering
|
||||
- Artifact buildup makes the scene look muddy
|
||||
|
||||
**Do this instead:** Clear intentionally, use particles for trails.
|
||||
|
||||
```javascript
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw arena
|
||||
ctx.fillStyle = '#222';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw entities
|
||||
entities.forEach(e => e.render(ctx));
|
||||
|
||||
// Particles create visual trails naturally
|
||||
particleSystem.render(ctx);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Internal Boundaries
|
||||
|
||||
| Boundary | Communication | Notes |
|
||||
|----------|---------------|-------|
|
||||
| **Collision System ↔ Event System** | Events (e.g., "ball-paddle-hit") | Decouples physics from reactions (audio, particles, score) |
|
||||
| **State ↔ State Machine** | State transitions, hooks | Each state is independent; only state machine knows how to switch |
|
||||
| **Input Manager ↔ Current State** | Polled each frame (keysPressed object) | State reads keys; input manager doesn't know about game logic |
|
||||
| **Entity Manager ↔ Collision System** | Entity list passed to collision check | Collision system reads entity data; doesn't modify entities directly |
|
||||
| **Audio Manager ↔ Collision/Scoring** | Direct play() calls from event handlers | Audio is triggered by events, not tightly coupled |
|
||||
| **Particle System ↔ Collision Events** | Direct burst() calls from event handlers | Particles are visual feedback, triggered by events |
|
||||
|
||||
### External Services (if any)
|
||||
|
||||
| Service | Integration Pattern | Notes |
|
||||
|---------|---------------------|-------|
|
||||
| **Sound Files** | Loaded via `new Audio(filename)` at init | Pre-load and cache; handle browser autoplay policy |
|
||||
| **Image Assets** | `new Image()` for sprites if used | Load before game state starts |
|
||||
|
||||
## Sources
|
||||
|
||||
- [The Complete Guide to Building HTML5 Games with Canvas and SVG - SitePoint](https://www.sitepoint.com/the-complete-guide-to-building-html5-games-with-canvas-and-svg/)
|
||||
- [Best JavaScript and HTML5 game engines (updated for 2025) - LogRocket Blog](https://blog.logrocket.com/best-javascript-html5-game-engines-2025/)
|
||||
- [Game Loop in HTML5 Canvas - Bryan Lew](https://blewjy.github.io/gamedev/html5/canvas/gameloop/javascript/2020/02/21/game-dev-html5-canvas-game-loop.html)
|
||||
- [Creating a State Stack Engine for your game with JavaScript - idiallo.com](https://idiallo.com/blog/javascript-game-state-stack-engine)
|
||||
- [Game State Management Patterns - Jake Gordon](https://jakesgordon.com/writing/javascript-game-foundations-state-management/)
|
||||
- [Create a Proper Game Loop - Spicy Yoghurt](https://spicyyoghurt.com/tutorials/html5-javascript-game-development/create-a-proper-game-loop-with-requestanimationframe)
|
||||
- [Collision Detection - MDN](https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript/Collision_detection)
|
||||
- [Collision Detection and Physics - Spicy Yoghurt](https://spicyyoghurt.com/tutorials/html5-javascript-game-development/collision-detection-physics)
|
||||
- [GitHub: html5-pong Vanilla JavaScript Implementation](https://github.com/SMenigat/html5-pong)
|
||||
- [Dev.to: Create Ping Pong Game Using JavaScript](https://dev.to/cwrcode/create-ping-pong-game-using-javascript-source-code-2d43)
|
||||
|
||||
---
|
||||
*Architecture research for: HTML5 Canvas arcade games (Pong-like)*
|
||||
*Researched: 2026-03-10*
|
||||
Reference in New Issue
Block a user