TCJSGame Advanced Architecture: Building Scalable, Maintainable Games
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/
🎮 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();
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
}
}
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();
}
}
}
🔧 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();
}
}
}
}
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 });
}
}
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();
}
}
}
🎯 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;
});
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);
}
}
🚀 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');
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));
}
}
📈 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
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 {}
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);
🎯 Your Architecture Challenge
Ready to architect like a pro? Try these exercises:
- Refactor an existing project using the Scene Manager pattern
- Implement the Event Bus in a current game to decouple systems
- Create a State Machine for a complex game entity (player, enemy, boss)
- Build a reusable Entity system that you can use across multiple projects
- 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)