12 KiB
Web Game Engine Architecture Guide
This guide provides web game engine-specific guidance (Phaser, PixiJS, Three.js, Babylon.js) for solution architecture generation.
Web Game-Specific Questions
1. Engine and Technology Selection
Ask:
- Which engine? (Phaser 3, PixiJS, Three.js, Babylon.js, custom Canvas/WebGL)
- TypeScript or JavaScript?
- Build tool? (Vite, Webpack, Rollup, Parcel)
- Target platform(s)? (Desktop web, mobile web, PWA, Cordova/Capacitor wrapper)
Guidance:
- Phaser 3: Full-featured 2D game framework, great for beginners
- PixiJS: 2D rendering library, more low-level than Phaser
- Three.js: 3D graphics library, mature ecosystem
- Babylon.js: Complete 3D game engine, WebXR support
- TypeScript: Recommended for all web games (type safety, better tooling)
- Vite: Modern, fast, HMR - best for development
Record ADR: Engine selection and build tooling
2. Architecture Pattern
Ask:
- Scene-based architecture? (Phaser scenes, custom scene manager)
- ECS (Entity Component System)? (Libraries: bitECS, ecsy)
- State management? (Redux, Zustand, custom FSM)
Guidance:
- Scene-based: Natural for Phaser, good for level-based games
- ECS: Better performance for large entity counts (100s+)
- FSM: Good for simple state transitions (menu → game → gameover)
Phaser Pattern:
// MainMenuScene.ts
export class MainMenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MainMenu' });
}
create() {
this.add.text(400, 300, 'Main Menu', { fontSize: '32px' });
const startButton = this.add
.text(400, 400, 'Start Game', { fontSize: '24px' })
.setInteractive()
.on('pointerdown', () => {
this.scene.start('GameScene');
});
}
}
Record ADR: Architecture pattern and scene management
3. Asset Management
Ask:
- Asset loading strategy? (Preload all, lazy load, progressive)
- Texture atlas usage? (TexturePacker, built-in tools)
- Audio format strategy? (MP3, OGG, WebM)
Guidance:
- Preload: Load all assets at start (simple, small games)
- Lazy load: Load per-level (better for larger games)
- Texture atlases: Essential for performance (reduce draw calls)
- Audio: MP3 for compatibility, OGG for smaller size, use both
Phaser loading:
class PreloadScene extends Phaser.Scene {
preload() {
// Show progress bar
this.load.on('progress', (value: number) => {
console.log('Loading: ' + Math.round(value * 100) + '%');
});
// Load assets
this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');
this.load.audio('music', ['assets/music.mp3', 'assets/music.ogg']);
this.load.audio('jump', ['assets/sfx/jump.mp3', 'assets/sfx/jump.ogg']);
}
create() {
this.scene.start('MainMenu');
}
}
Record ADR: Asset loading and management strategy
Web Game-Specific Architecture Sections
Performance Optimization
Web-specific considerations:
- Object Pooling: Mandatory for bullets, particles, enemies (avoid GC pauses)
- Sprite Batching: Use texture atlases, minimize state changes
- Canvas vs WebGL: WebGL for better performance (most games)
- Draw Call Reduction: Batch similar sprites, use sprite sheets
- Memory Management: Watch heap size, profile with Chrome DevTools
Object Pooling Pattern:
class BulletPool {
private pool: Bullet[] = [];
private scene: Phaser.Scene;
constructor(scene: Phaser.Scene, size: number) {
this.scene = scene;
for (let i = 0; i < size; i++) {
const bullet = new Bullet(scene);
bullet.setActive(false).setVisible(false);
this.pool.push(bullet);
}
}
spawn(x: number, y: number, velocityX: number, velocityY: number): Bullet | null {
const bullet = this.pool.find((b) => !b.active);
if (bullet) {
bullet.spawn(x, y, velocityX, velocityY);
}
return bullet || null;
}
}
Target Performance:
- Desktop: 60 FPS minimum
- Mobile: 60 FPS (high-end), 30 FPS (low-end)
- Profile with: Chrome DevTools Performance tab, Phaser Debug plugin
Input Handling
Multi-input support:
class GameScene extends Phaser.Scene {
private cursors?: Phaser.Types.Input.Keyboard.CursorKeys;
private wasd?: { [key: string]: Phaser.Input.Keyboard.Key };
create() {
// Keyboard
this.cursors = this.input.keyboard?.createCursorKeys();
this.wasd = this.input.keyboard?.addKeys('W,S,A,D') as any;
// Mouse/Touch
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
this.handleClick(pointer.x, pointer.y);
});
// Gamepad (optional)
this.input.gamepad?.on('down', (pad, button, index) => {
this.handleGamepadButton(button);
});
}
update() {
// Handle keyboard input
if (this.cursors?.left.isDown || this.wasd?.A.isDown) {
this.player.moveLeft();
}
}
}
State Persistence
LocalStorage pattern:
interface GameSaveData {
level: number;
score: number;
playerStats: {
health: number;
lives: number;
};
}
class SaveManager {
private static SAVE_KEY = 'game_save_data';
static save(data: GameSaveData): void {
localStorage.setItem(this.SAVE_KEY, JSON.stringify(data));
}
static load(): GameSaveData | null {
const data = localStorage.getItem(this.SAVE_KEY);
return data ? JSON.parse(data) : null;
}
static clear(): void {
localStorage.removeItem(this.SAVE_KEY);
}
}
Source Tree Structure
Phaser + TypeScript + Vite:
project/
├── public/ # Static assets
│ ├── assets/
│ │ ├── sprites/
│ │ ├── audio/
│ │ │ ├── music/
│ │ │ └── sfx/
│ │ └── fonts/
│ └── index.html
├── src/
│ ├── main.ts # Game initialization
│ ├── config.ts # Phaser config
│ ├── scenes/ # Game scenes
│ │ ├── PreloadScene.ts
│ │ ├── MainMenuScene.ts
│ │ ├── GameScene.ts
│ │ └── GameOverScene.ts
│ ├── entities/ # Game objects
│ │ ├── Player.ts
│ │ ├── Enemy.ts
│ │ └── Bullet.ts
│ ├── systems/ # Game systems
│ │ ├── InputManager.ts
│ │ ├── AudioManager.ts
│ │ └── SaveManager.ts
│ ├── utils/ # Utilities
│ │ ├── ObjectPool.ts
│ │ └── Constants.ts
│ └── types/ # TypeScript types
│ └── index.d.ts
├── tests/ # Unit tests
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
Testing Strategy
Jest + TypeScript:
// Player.test.ts
import { Player } from '../entities/Player';
describe('Player', () => {
let player: Player;
beforeEach(() => {
// Mock Phaser scene
const mockScene = {
add: { sprite: jest.fn() },
physics: { add: { sprite: jest.fn() } },
} as any;
player = new Player(mockScene, 0, 0);
});
test('takes damage correctly', () => {
player.health = 100;
player.takeDamage(20);
expect(player.health).toBe(80);
});
test('dies when health reaches zero', () => {
player.health = 10;
player.takeDamage(20);
expect(player.alive).toBe(false);
});
});
E2E Testing:
- Playwright for browser automation
- Cypress for interactive testing
- Test game states, not individual frames
Deployment and Build
Build for production:
// package.json scripts
{
"scripts": {
"dev": "vite",
"build": "tsc andand vite build",
"preview": "vite preview",
"test": "jest"
}
}
Deployment targets:
- Static hosting: Netlify, Vercel, GitHub Pages, AWS S3
- CDN: Cloudflare, Fastly for global distribution
- PWA: Service worker for offline play
- Mobile wrapper: Cordova or Capacitor for app stores
Optimization:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
phaser: ['phaser'], // Separate Phaser bundle
},
},
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in prod
},
},
},
});
Specialist Recommendations
Audio Designer
When needed: Games with music, sound effects, ambience Responsibilities:
- Web Audio API architecture
- Audio sprite creation (combine sounds into one file)
- Music loop management
- Sound effect implementation
- Audio performance on web (decode strategy)
Performance Optimizer
When needed: Mobile web games, complex games Responsibilities:
- Chrome DevTools profiling
- Object pooling implementation
- Draw call optimization
- Memory management
- Bundle size optimization
- Network performance (asset loading)
Monetization Specialist
When needed: F2P web games Responsibilities:
- Ad network integration (Google AdSense, AdMob for web)
- In-game purchases (Stripe, PayPal)
- Analytics (Google Analytics, custom events)
- A/B testing frameworks
- Economy design
Platform Specialist
When needed: Mobile wrapper apps (Cordova/Capacitor) Responsibilities:
- Native plugin integration
- Platform-specific performance tuning
- App store submission
- Device compatibility testing
- Push notification setup
Common Pitfalls
- Not using object pooling - Frequent instantiation causes GC pauses
- Too many draw calls - Use texture atlases and sprite batching
- Loading all assets at once - Causes long initial load times
- Not testing on mobile - Performance vastly different on phones
- Ignoring bundle size - Large bundles = slow load times
- Not handling window resize - Web games run in resizable windows
- Forgetting audio autoplay restrictions - Browsers block auto-play without user interaction
Engine-Specific Patterns
Phaser 3
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO, // WebGL with Canvas fallback
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: { gravity: { y: 300 }, debug: false },
},
scene: [PreloadScene, MainMenuScene, GameScene, GameOverScene],
};
const game = new Phaser.Game(config);
PixiJS
const app = new PIXI.Application({
width: 800,
height: 600,
backgroundColor: 0x1099bb,
});
document.body.appendChild(app.view);
const sprite = PIXI.Sprite.from('assets/player.png');
app.stage.addChild(sprite);
app.ticker.add((delta) => {
sprite.rotation += 0.01 * delta;
});
Three.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
renderer.render(scene, camera);
}
animate();
Key Architecture Decision Records
ADR Template for Web Games
ADR-XXX: [Title]
Context: What web game-specific issue are we solving?
Options:
- Phaser 3 (full framework)
- PixiJS (rendering library)
- Three.js/Babylon.js (3D)
- Custom Canvas/WebGL
Decision: We chose [Option X]
Web-specific Rationale:
- Engine features vs bundle size
- Community and plugin ecosystem
- TypeScript support
- Performance on target devices (mobile web)
- Browser compatibility
- Development velocity
Consequences:
- Impact on bundle size (Phaser ~1.2MB gzipped)
- Learning curve
- Platform limitations
- Plugin availability
This guide is specific to web game engines. For native engines, see:
- game-engine-unity-guide.md
- game-engine-godot-guide.md
- game-engine-unreal-guide.md