Lesson 4: Game Framework & Core Systems
Your game framework is the foundation that everything else builds on. A well-designed framework makes adding features easy, keeps your code organized, and ensures your game runs smoothly. In this lesson, you'll build a professional game framework with scene management, game states, and core systems that will support all your future features, including AI integration.
What You'll Learn
By the end of this lesson, you'll be able to:
- Design a scalable game framework architecture
- Implement scene management for different game screens
- Create game state system for managing game flow
- Set up core game systems (input, audio, rendering)
- Build a modular architecture that's easy to extend
- Organize code for maintainability and scalability
Why This Matters
A solid framework provides:
- Organization - Code is structured and easy to find
- Scalability - Easy to add new features without breaking existing ones
- Maintainability - Changes are isolated and predictable
- Performance - Efficient systems that don't waste resources
- Team Collaboration - Clear structure that multiple developers can work with
Without a good framework, you'll face:
- Code that's hard to understand and modify
- Features that break when you add new ones
- Performance issues as your game grows
- Difficulty collaborating with other developers
Prerequisites
Before starting this lesson, make sure you have:
- Completed Lesson 3: AI Integration Planning
- Development environment set up (from Lesson 2)
- Basic understanding of JavaScript and object-oriented programming
- Chosen your game framework (Phaser, PixiJS, or custom)
Understanding Game Framework Architecture
A game framework provides the structure and systems your game needs to run. Think of it as the skeleton that holds everything together.
Core Components
1. Scene Management
- Handles different screens (menu, gameplay, pause, game over)
- Manages transitions between scenes
- Controls what's active at any given time
2. Game State System
- Tracks current game state (playing, paused, menu, etc.)
- Manages state transitions
- Ensures proper cleanup when switching states
3. Core Systems
- Input handling (keyboard, mouse, touch)
- Audio management (sound effects, music)
- Rendering (drawing graphics)
- Update loop (game logic execution)
4. Module System
- Organizes code into logical modules
- Enables code reuse
- Makes testing easier
Step 1: Project Structure Setup
Let's start by creating a clean, organized project structure.
Directory Structure
web-game-with-ai/
├── index.html
├── css/
│ └── styles.css
├── js/
│ ├── main.js
│ ├── core/
│ │ ├── Game.js
│ │ ├── SceneManager.js
│ │ ├── StateManager.js
│ │ └── EventBus.js
│ ├── scenes/
│ │ ├── BaseScene.js
│ │ ├── MenuScene.js
│ │ ├── GameScene.js
│ │ └── PauseScene.js
│ ├── systems/
│ │ ├── InputSystem.js
│ │ ├── AudioSystem.js
│ │ └── RenderSystem.js
│ └── utils/
│ ├── Logger.js
│ └── Config.js
└── assets/
├── images/
├── audio/
└── data/
Why This Structure?
- Separation of concerns - Each module has a clear purpose
- Easy navigation - Find code quickly
- Scalable - Add new features without clutter
- Testable - Test modules independently
Creating the Base Structure
Create your project directories:
mkdir -p js/core js/scenes js/systems js/utils
mkdir -p assets/images assets/audio assets/data
Step 2: Core Game Class
The Game class is the main entry point that initializes everything.
Game.js
// js/core/Game.js
import { SceneManager } from './SceneManager.js';
import { StateManager } from './StateManager.js';
import { EventBus } from './EventBus.js';
import { InputSystem } from '../systems/InputSystem.js';
import { AudioSystem } from '../systems/AudioSystem.js';
import { Config } from '../utils/Config.js';
export class Game {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
// Set canvas size
this.canvas.width = Config.WIDTH;
this.canvas.height = Config.HEIGHT;
// Initialize core systems
this.sceneManager = new SceneManager(this);
this.stateManager = new StateManager(this);
this.eventBus = new EventBus();
this.input = new InputSystem(this.canvas);
this.audio = new AudioSystem();
// Game loop variables
this.lastTime = 0;
this.isRunning = false;
// Bind methods
this.update = this.update.bind(this);
this.render = this.render.bind(this);
}
start() {
this.isRunning = true;
this.sceneManager.loadScene('Menu');
this.gameLoop(performance.now());
}
stop() {
this.isRunning = false;
}
gameLoop(currentTime) {
if (!this.isRunning) return;
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
this.lastTime = currentTime;
// Update game systems
this.update(deltaTime);
// Render game
this.render();
// Continue loop
requestAnimationFrame((time) => this.gameLoop(time));
}
update(deltaTime) {
// Update input system
this.input.update();
// Update current scene
this.sceneManager.update(deltaTime);
}
render() {
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Render current scene
this.sceneManager.render(this.ctx);
}
resize(width, height) {
this.canvas.width = width;
this.canvas.height = height;
this.sceneManager.resize(width, height);
}
}
Key Features:
- Initialization - Sets up all core systems
- Game loop - Handles update and render cycles
- Delta time - Ensures consistent gameplay speed
- System management - Coordinates all game systems
Step 3: Scene Management System
Scene management handles different screens and transitions.
SceneManager.js
// js/core/SceneManager.js
import { BaseScene } from '../scenes/BaseScene.js';
export class SceneManager {
constructor(game) {
this.game = game;
this.scenes = new Map();
this.currentScene = null;
this.nextScene = null;
this.isTransitioning = false;
}
registerScene(name, sceneClass) {
if (!(sceneClass.prototype instanceof BaseScene)) {
throw new Error(`Scene ${name} must extend BaseScene`);
}
this.scenes.set(name, sceneClass);
}
loadScene(name, transitionData = {}) {
if (this.isTransitioning) return;
const SceneClass = this.scenes.get(name);
if (!SceneClass) {
console.error(`Scene ${name} not found`);
return;
}
this.nextScene = name;
this.startTransition(transitionData);
}
startTransition(transitionData) {
this.isTransitioning = true;
// Cleanup current scene
if (this.currentScene) {
this.currentScene.exit();
}
// Create new scene
const SceneClass = this.scenes.get(this.nextScene);
this.currentScene = new SceneClass(this.game, transitionData);
// Initialize new scene
this.currentScene.enter();
this.isTransitioning = false;
this.nextScene = null;
}
update(deltaTime) {
if (this.currentScene) {
this.currentScene.update(deltaTime);
}
}
render(ctx) {
if (this.currentScene) {
this.currentScene.render(ctx);
}
}
resize(width, height) {
if (this.currentScene) {
this.currentScene.resize(width, height);
}
}
getCurrentScene() {
return this.currentScene;
}
}
Key Features:
- Scene registration - Register scenes before use
- Scene loading - Switch between scenes smoothly
- Transitions - Handle scene transitions
- Lifecycle management - Proper enter/exit handling
Step 4: Base Scene Class
All scenes inherit from BaseScene for consistent behavior.
BaseScene.js
// js/scenes/BaseScene.js
export class BaseScene {
constructor(game, transitionData = {}) {
this.game = game;
this.transitionData = transitionData;
this.objects = [];
this.isActive = false;
}
enter() {
this.isActive = true;
this.onEnter();
}
exit() {
this.isActive = false;
this.onExit();
this.cleanup();
}
update(deltaTime) {
if (!this.isActive) return;
// Update all objects in scene
this.objects.forEach(obj => {
if (obj.update) {
obj.update(deltaTime);
}
});
this.onUpdate(deltaTime);
}
render(ctx) {
if (!this.isActive) return;
// Render all objects in scene
this.objects.forEach(obj => {
if (obj.render) {
obj.render(ctx);
}
});
this.onRender(ctx);
}
resize(width, height) {
this.onResize(width, height);
}
addObject(obj) {
this.objects.push(obj);
}
removeObject(obj) {
const index = this.objects.indexOf(obj);
if (index > -1) {
this.objects.splice(index, 1);
}
}
cleanup() {
this.objects = [];
}
// Override these methods in child classes
onEnter() {}
onExit() {}
onUpdate(deltaTime) {}
onRender(ctx) {}
onResize(width, height) {}
}
Key Features:
- Lifecycle methods - Enter, exit, update, render
- Object management - Add/remove game objects
- Override points - Customize behavior in child classes
- Cleanup - Proper resource management
Step 5: Game State Manager
Manages game states (playing, paused, menu, etc.).
StateManager.js
// js/core/StateManager.js
export const GameState = {
MENU: 'menu',
PLAYING: 'playing',
PAUSED: 'paused',
GAME_OVER: 'game_over',
SETTINGS: 'settings'
};
export class StateManager {
constructor(game) {
this.game = game;
this.currentState = GameState.MENU;
this.previousState = null;
this.stateHandlers = new Map();
}
registerState(state, enterHandler, exitHandler) {
this.stateHandlers.set(state, {
enter: enterHandler,
exit: exitHandler
});
}
setState(newState, data = {}) {
if (this.currentState === newState) return;
// Exit current state
const currentHandler = this.stateHandlers.get(this.currentState);
if (currentHandler && currentHandler.exit) {
currentHandler.exit(data);
}
// Update state
this.previousState = this.currentState;
this.currentState = newState;
// Enter new state
const newHandler = this.stateHandlers.get(newState);
if (newHandler && newHandler.enter) {
newHandler.enter(data);
}
// Emit state change event
this.game.eventBus.emit('stateChanged', {
from: this.previousState,
to: this.currentState,
data: data
});
}
getState() {
return this.currentState;
}
getPreviousState() {
return this.previousState;
}
isState(state) {
return this.currentState === state;
}
}
Key Features:
- State definitions - Clear state constants
- State transitions - Proper enter/exit handling
- Event emission - Notify other systems of state changes
- State queries - Check current state
Step 6: Event Bus System
Enables communication between systems without tight coupling.
EventBus.js
// js/core/EventBus.js
export class EventBus {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
off(event, callback) {
if (!this.listeners.has(event)) return;
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
emit(event, data = {}) {
if (!this.listeners.has(event)) return;
const callbacks = this.listeners.get(event);
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
once(event, callback) {
const wrapper = (data) => {
callback(data);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
clear() {
this.listeners.clear();
}
}
Key Features:
- Event subscription - Listen to events
- Event emission - Trigger events
- One-time listeners - Auto-remove after firing
- Error handling - Prevents crashes from bad handlers
Step 7: Input System
Handles all input (keyboard, mouse, touch).
InputSystem.js
// js/systems/InputSystem.js
export class InputSystem {
constructor(canvas) {
this.canvas = canvas;
this.keys = new Set();
this.mouse = {
x: 0,
y: 0,
buttons: new Set(),
wheel: 0
};
this.setupEventListeners();
}
setupEventListeners() {
// Keyboard events
window.addEventListener('keydown', (e) => {
this.keys.add(e.key.toLowerCase());
});
window.addEventListener('keyup', (e) => {
this.keys.delete(e.key.toLowerCase());
});
// Mouse events
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
});
this.canvas.addEventListener('mousedown', (e) => {
this.mouse.buttons.add(e.button);
});
this.canvas.addEventListener('mouseup', (e) => {
this.mouse.buttons.delete(e.button);
});
this.canvas.addEventListener('wheel', (e) => {
this.mouse.wheel = e.deltaY;
});
// Touch events
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = touch.clientX - rect.left;
this.mouse.y = touch.clientY - rect.top;
this.mouse.buttons.add(0); // Left mouse button
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
this.mouse.buttons.delete(0);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = touch.clientX - rect.left;
this.mouse.y = touch.clientY - rect.top;
});
}
update() {
// Reset wheel delta
this.mouse.wheel = 0;
}
isKeyPressed(key) {
return this.keys.has(key.toLowerCase());
}
isMouseButtonPressed(button) {
return this.mouse.buttons.has(button);
}
getMousePosition() {
return { x: this.mouse.x, y: this.mouse.y };
}
getMouseWheel() {
return this.mouse.wheel;
}
}
Key Features:
- Keyboard input - Track key presses
- Mouse input - Position and button tracking
- Touch support - Mobile-friendly input
- Wheel support - Scroll wheel detection
Step 8: Audio System
Manages sound effects and music.
AudioSystem.js
// js/systems/AudioSystem.js
export class AudioSystem {
constructor() {
this.sounds = new Map();
this.music = null;
this.musicVolume = 0.5;
this.soundVolume = 0.7;
this.masterVolume = 1.0;
this.isMuted = false;
}
loadSound(name, url) {
return new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.preload = 'auto';
audio.addEventListener('canplaythrough', () => {
this.sounds.set(name, audio);
resolve(audio);
});
audio.addEventListener('error', reject);
});
}
playSound(name, volume = 1.0) {
if (this.isMuted) return;
const sound = this.sounds.get(name);
if (!sound) {
console.warn(`Sound ${name} not found`);
return;
}
const audio = sound.cloneNode();
audio.volume = volume * this.soundVolume * this.masterVolume;
audio.play().catch(error => {
console.error(`Error playing sound ${name}:`, error);
});
}
playMusic(name, loop = true) {
if (this.music) {
this.music.pause();
}
const music = this.sounds.get(name);
if (!music) {
console.warn(`Music ${name} not found`);
return;
}
this.music = music.cloneNode();
this.music.loop = loop;
this.music.volume = this.musicVolume * this.masterVolume;
this.music.play().catch(error => {
console.error(`Error playing music ${name}:`, error);
});
}
stopMusic() {
if (this.music) {
this.music.pause();
this.music.currentTime = 0;
this.music = null;
}
}
setMasterVolume(volume) {
this.masterVolume = Math.max(0, Math.min(1, volume));
}
setSoundVolume(volume) {
this.soundVolume = Math.max(0, Math.min(1, volume));
}
setMusicVolume(volume) {
this.musicVolume = Math.max(0, Math.min(1, volume));
if (this.music) {
this.music.volume = this.musicVolume * this.masterVolume;
}
}
mute() {
this.isMuted = true;
if (this.music) {
this.music.pause();
}
}
unmute() {
this.isMuted = false;
if (this.music) {
this.music.play();
}
}
}
Key Features:
- Sound loading - Load audio files
- Sound playback - Play sound effects
- Music management - Background music with looping
- Volume control - Master, sound, and music volumes
- Mute support - Toggle audio on/off
Step 9: Example Scene Implementation
Let's create a simple menu scene to demonstrate usage.
MenuScene.js
// js/scenes/MenuScene.js
import { BaseScene } from './BaseScene.js';
export class MenuScene extends BaseScene {
constructor(game, transitionData) {
super(game, transitionData);
this.titleY = 100;
this.buttonY = 300;
this.selectedButton = 0;
this.buttons = ['Start Game', 'Settings', 'Quit'];
}
onEnter() {
// Play menu music
this.game.audio.playMusic('menu_theme');
// Setup input handlers
this.game.eventBus.on('keydown', this.handleKeyDown.bind(this));
}
onExit() {
// Cleanup
this.game.eventBus.off('keydown', this.handleKeyDown.bind(this));
}
handleKeyDown(data) {
const key = data.key;
if (key === 'arrowdown' || key === 's') {
this.selectedButton = (this.selectedButton + 1) % this.buttons.length;
} else if (key === 'arrowup' || key === 'w') {
this.selectedButton = (this.selectedButton - 1 + this.buttons.length) % this.buttons.length;
} else if (key === 'enter' || key === ' ') {
this.selectButton();
}
}
selectButton() {
const button = this.buttons[this.selectedButton];
switch(button) {
case 'Start Game':
this.game.sceneManager.loadScene('Game');
break;
case 'Settings':
this.game.sceneManager.loadScene('Settings');
break;
case 'Quit':
// Handle quit
break;
}
}
onUpdate(deltaTime) {
// Animate title
this.titleY += Math.sin(Date.now() / 500) * 0.5;
}
onRender(ctx) {
// Draw title
ctx.fillStyle = '#ffffff';
ctx.font = '48px Arial';
ctx.textAlign = 'center';
ctx.fillText('My Web Game', this.canvas.width / 2, this.titleY);
// Draw buttons
this.buttons.forEach((button, index) => {
ctx.fillStyle = index === this.selectedButton ? '#ffff00' : '#ffffff';
ctx.font = '24px Arial';
ctx.fillText(button, this.canvas.width / 2, this.buttonY + index * 50);
});
}
}
Step 10: Configuration File
Centralize game configuration.
Config.js
// js/utils/Config.js
export const Config = {
// Canvas settings
WIDTH: 1280,
HEIGHT: 720,
// Game settings
FPS: 60,
TARGET_FPS: 60,
// Debug settings
DEBUG: true,
SHOW_FPS: true,
SHOW_BOUNDS: false,
// Audio settings
DEFAULT_MUSIC_VOLUME: 0.5,
DEFAULT_SOUND_VOLUME: 0.7,
// Scene settings
FADE_DURATION: 0.5,
// API settings (for future AI integration)
AI_API_URL: 'https://api.example.com',
AI_API_KEY: '', // Set in environment
// Version
VERSION: '1.0.0'
};
Step 11: Main Entry Point
Initialize and start the game.
main.js
// js/main.js
import { Game } from './core/Game.js';
import { MenuScene } from './scenes/MenuScene.js';
import { GameScene } from './scenes/GameScene.js';
import { Config } from './utils/Config.js';
// Initialize game
const game = new Game('gameCanvas');
// Register scenes
game.sceneManager.registerScene('Menu', MenuScene);
game.sceneManager.registerScene('Game', GameScene);
// Setup state handlers
game.stateManager.registerState('menu', () => {
game.sceneManager.loadScene('Menu');
});
game.stateManager.registerState('playing', () => {
game.sceneManager.loadScene('Game');
});
// Handle window resize
window.addEventListener('resize', () => {
const width = Math.min(window.innerWidth, Config.WIDTH);
const height = Math.min(window.innerHeight, Config.HEIGHT);
game.resize(width, height);
});
// Start game
game.start();
Pro Tips
1. Keep Systems Independent
- Systems should communicate through events, not direct references
- Makes testing and debugging easier
2. Use Configuration Files
- Centralize settings for easy tweaking
- Makes it easy to create different builds
3. Implement Proper Cleanup
- Always clean up resources when scenes exit
- Prevents memory leaks
4. Use TypeScript for Large Projects
- Type safety catches errors early
- Better IDE support
5. Profile Performance
- Monitor frame rate and memory usage
- Optimize bottlenecks early
Common Mistakes to Avoid
1. Tight Coupling
- Don't make systems directly depend on each other
- Use events for communication
2. Ignoring Cleanup
- Always clean up event listeners and resources
- Prevents memory leaks
3. Hardcoding Values
- Use configuration files instead
- Makes tweaking easier
4. Skipping Error Handling
- Always handle potential errors
- Prevents crashes
5. Over-Engineering
- Start simple, add complexity as needed
- Don't build systems you don't need yet
Troubleshooting
Problem: Game runs too fast or too slow
Solution: Check your delta time calculation. Make sure you're converting milliseconds to seconds correctly.
Problem: Scenes don't transition properly
Solution: Ensure you're calling enter() and exit() methods. Check that scene cleanup is working.
Problem: Input doesn't work
Solution: Verify event listeners are set up correctly. Check that canvas has focus.
Problem: Audio doesn't play
Solution: Browsers require user interaction before playing audio. Trigger audio from a user action first.
Mini-Task: Create Basic Game Structure
Your Task:
- Set up the project structure as shown
- Implement the core Game class
- Create a simple MenuScene
- Create a simple GameScene with a moving object
- Test scene transitions
Success Criteria:
- Game starts and shows menu
- Can navigate menu with keyboard
- Can transition to game scene
- Game scene shows a moving object
- Can return to menu
Share Your Results: Post your game structure in the community and get feedback on your architecture!
What's Next?
In the next lesson, you'll integrate AI APIs into your game framework. You'll learn how to:
- Connect to AI APIs
- Handle API responses
- Integrate AI features into your scenes
- Manage API costs and rate limits
Next Lesson: Lesson 5: AI Integration & Smart Features
Summary
You've built a professional game framework with:
- Core Game class - Main entry point and game loop
- Scene Management - Handle different game screens
- State Management - Track game states
- Event System - Decoupled communication
- Input System - Keyboard, mouse, and touch support
- Audio System - Sound effects and music
- Modular Architecture - Easy to extend and maintain
This foundation will support all your future features, including AI integration, multiplayer, and advanced gameplay systems.
Key Takeaways:
- A good framework makes everything easier
- Modular design enables scalability
- Proper cleanup prevents issues
- Events enable loose coupling
- Configuration centralizes settings
Found this lesson helpful? Bookmark it for reference and share your game framework with the community!
Last updated: February 20, 2026