DEV Community

Cover image for TCJSGame Advanced Architecture: Building Scalable, Maintainable Games
Kehinde Owolabi
Kehinde Owolabi

Posted on

TCJSGame Advanced Architecture: Building Scalable, Maintainable Games

TCJSGame Advanced Architecture: Building Scalable, Maintainable Games

Game Architecture

You've mastered TCJSGame's core features and optimized for performance—now it's time to level up your architecture. Building games that scale beyond simple prototypes requires thoughtful design patterns and organization. In this guide, we'll explore professional game architecture that will transform your TCJSGame projects from spaghetti code to scalable, maintainable masterpieces.

Why Architecture Matters in Game Development

Great architecture isn't about over-engineering—it's about:

  • Maintainability: Being able to fix bugs and add features months later
  • Scalability: Supporting your game as it grows from 100 to 10,000 lines of code
  • Team Collaboration: Making your code understandable to other developers
  • Reusability: Building systems you can use across multiple projects
  • Debugging: Finding and fixing issues quickly and efficiently

🏗️ The Foundation: Project Structure

Let's start with a professional project structure:

my-tcjsgame-project/
│
├── index.html
├── css/
│   └── style.css
├── js/
│   ├── engine/
│   │   ├── core/
│   │   │   ├── Game.js
│   │   │   ├── SceneManager.js
│   │   │   └── StateMachine.js
│   │   ├── systems/
│   │   │   ├── InputSystem.js
│   │   │   ├── AudioSystem.js
│   │   │   └── PhysicsSystem.js
│   │   └── utils/
│   │       ├── helpers.js
│   │       └── constants.js
│   ├── scenes/
│   │   ├── BootScene.js
│   │   ├── MenuScene.js
│   │   ├── GameScene.js
│   │   └── GameOverScene.js
│   ├── entities/
│   │   ├── Player.js
│   │   ├── Enemy.js
│   │   └── Projectile.js
│   └── main.js
└── assets/
    ├── images/
    ├── sounds/
    └── data/
Enter fullscreen mode Exit fullscreen mode

🎮 Core Architecture Patterns

1. Game Manager Pattern

Create a central game controller that orchestrates everything:

class Game {
    constructor() {
        this.display = null;
        this.sceneManager = null;
        this.systems = new Map();
        this.isRunning = false;
        this.config = {
            width: 800,
            height: 600,
            backgroundColor: '#1a1a1a'
        };
    }

    init() {
        // Initialize TCJSGame display
        this.display = new Display();
        this.display.start(this.config.width, this.config.height);
        this.display.backgroundColor(this.config.backgroundColor);

        // Initialize systems
        this.initSystems();

        // Initialize scene manager
        this.sceneManager = new SceneManager(this);

        // Set up global error handling
        this.setupErrorHandling();

        console.log('🎮 Game initialized successfully');
    }

    initSystems() {
        // Register all game systems
        this.systems.set('input', new InputSystem(this));
        this.systems.set('audio', new AudioSystem(this));
        this.systems.set('physics', new PhysicsSystem(this));
        this.systems.set('particles', new ParticleSystem(this));

        // Initialize all systems
        this.systems.forEach(system => system.init());
    }

    getSystem(name) {
        return this.systems.get(name);
    }

    start() {
        this.isRunning = true;
        this.sceneManager.change('boot');
        console.log('🚀 Game started');
    }

    update() {
        if (!this.isRunning) return;

        // Update all systems
        this.systems.forEach(system => {
            if (system.update) system.update();
        });

        // Update current scene
        this.sceneManager.update();
    }

    shutdown() {
        this.isRunning = false;
        this.systems.forEach(system => {
            if (system.shutdown) system.shutdown();
        });
        console.log('🛑 Game shutdown');
    }

    setupErrorHandling() {
        window.addEventListener('error', (event) => {
            console.error('Game error:', event.error);
            this.handleError(event.error);
        });

        window.addEventListener('unhandledrejection', (event) => {
            console.error('Unhandled promise rejection:', event.reason);
            this.handleError(event.reason);
        });
    }

