Files
gijs_pong/.planning/research/ARCHITECTURE.md
Dabit 28f86d781c 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>
2026-03-10 14:18:11 +01:00

29 KiB

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)

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:

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:

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:

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:

// 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:

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:

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]

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:

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.

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:

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().

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:

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.

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:

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.

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


Architecture research for: HTML5 Canvas arcade games (Pong-like) Researched: 2026-03-10