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:

  1. Set up the project structure as shown
  2. Implement the core Game class
  3. Create a simple MenuScene
  4. Create a simple GameScene with a moving object
  5. 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