    handleError(error) {
        // Graceful error handling - don't crash the game
        console.error('Handled game error:', error);
        // Could show error UI, log to analytics, etc.
    }
}

// Global game instance
const game = new Game();
Enter fullscreen mode Exit fullscreen mode

2. Scene Management System

Manage different game states (menu, gameplay, game over) cleanly:

class SceneManager {
    constructor(game) {
        this.game = game;
        this.scenes = new Map();
        this.currentScene = null;
        this.previousScene = null;
    }

    register(name, sceneClass) {
        this.scenes.set(name, sceneClass);
    }

    change(name, data = {}) {
        if (!this.scenes.has(name)) {
            console.error(`Scene not found: ${name}`);
            return;
        }

        // Clean up current scene
        if (this.currentScene) {
            this.currentScene.exit();
            this.previousScene = this.currentScene.name;
        }

        // Create and initialize new scene
        const SceneClass = this.scenes.get(name);
        this.currentScene = new SceneClass(this.game, name);
        this.currentScene.enter(data);

        console.log(`🔄 Scene changed to: ${name}`);
    }

    update() {
        if (this.currentScene && this.currentScene.update) {
            this.currentScene.update();
        }
    }

    goBack() {
        if (this.previousScene) {
            this.change(this.previousScene);
        }
    }
}

class Scene {
    constructor(game, name) {
        this.game = game;
        this.name = name;
        this.display = game.display;
        this.isActive = false;
        this.components = [];
    }

    enter(data = {}) {
        this.isActive = true;
        this.onEnter(data);
    }

    exit() {
        this.isActive = false;
        this.cleanup();
        this.onExit();
    }

    onEnter(data) {
        // Override in child classes
    }

    onExit() {
        // Override in child classes
    }

    add(component, scene = 0) {
        this.components.push(component);
        this.display.add(component, scene);
    }

    cleanup() {
        // Remove all scene components
        this.components.forEach(component => {
            this.display.add(component, 1); // Move to inactive scene
        });
        this.components = [];
    }

    update() {
        // Override in child classes
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Entity Component System (ECS) Pattern

Build flexible, reusable game entities:

class Entity {
    constructor(name = 'Entity') {
        this.id = Entity.nextId++;
        this.name = name;
        this.components = new Map();
        this.tags = new Set();
        this.isActive = true;
    }

    addComponent(component) {
        this.components.set(component.constructor.name, component);
        component.entity = this;
        if (component.init) component.init();
        return this;
    }

    getComponent(componentClass) {
        return this.components.get(componentClass.name);
    }

    hasComponent(componentClass) {
        return this.components.has(componentClass.name);
    }

    removeComponent(componentClass) {
        const component = this.components.get(componentClass.name);
        if (component && component.destroy) {
            component.destroy();
        }
        this.components.delete(componentClass.name);
    }

    addTag(tag) {
        this.tags.add(tag);
    }

    hasTag(tag) {
        return this.tags.has(tag);
    }

    destroy() {
        this.components.forEach(component => {
            if (component.destroy) component.destroy();
        });
        this.components.clear();
        this.isActive = false;
    }

    update() {
        this.components.forEach(component => {
            if (component.update && this.isActive) {
                component.update();
            }
        });
    }
}

Entity.nextId = 1;

// Base Component class
class Component {
    constructor() {
        this.entity = null;
    }

    init() {
        // Override in derived classes
    }

    update() {
        // Override in derived classes
    }

    destroy() {
        // Override in derived classes
    }
}

// Example components
class TransformComponent extends Component {
    constructor(x = 0, y = 0) {
        super();
        this.x = x;
        this.y = y;
        this.rotation = 0;
        this.scale = 1;
    }
}

class PhysicsComponent extends Component {
    constructor() {
        super();
        this.velocityX = 0;
        this.velocityY = 0;
        this.gravity = 0;
        this.friction = 0.98;
        this.isGrounded = false;
    }

    update() {
        // Apply physics
        this.entity.transform.x += this.velocityX;
        this.entity.transform.y += this.velocityY;

        // Apply gravity
        if (!this.isGrounded) {
            this.velocityY += this.gravity;
        }

        // Apply friction
        this.velocityX *= this.friction;
        this.velocityY *= this.friction;
    }
}

class RenderComponent extends Component {
    constructor(width, height, color, type = 'rect') {
        super();
        this.tcjsComponent = new Component(width, height, color, 0, 0, type);
        this.width = width;
        this.height = height;
        this.color = color;
        this.type = type;
    }

    init() {
        // Sync with entity position
        this.updateRenderPosition();
    }

    update() {
        this.updateRenderPosition();
    }

    updateRenderPosition() {
        const transform = this.entity.getComponent(TransformComponent);
        if (transform) {
            this.tcjsComponent.x = transform.x;
            this.tcjsComponent.y = transform.y;
            this.tcjsComponent.angle = transform.rotation;
        }
    }

    destroy() {
        // Clean up TCJSGame component
        if (this.tcjsComponent && this.tcjsComponent.destroy) {
            this.tcjsComponent.destroy();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🔧 System Architecture Patterns

1. Input System with Command Pattern

class InputSystem {
    constructor(game) {
        this.game = game;
        this.keys = new Set();
        this.mouse = { x: 0, y: 0, down: false };
        this.commands = new Map();
        this.setupInputHandling();
    }

    init() {
        this.bindCommands();
    }

    setupInputHandling() {
        // Keyboard events
        window.addEventListener('keydown', (e) => {
            this.keys.add(e.code);
            this.executeCommand(e.code, 'keydown');
        });

        window.addEventListener('keyup', (e) => {
            this.keys.delete(e.code);
            this.executeCommand(e.code, 'keyup');
        });

        // Mouse events
        window.addEventListener('mousemove', (e) => {
            this.mouse.x = e.clientX;
            this.mouse.y = e.clientY;
        });

        window.addEventListener('mousedown', (e) => {
            this.mouse.down = true;
            this.executeCommand('mouse', 'mousedown');
        });

        window.addEventListener('mouseup', (e) => {
            this.mouse.down = false;
            this.executeCommand('mouse', 'mouseup');
        });
    }

    bindCommands() {
        // Bind keys to game commands
        this.commands.set('KeyW', new MoveCommand('up'));
        this.commands.set('KeyS', new MoveCommand('down'));
        this.commands.set('KeyA', new MoveCommand('left'));
        this.commands.set('KeyD', new MoveCommand('right'));
        this.commands.set('Space', new JumpCommand());
        this.commands.set('KeyE', new InteractCommand());
    }

    executeCommand(input, type) {
        const command = this.commands.get(input);
        if (command) {
            command.execute(type, this.game);
        }
    }

    isKeyPressed(key) {
        return this.keys.has(key);
    }

    update() {
        // Handle continuous input (like holding down movement keys)
        this.keys.forEach(key => {
            this.executeCommand(key, 'hold');
        });
    }
}

// Command pattern for input handling
class Command {
    execute(type, game) {
        // Override in derived classes
    }
}

class MoveCommand extends Command {
    constructor(direction) {
        super();
        this.direction = direction;
    }

    execute(type, game) {
        if (type === 'hold' || type === 'keydown') {
            const scene = game.sceneManager.currentScene;
            if (scene && scene.player) {
                scene.player.handleMove(this.direction, type === 'keydown');
            }
        }
    }
}

class JumpCommand extends Command {
    execute(type, game) {
        if (type === 'keydown') {
            const scene = game.sceneManager.currentScene;
            if (scene && scene.player) {
                scene.player.jump();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Event Bus System

Decouple your systems with a centralized event system:

class EventBus {
    constructor() {
        this.listeners = new Map();
        this.queue = [];
        this.isProcessing = false;
    }

    on(event, callback, context = null) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push({ callback, context });
    }

    off(event, callback) {
        if (!this.listeners.has(event)) return;

        const listeners = this.listeners.get(event);
        const index = listeners.findIndex(l => l.callback === callback);
        if (index !== -1) {
            listeners.splice(index, 1);
        }
    }

    emit(event, data = null) {
        this.queue.push({ event, data });

        if (!this.isProcessing) {
            this.processQueue();
        }
    }

    processQueue() {
        this.isProcessing = true;

        while (this.queue.length > 0) {
            const { event, data } = this.queue.shift();

            if (this.listeners.has(event)) {
                // Clone array to prevent issues if listeners are removed during processing
                const listeners = [...this.listeners.get(event)];
                listeners.forEach(({ callback, context }) => {
                    try {
                        callback.call(context, data);
                    } catch (error) {
                        console.error(`Error in event listener for ${event}:`, error);
                    }
                });
            }
        }

        this.isProcessing = false;
    }

    clear() {
        this.listeners.clear();
        this.queue = [];
    }
}

// Global event bus
const eventBus = new EventBus();

// Example usage
class AchievementSystem {
    constructor() {
        eventBus.on('player:coin-collected', this.onCoinCollected, this);
        eventBus.on('player:enemy-defeated', this.onEnemyDefeated, this);
        eventBus.on('player:level-completed', this.onLevelCompleted, this);
    }

    onCoinCollected(data) {
        if (data.totalCoins >= 100) {
            this.unlockAchievement('coin_collector');
        }
    }

    onEnemyDefeated(data) {
        // Track enemy defeats for achievements
    }

    onLevelCompleted(data) {
        // Handle level completion achievements
    }

    unlockAchievement(name) {
        console.log(`🏆 Achievement unlocked: ${name}`);
        eventBus.emit('achievement:unlocked', { name });
    }
}
Enter fullscreen mode Exit fullscreen mode

3. State Machine Pattern

Manage complex entity states cleanly:

class StateMachine {
    constructor(entity) {
        this.entity = entity;
        this.states = new Map();
        this.currentState = null;
        this.previousState = null;
    }

    addState(name, state) {
        this.states.set(name, state);
        state.machine = this;
    }

    changeTo(stateName, data = {}) {
        if (!this.states.has(stateName)) {
            console.error(`State not found: ${stateName}`);
            return;
        }

        const newState = this.states.get(stateName);

        // Exit current state
        if (this.currentState && this.currentState.exit) {
            this.currentState.exit();
        }

        // Update state references
        this.previousState = this.currentState;
        this.currentState = newState;

        // Enter new state
        if (this.currentState.enter) {
            this.currentState.enter(data);
        }

        console.log(`🔄 ${this.entity.name} state: ${stateName}`);
    }

    update() {
        if (this.currentState && this.currentState.update) {
            this.currentState.update();
        }
    }

    revertToPrevious() {
        if (this.previousState) {
            this.changeTo(this.previousState.constructor.name);
        }
    }
}

class State {
    constructor(name) {
        this.name = name;
        this.machine = null;
    }

    enter(data) {
        // Override in derived classes
    }

    exit() {
        // Override in derived classes
    }

    update() {
        // Override in derived classes
    }
}

// Example states for a player
class PlayerIdleState extends State {
    enter() {
        this.machine.entity.animation.play('idle');
    }

    update() {
        const input = this.machine.entity.game.getSystem('input');

        if (input.isKeyPressed('KeyA') || input.isKeyPressed('KeyD')) {
            this.machine.changeTo('walking');
        }

        if (input.isKeyPressed('Space')) {
            this.machine.changeTo('jumping');
        }
    }
}

class PlayerWalkingState extends State {
    enter() {
        this.machine.entity.animation.play('walk');
    }

    update() {
        const input = this.machine.entity.game.getSystem('input');

        if (!input.isKeyPressed('KeyA') && !input.isKeyPressed('KeyD')) {
            this.machine.changeTo('idle');
        }

        if (input.isKeyPressed('Space')) {
            this.machine.changeTo('jumping');
        }

        // Handle movement
        if (input.isKeyPressed('KeyA')) {
            this.machine.entity.moveLeft();
        }
        if (input.isKeyPressed('KeyD')) {
            this.machine.entity.moveRight();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Complete Game Implementation

Let's see how everything comes together in a complete game:

Main Game Entry Point

// main.js
import { Game } from './engine/core/Game.js';
import { BootScene } from './scenes/BootScene.js';
import { MenuScene } from './scenes/MenuScene.js';
import { GameScene } from './scenes/GameScene.js';

// Initialize game when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
    // Create and initialize game
    const game = new Game();
    game.init();

    // Register scenes
    game.sceneManager.register('boot', BootScene);
    game.sceneManager.register('menu', MenuScene);
    game.sceneManager.register('game', GameScene);

    // Start the game
    game.start();

    // Make game globally available for debugging
    window.game = game;
});
Enter fullscreen mode Exit fullscreen mode

Example Game Scene

// scenes/GameScene.js
class GameScene extends Scene {
    onEnter(data) {
        console.log('Entering game scene');

        // Create game world
        this.createWorld();

        // Create player
        this.createPlayer();

        // Create UI
        this.createUI();

        // Start background music
        this.game.getSystem('audio').playMusic('game_theme');

        // Listen for game events
        eventBus.on('player:damaged', this.onPlayerDamaged, this);
        eventBus.on('enemy:defeated', this.onEnemyDefeated, this);
    }

    createWorld() {
        // Set up TileMap
        const tiles = [
            new Component(0, 0, 'green', 0, 0, 'rect'), // Grass
            new Component(0, 0, 'gray', 0, 0, 'rect'),  // Stone
            new Component(0, 0, 'blue', 0, 0, 'rect')   // Water
        ];

        const levelMap = [
            [2, 2, 2, 2, 2, 2],
            [2, 1, 1, 1, 1, 2],
            [2, 1, 3, 3, 1, 2],
            [2, 1, 1, 1, 1, 2],
            [2, 2, 2, 2, 2, 2]
        ];

        this.display.tile = tiles;
        this.display.map = levelMap;
        this.display.tileMap();
    }

    createPlayer() {
        this.player = new Player(this.game);
        this.player.spawn(100, 100);
        this.add(this.player.renderComponent.tcjsComponent);
    }

    createUI() {
        this.scoreText = new Component(20, 20, 'white', 10, 10, 'text');
        this.scoreText.text = 'Score: 0';
        this.add(this.scoreText);

        this.healthText = new Component(20, 20, 'white', 10, 40, 'text');
        this.healthText.text = 'Health: 100';
        this.add(this.healthText);
    }

    update() {
        // Update TileMap (v3 requirement)
        this.display.tileFace.show();

        // Update player
        if (this.player) {
            this.player.update();
        }

        // Update UI
        this.updateUI();
    }

    updateUI() {
        if (this.player) {
            this.scoreText.text = `Score: ${this.player.score}`;
            this.healthText.text = `Health: ${this.player.health}`;
        }
    }

    onPlayerDamaged(data) {
        // Handle player damage (screen shake, sound, etc.)
        this.game.getSystem('audio').play('player_hurt');
        this.game.getSystem('camera').shake(10, 200);
    }

    onEnemyDefeated(data) {
        // Handle enemy defeat
        this.player.addScore(data.points);
        eventBus.emit('game:score-changed', { score: this.player.score });
    }

    onExit() {
        // Clean up event listeners
        eventBus.off('player:damaged', this.onPlayerDamaged);
        eventBus.off('enemy:defeated', this.onEnemyDefeated);
    }
}
Enter fullscreen mode Exit fullscreen mode

🚀 Advanced Patterns

1. Dependency Injection Container

class Container {
    constructor() {
        this.services = new Map();
        this.singletons = new Map();
    }

    register(name, definition, isSingleton = false) {
        this.services.set(name, { definition, isSingleton });
    }

    get(name) {
        const service = this.services.get(name);

        if (!service) {
            throw new Error(`Service not found: ${name}`);
        }

        if (service.isSingleton) {
            if (!this.singletons.has(name)) {
                this.singletons.set(name, new service.definition());
            }
            return this.singletons.get(name);
        }

        return new service.definition();
    }
}

// Usage
const container = new Container();
container.register('AudioSystem', AudioSystem, true);
container.register('InputSystem', InputSystem, true);

const audio = container.get('AudioSystem');
Enter fullscreen mode Exit fullscreen mode

2. Configuration Management

class Config {
    constructor() {
        this.data = new Map();
        this.load();
    }

    load() {
        // Load from localStorage or defaults
        const saved = localStorage.getItem('game_config');
        if (saved) {
            this.data = new Map(Object.entries(JSON.parse(saved)));
        } else {
            this.setDefaults();
        }
    }

    setDefaults() {
        this.data.set('audio.masterVolume', 1.0);
        this.data.set('audio.sfxVolume', 0.8);
        this.data.set('audio.musicVolume', 0.6);
        this.data.set('graphics.quality', 'medium');
        this.data.set('controls.sensitivity', 1.0);
    }

    get(key, defaultValue = null) {
        return this.data.has(key) ? this.data.get(key) : defaultValue;
    }

    set(key, value) {
        this.data.set(key, value);
        this.save();
    }

    save() {
        const obj = Object.fromEntries(this.data);
        localStorage.setItem('game_config', JSON.stringify(obj));
    }
}
Enter fullscreen mode Exit fullscreen mode

📈 Best Practices for Scalable Architecture

1. Code Organization

// Good: Organized by feature
// entities/Player.js
class Player extends Entity {
    // All player-related code
}

// systems/PlayerSystem.js  
class PlayerSystem {
    // Handles player updates and logic
}

// Bad: Everything in one file
// game.js - 1000+ lines of mixed concerns
Enter fullscreen mode Exit fullscreen mode

2. Consistent Naming Conventions

// Events: domain:action
eventBus.emit('player:jumped', { height: 10 });
eventBus.emit('game:paused', { timestamp: Date.now() });

// Components: Noun + Component
class HealthComponent extends Component {}
class RenderComponent extends Component {}

// Systems: Domain + System
class PhysicsSystem extends System {}
class AudioSystem extends System {}
Enter fullscreen mode Exit fullscreen mode

3. Error Boundaries

class ErrorBoundary {
    static wrap(method, context, fallback = null) {
        return function(...args) {
            try {
                return method.apply(context, args);
            } catch (error) {
                console.error(`Error in ${method.name}:`, error);
                eventBus.emit('error:occurred', { error, method: method.name });
                return fallback;
            }
        };
    }
}

// Usage
this.update = ErrorBoundary.wrap(this.update, this);
Enter fullscreen mode Exit fullscreen mode

🎯 Your Architecture Challenge

Ready to architect like a pro? Try these exercises:

  1. Refactor an existing project using the Scene Manager pattern
  2. Implement the Event Bus in a current game to decouple systems
  3. Create a State Machine for a complex game entity (player, enemy, boss)
  4. Build a reusable Entity system that you can use across multiple projects
  5. Implement a proper Configuration system with save/load functionality

📚 Key Architecture Principles

  • Separation of Concerns: Each class should have a single responsibility
  • Composition over Inheritance: Build complex behaviors from simple components
  • Dependency Inversion: Depend on abstractions, not concretions
  • Event-Driven Architecture: Use events to communicate between systems
  • Configuration over Code: Make behavior configurable when possible

Great architecture isn't about following rules—it's about creating code that's easy to understand, modify, and extend. With these TCJSGame architecture patterns, you're equipped to build games that can grow from simple prototypes to complex, feature-rich experiences.

What architecture challenge are you facing in your TCJSGame projects? Share your experiences and solutions in the comments below!


This completes our TCJSGame mastery series! You now have expert knowledge of Performance, Architecture, and all the core systems needed to build professional-quality games. Go forth and create something amazing!


This architecture guide provides the final piece of professional game development knowledge. Combined with performance optimization and core TCJSGame features, you now have a complete toolkit for building scalable, maintainable games that stand the test of time.

Top comments (0)