JavaScript Game Development: Canvas, WebGL, Physics, and Game Engines
Build games with JavaScript using Canvas API, WebGL, physics engines, and game development frameworks. Master 2D/3D graphics and game mechanics.
JavaScript game development has evolved dramatically, enabling the creation of sophisticated 2D and 3D games that run directly in browsers. Using the Canvas API, WebGL, physics engines, and modern game frameworks, developers can build everything from simple arcade games to complex multiplayer experiences. This comprehensive guide covers the complete game development pipeline with JavaScript.
Canvas API and 2D Graphics
Game Engine Foundation
// Core Game Engine for 2D games
class GameEngine {
constructor(canvasId, width = 800, height = 600) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.canvas.width = width;
this.canvas.height = height;
// Game state
this.isRunning = false;
this.lastTime = 0;
this.deltaTime = 0;
this.fps = 0;
this.frameCount = 0;
// Game systems
this.scenes = new Map();
this.currentScene = null;
this.input = new InputManager();
this.audio = new AudioManager();
this.assets = new AssetManager();
this.physics = new PhysicsEngine();
this.renderer = new Renderer(this.ctx);
// Performance monitoring
this.performanceStats = {
drawCalls: 0,
entities: 0,
averageFPS: 0,
};
this.setupCanvas();
}
setupCanvas() {
// Set up canvas styling
this.canvas.style.display = 'block';
this.canvas.style.margin = '0 auto';
this.canvas.style.border = '1px solid #333';
this.canvas.style.background = '#000';
// Handle high DPI displays
const devicePixelRatio = window.devicePixelRatio || 1;
const backingStoreRatio =
this.ctx.webkitBackingStorePixelRatio ||
this.ctx.mozBackingStorePixelRatio ||
this.ctx.msBackingStorePixelRatio ||
this.ctx.oBackingStorePixelRatio ||
this.ctx.backingStorePixelRatio ||
1;
const ratio = devicePixelRatio / backingStoreRatio;
if (ratio !== 1) {
this.canvas.width = this.canvas.width * ratio;
this.canvas.height = this.canvas.height * ratio;
this.canvas.style.width = this.canvas.width / ratio + 'px';
this.canvas.style.height = this.canvas.height / ratio + 'px';
this.ctx.scale(ratio, ratio);
}
}
// Scene management
addScene(name, scene) {
this.scenes.set(name, scene);
scene.engine = this;
}
setScene(name) {
const scene = this.scenes.get(name);
if (!scene) {
throw new Error(`Scene "${name}" not found`);
}
if (this.currentScene) {
this.currentScene.onExit();
}
this.currentScene = scene;
scene.onEnter();
}
// Game loop
start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
// Start input system
this.input.initialize(this.canvas);
// Start game loop
this.gameLoop();
}
stop() {
this.isRunning = false;
this.input.cleanup();
}
gameLoop() {
if (!this.isRunning) return;
const currentTime = performance.now();
this.deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
// Calculate FPS
this.frameCount++;
if (this.frameCount % 60 === 0) {
this.fps = Math.round(1 / this.deltaTime);
this.performanceStats.averageFPS = this.fps;
}
// Update and render
this.update(this.deltaTime);
this.render();
// Continue loop
requestAnimationFrame(() => this.gameLoop());
}
update(deltaTime) {
// Update input
this.input.update();
// Update current scene
if (this.currentScene) {
this.currentScene.update(deltaTime);
}
// Update physics
this.physics.update(deltaTime);
// Update audio
this.audio.update();
}
render() {
// Clear canvas
this.renderer.clear();
// Reset performance stats
this.performanceStats.drawCalls = 0;
this.performanceStats.entities = 0;
// Render current scene
if (this.currentScene) {
this.currentScene.render(this.renderer);
}
// Render debug info
this.renderDebugInfo();
}
renderDebugInfo() {
this.ctx.fillStyle = 'white';
this.ctx.font = '12px monospace';
this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
this.ctx.fillText(`Entities: ${this.performanceStats.entities}`, 10, 35);
this.ctx.fillText(`Draw Calls: ${this.performanceStats.drawCalls}`, 10, 50);
}
// Utility methods
getMousePosition() {
return this.input.mouse.position;
}
isKeyPressed(key) {
return this.input.isKeyPressed(key);
}
isKeyDown(key) {
return this.input.isKeyDown(key);
}
}
// Input Manager
class InputManager {
constructor() {
this.keys = {};
this.keysPressed = {};
this.mouse = {
position: { x: 0, y: 0 },
buttons: {},
wheel: 0,
};
this.touch = {
touches: [],
isActive: false,
};
this.eventHandlers = [];
}
initialize(canvas) {
this.canvas = canvas;
// Keyboard events
const keyDownHandler = (e) => {
this.keys[e.code] = true;
this.keysPressed[e.code] = true;
e.preventDefault();
};
const keyUpHandler = (e) => {
this.keys[e.code] = false;
e.preventDefault();
};
// Mouse events
const mouseDownHandler = (e) => {
this.mouse.buttons[e.button] = true;
this.updateMousePosition(e);
e.preventDefault();
};
const mouseUpHandler = (e) => {
this.mouse.buttons[e.button] = false;
e.preventDefault();
};
const mouseMoveHandler = (e) => {
this.updateMousePosition(e);
};
const wheelHandler = (e) => {
this.mouse.wheel = e.deltaY;
e.preventDefault();
};
// Touch events
const touchStartHandler = (e) => {
this.updateTouches(e);
e.preventDefault();
};
const touchMoveHandler = (e) => {
this.updateTouches(e);
e.preventDefault();
};
const touchEndHandler = (e) => {
this.updateTouches(e);
e.preventDefault();
};
// Add event listeners
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('keyup', keyUpHandler);
canvas.addEventListener('mousedown', mouseDownHandler);
canvas.addEventListener('mouseup', mouseUpHandler);
canvas.addEventListener('mousemove', mouseMoveHandler);
canvas.addEventListener('wheel', wheelHandler);
canvas.addEventListener('touchstart', touchStartHandler);
canvas.addEventListener('touchmove', touchMoveHandler);
canvas.addEventListener('touchend', touchEndHandler);
// Store handlers for cleanup
this.eventHandlers = [
{ element: document, event: 'keydown', handler: keyDownHandler },
{ element: document, event: 'keyup', handler: keyUpHandler },
{ element: canvas, event: 'mousedown', handler: mouseDownHandler },
{ element: canvas, event: 'mouseup', handler: mouseUpHandler },
{ element: canvas, event: 'mousemove', handler: mouseMoveHandler },
{ element: canvas, event: 'wheel', handler: wheelHandler },
{ element: canvas, event: 'touchstart', handler: touchStartHandler },
{ element: canvas, event: 'touchmove', handler: touchMoveHandler },
{ element: canvas, event: 'touchend', handler: touchEndHandler },
];
}
updateMousePosition(e) {
const rect = this.canvas.getBoundingClientRect();
this.mouse.position.x = e.clientX - rect.left;
this.mouse.position.y = e.clientY - rect.top;
}
updateTouches(e) {
this.touch.touches = Array.from(e.touches).map((touch) => {
const rect = this.canvas.getBoundingClientRect();
return {
id: touch.identifier,
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
};
});
this.touch.isActive = this.touch.touches.length > 0;
}
update() {
// Reset pressed keys (only true for one frame)
this.keysPressed = {};
// Reset mouse wheel
this.mouse.wheel = 0;
}
isKeyDown(key) {
return !!this.keys[key];
}
isKeyPressed(key) {
return !!this.keysPressed[key];
}
isMouseButtonDown(button) {
return !!this.mouse.buttons[button];
}
cleanup() {
this.eventHandlers.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.eventHandlers = [];
}
}
// Game Object base class
class GameObject {
constructor(x = 0, y = 0) {
this.position = { x, y };
this.velocity = { x: 0, y: 0 };
this.acceleration = { x: 0, y: 0 };
this.rotation = 0;
this.scale = { x: 1, y: 1 };
this.anchor = { x: 0.5, y: 0.5 };
this.width = 0;
this.height = 0;
this.bounds = { x: 0, y: 0, width: 0, height: 0 };
this.visible = true;
this.active = true;
this.destroyed = false;
this.components = new Map();
this.children = [];
this.parent = null;
this.tags = new Set();
}
// Component system
addComponent(name, component) {
this.components.set(name, component);
component.gameObject = this;
component.onAdd?.();
return this;
}
getComponent(name) {
return this.components.get(name);
}
removeComponent(name) {
const component = this.components.get(name);
if (component) {
component.onRemove?.();
this.components.delete(name);
}
return this;
}
// Hierarchy management
addChild(child) {
if (child.parent) {
child.parent.removeChild(child);
}
child.parent = this;
this.children.push(child);
return this;
}
removeChild(child) {
const index = this.children.indexOf(child);
if (index > -1) {
this.children.splice(index, 1);
child.parent = null;
}
return this;
}
// Update methods
update(deltaTime) {
if (!this.active) return;
// Update physics
this.velocity.x += this.acceleration.x * deltaTime;
this.velocity.y += this.acceleration.y * deltaTime;
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
// Update components
this.components.forEach((component) => {
component.update?.(deltaTime);
});
// Update children
this.children.forEach((child) => {
child.update(deltaTime);
});
// Update bounds
this.updateBounds();
// Custom update
this.onUpdate(deltaTime);
}
render(renderer) {
if (!this.visible) return;
renderer.save();
// Apply transform
renderer.translate(this.position.x, this.position.y);
renderer.rotate(this.rotation);
renderer.scale(this.scale.x, this.scale.y);
// Render components
this.components.forEach((component) => {
component.render?.(renderer);
});
// Custom render
this.onRender(renderer);
// Render children
this.children.forEach((child) => {
child.render(renderer);
});
renderer.restore();
}
updateBounds() {
this.bounds.x = this.position.x - this.width * this.anchor.x;
this.bounds.y = this.position.y - this.height * this.anchor.y;
this.bounds.width = this.width;
this.bounds.height = this.height;
}
// Collision detection
intersects(other) {
return !(
this.bounds.x + this.bounds.width < other.bounds.x ||
other.bounds.x + other.bounds.width < this.bounds.x ||
this.bounds.y + this.bounds.height < other.bounds.y ||
other.bounds.y + other.bounds.height < this.bounds.y
);
}
containsPoint(x, y) {
return (
x >= this.bounds.x &&
x <= this.bounds.x + this.bounds.width &&
y >= this.bounds.y &&
y <= this.bounds.y + this.bounds.height
);
}
// Tag system
addTag(tag) {
this.tags.add(tag);
return this;
}
removeTag(tag) {
this.tags.delete(tag);
return this;
}
hasTag(tag) {
return this.tags.has(tag);
}
// Lifecycle methods (override in subclasses)
onUpdate(deltaTime) {}
onRender(renderer) {}
onDestroy() {}
// Utility methods
destroy() {
this.destroyed = true;
this.onDestroy();
// Remove from parent
if (this.parent) {
this.parent.removeChild(this);
}
// Destroy children
this.children.forEach((child) => child.destroy());
// Clean up components
this.components.forEach((component) => {
component.onRemove?.();
});
}
setPosition(x, y) {
this.position.x = x;
this.position.y = y;
return this;
}
setVelocity(x, y) {
this.velocity.x = x;
this.velocity.y = y;
return this;
}
setScale(x, y = x) {
this.scale.x = x;
this.scale.y = y;
return this;
}
getGlobalPosition() {
let x = this.position.x;
let y = this.position.y;
let parent = this.parent;
while (parent) {
x += parent.position.x;
y += parent.position.y;
parent = parent.parent;
}
return { x, y };
}
distanceTo(other) {
const dx = this.position.x - other.position.x;
const dy = this.position.y - other.position.y;
return Math.sqrt(dx * dx + dy * dy);
}
angleTo(other) {
const dx = other.position.x - this.position.x;
const dy = other.position.y - this.position.y;
return Math.atan2(dy, dx);
}
}
// Sprite component for rendering images
class SpriteComponent {
constructor(image, sourceRect = null) {
this.image = image;
this.sourceRect = sourceRect;
this.tint = null;
this.alpha = 1;
this.flipX = false;
this.flipY = false;
}
onAdd() {
if (this.image && !this.gameObject.width) {
this.gameObject.width = this.sourceRect
? this.sourceRect.width
: this.image.width;
this.gameObject.height = this.sourceRect
? this.sourceRect.height
: this.image.height;
}
}
render(renderer) {
if (!this.image) return;
renderer.save();
// Apply sprite-specific transforms
if (this.flipX || this.flipY) {
renderer.scale(this.flipX ? -1 : 1, this.flipY ? -1 : 1);
}
if (this.alpha < 1) {
renderer.setAlpha(this.alpha);
}
// Calculate draw position
const width = this.gameObject.width;
const height = this.gameObject.height;
const x = -width * this.gameObject.anchor.x;
const y = -height * this.gameObject.anchor.y;
if (this.sourceRect) {
renderer.drawImage(
this.image,
this.sourceRect.x,
this.sourceRect.y,
this.sourceRect.width,
this.sourceRect.height,
x,
y,
width,
height
);
} else {
renderer.drawImage(this.image, x, y, width, height);
}
renderer.restore();
}
}
// Animation component for sprite animations
class AnimationComponent {
constructor(spriteSheet, animations) {
this.spriteSheet = spriteSheet;
this.animations = animations;
this.currentAnimation = null;
this.currentFrame = 0;
this.frameTime = 0;
this.isPlaying = false;
this.loop = true;
this.onComplete = null;
}
play(animationName, loop = true) {
const animation = this.animations[animationName];
if (!animation) {
console.warn(`Animation "${animationName}" not found`);
return;
}
this.currentAnimation = animation;
this.currentFrame = 0;
this.frameTime = 0;
this.isPlaying = true;
this.loop = loop;
// Update sprite component
const sprite = this.gameObject.getComponent('sprite');
if (sprite) {
sprite.sourceRect = animation.frames[0];
}
}
stop() {
this.isPlaying = false;
}
update(deltaTime) {
if (!this.isPlaying || !this.currentAnimation) return;
this.frameTime += deltaTime;
if (this.frameTime >= this.currentAnimation.frameRate) {
this.frameTime = 0;
this.currentFrame++;
if (this.currentFrame >= this.currentAnimation.frames.length) {
if (this.loop) {
this.currentFrame = 0;
} else {
this.currentFrame = this.currentAnimation.frames.length - 1;
this.isPlaying = false;
this.onComplete?.();
}
}
// Update sprite component
const sprite = this.gameObject.getComponent('sprite');
if (sprite) {
sprite.sourceRect = this.currentAnimation.frames[this.currentFrame];
}
}
}
}
// Simple renderer wrapper
class Renderer {
constructor(ctx) {
this.ctx = ctx;
this.drawCalls = 0;
}
clear() {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.drawCalls = 0;
}
save() {
this.ctx.save();
}
restore() {
this.ctx.restore();
}
translate(x, y) {
this.ctx.translate(x, y);
}
rotate(angle) {
this.ctx.rotate(angle);
}
scale(x, y) {
this.ctx.scale(x, y);
}
setAlpha(alpha) {
this.ctx.globalAlpha = alpha;
}
drawImage(image, ...args) {
this.ctx.drawImage(image, ...args);
this.drawCalls++;
}
fillRect(x, y, width, height) {
this.ctx.fillRect(x, y, width, height);
this.drawCalls++;
}
strokeRect(x, y, width, height) {
this.ctx.strokeRect(x, y, width, height);
this.drawCalls++;
}
fillCircle(x, y, radius) {
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2);
this.ctx.fill();
this.drawCalls++;
}
fillText(text, x, y) {
this.ctx.fillText(text, x, y);
this.drawCalls++;
}
setFillStyle(style) {
this.ctx.fillStyle = style;
}
setStrokeStyle(style) {
this.ctx.strokeStyle = style;
}
setFont(font) {
this.ctx.font = font;
}
}
// Usage example - Simple game scene
class GameScene {
constructor() {
this.entities = [];
this.player = null;
this.enemies = [];
this.projectiles = [];
}
onEnter() {
console.log('Game scene entered');
this.setupScene();
}
onExit() {
console.log('Game scene exited');
this.cleanup();
}
setupScene() {
// Create player
this.player = new GameObject(400, 500);
this.player.width = 32;
this.player.height = 32;
this.player.addTag('player');
// Add player to entities
this.entities.push(this.player);
// Create some enemies
for (let i = 0; i < 5; i++) {
const enemy = new GameObject(100 + i * 120, 100);
enemy.width = 24;
enemy.height = 24;
enemy.addTag('enemy');
enemy.setVelocity(50, 0);
this.enemies.push(enemy);
this.entities.push(enemy);
}
}
update(deltaTime) {
// Update all entities
this.entities.forEach((entity) => {
entity.update(deltaTime);
});
// Handle input
this.handleInput(deltaTime);
// Update game logic
this.updateGameLogic(deltaTime);
// Remove destroyed entities
this.entities = this.entities.filter((entity) => !entity.destroyed);
}
handleInput(deltaTime) {
if (!this.player) return;
const speed = 200;
if (this.engine.isKeyDown('ArrowLeft') || this.engine.isKeyDown('KeyA')) {
this.player.velocity.x = -speed;
} else if (
this.engine.isKeyDown('ArrowRight') ||
this.engine.isKeyDown('KeyD')
) {
this.player.velocity.x = speed;
} else {
this.player.velocity.x = 0;
}
if (this.engine.isKeyPressed('Space')) {
this.shootProjectile();
}
}
shootProjectile() {
const projectile = new GameObject(
this.player.position.x,
this.player.position.y - 20
);
projectile.width = 4;
projectile.height = 10;
projectile.setVelocity(0, -300);
projectile.addTag('projectile');
this.projectiles.push(projectile);
this.entities.push(projectile);
}
updateGameLogic(deltaTime) {
// Move enemies
this.enemies.forEach((enemy) => {
if (enemy.position.x <= 0 || enemy.position.x >= 800) {
enemy.velocity.x *= -1;
enemy.position.y += 20;
}
});
// Check collisions
this.checkCollisions();
// Remove off-screen projectiles
this.projectiles = this.projectiles.filter((projectile) => {
if (projectile.position.y < -10) {
projectile.destroy();
return false;
}
return true;
});
}
checkCollisions() {
// Projectile vs enemy collisions
this.projectiles.forEach((projectile) => {
this.enemies.forEach((enemy) => {
if (projectile.intersects(enemy)) {
projectile.destroy();
enemy.destroy();
// Remove from arrays
const projIndex = this.projectiles.indexOf(projectile);
if (projIndex > -1) this.projectiles.splice(projIndex, 1);
const enemyIndex = this.enemies.indexOf(enemy);
if (enemyIndex > -1) this.enemies.splice(enemyIndex, 1);
}
});
});
}
render(renderer) {
// Render all entities
this.entities.forEach((entity) => {
this.renderEntity(entity, renderer);
});
// Render UI
this.renderUI(renderer);
}
renderEntity(entity, renderer) {
renderer.save();
// Set color based on tag
if (entity.hasTag('player')) {
renderer.setFillStyle('#00ff00');
} else if (entity.hasTag('enemy')) {
renderer.setFillStyle('#ff0000');
} else if (entity.hasTag('projectile')) {
renderer.setFillStyle('#ffff00');
} else {
renderer.setFillStyle('#ffffff');
}
// Draw entity as rectangle
renderer.fillRect(
entity.bounds.x,
entity.bounds.y,
entity.bounds.width,
entity.bounds.height
);
renderer.restore();
}
renderUI(renderer) {
renderer.setFillStyle('white');
renderer.setFont('16px Arial');
renderer.fillText(`Enemies: ${this.enemies.length}`, 10, 80);
renderer.fillText('Arrow keys or WASD to move, Space to shoot', 10, 580);
}
cleanup() {
this.entities.forEach((entity) => entity.destroy());
this.entities = [];
this.enemies = [];
this.projectiles = [];
this.player = null;
}
}
// Initialize game
const canvas = document.createElement('canvas');
canvas.id = 'game-canvas';
document.body.appendChild(canvas);
const game = new GameEngine('game-canvas', 800, 600);
const gameScene = new GameScene();
game.addScene('game', gameScene);
game.setScene('game');
game.start();
console.log('Game started! Use arrow keys or WASD to move, Space to shoot');
Physics Engine Integration
2D Physics System
// Physics Engine for game physics simulation
class PhysicsEngine {
constructor() {
this.gravity = { x: 0, y: 980 }; // pixels/second²
this.bodies = [];
this.constraints = [];
this.collisionPairs = [];
this.spatialHash = new SpatialHash(64);
// Physics settings
this.iterations = 6;
this.damping = 0.99;
this.timeScale = 1;
}
// Add physics body
addBody(body) {
this.bodies.push(body);
body.physicsEngine = this;
return body;
}
removeBody(body) {
const index = this.bodies.indexOf(body);
if (index > -1) {
this.bodies.splice(index, 1);
}
}
// Physics update
update(deltaTime) {
const dt = deltaTime * this.timeScale;
// Clear spatial hash
this.spatialHash.clear();
// Update bodies
this.bodies.forEach((body) => {
this.updateBody(body, dt);
this.spatialHash.insert(body);
});
// Detect collisions
this.detectCollisions();
// Solve constraints and collisions
for (let i = 0; i < this.iterations; i++) {
this.solveConstraints();
this.solveCollisions();
}
// Update positions
this.bodies.forEach((body) => {
this.updateBodyPosition(body, dt);
});
}
updateBody(body, deltaTime) {
if (body.isStatic) return;
// Apply gravity
if (body.useGravity) {
body.force.x += this.gravity.x * body.mass;
body.force.y += this.gravity.y * body.mass;
}
// Apply forces (F = ma)
body.acceleration.x = body.force.x / body.mass;
body.acceleration.y = body.force.y / body.mass;
// Integrate velocity (Verlet integration)
body.velocity.x += body.acceleration.x * deltaTime;
body.velocity.y += body.acceleration.y * deltaTime;
// Apply damping
body.velocity.x *= this.damping;
body.velocity.y *= this.damping;
// Store previous position for Verlet integration
body.previousPosition.x = body.position.x;
body.previousPosition.y = body.position.y;
// Clear forces
body.force.x = 0;
body.force.y = 0;
}
updateBodyPosition(body, deltaTime) {
if (body.isStatic) return;
// Update position
body.position.x += body.velocity.x * deltaTime;
body.position.y += body.velocity.y * deltaTime;
// Update game object position if linked
if (body.gameObject) {
body.gameObject.position.x = body.position.x;
body.gameObject.position.y = body.position.y;
}
}
detectCollisions() {
this.collisionPairs = [];
// Use spatial hashing for broad phase
const potentialCollisions = this.spatialHash.getPotentialCollisions();
// Narrow phase collision detection
potentialCollisions.forEach(([bodyA, bodyB]) => {
const collision = this.checkCollision(bodyA, bodyB);
if (collision) {
this.collisionPairs.push(collision);
}
});
}
checkCollision(bodyA, bodyB) {
// Circle vs Circle collision
if (bodyA.shape.type === 'circle' && bodyB.shape.type === 'circle') {
return this.circleVsCircle(bodyA, bodyB);
}
// Rectangle vs Rectangle collision
if (bodyA.shape.type === 'rectangle' && bodyB.shape.type === 'rectangle') {
return this.rectangleVsRectangle(bodyA, bodyB);
}
// Circle vs Rectangle collision
if (bodyA.shape.type === 'circle' && bodyB.shape.type === 'rectangle') {
return this.circleVsRectangle(bodyA, bodyB);
}
if (bodyA.shape.type === 'rectangle' && bodyB.shape.type === 'circle') {
return this.circleVsRectangle(bodyB, bodyA);
}
return null;
}
circleVsCircle(bodyA, bodyB) {
const dx = bodyB.position.x - bodyA.position.x;
const dy = bodyB.position.y - bodyA.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const combinedRadius = bodyA.shape.radius + bodyB.shape.radius;
if (distance < combinedRadius) {
const overlap = combinedRadius - distance;
const normalX = dx / distance;
const normalY = dy / distance;
return {
bodyA,
bodyB,
normal: { x: normalX, y: normalY },
overlap,
contactPoint: {
x: bodyA.position.x + normalX * bodyA.shape.radius,
y: bodyA.position.y + normalY * bodyA.shape.radius,
},
};
}
return null;
}
rectangleVsRectangle(bodyA, bodyB) {
const boundsA = this.getRectangleBounds(bodyA);
const boundsB = this.getRectangleBounds(bodyB);
const overlapX =
Math.min(boundsA.right, boundsB.right) -
Math.max(boundsA.left, boundsB.left);
const overlapY =
Math.min(boundsA.bottom, boundsB.bottom) -
Math.max(boundsA.top, boundsB.top);
if (overlapX > 0 && overlapY > 0) {
let normalX, normalY, overlap;
if (overlapX < overlapY) {
// Horizontal collision
overlap = overlapX;
normalX = bodyA.position.x < bodyB.position.x ? -1 : 1;
normalY = 0;
} else {
// Vertical collision
overlap = overlapY;
normalX = 0;
normalY = bodyA.position.y < bodyB.position.y ? -1 : 1;
}
return {
bodyA,
bodyB,
normal: { x: normalX, y: normalY },
overlap,
contactPoint: {
x: (boundsA.left + boundsA.right + boundsB.left + boundsB.right) / 4,
y: (boundsA.top + boundsA.bottom + boundsB.top + boundsB.bottom) / 4,
},
};
}
return null;
}
circleVsRectangle(circle, rectangle) {
const bounds = this.getRectangleBounds(rectangle);
// Find closest point on rectangle to circle center
const closestX = Math.max(
bounds.left,
Math.min(circle.position.x, bounds.right)
);
const closestY = Math.max(
bounds.top,
Math.min(circle.position.y, bounds.bottom)
);
const dx = circle.position.x - closestX;
const dy = circle.position.y - closestY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < circle.shape.radius) {
const overlap = circle.shape.radius - distance;
const normalX = distance > 0 ? dx / distance : 1;
const normalY = distance > 0 ? dy / distance : 0;
return {
bodyA: circle,
bodyB: rectangle,
normal: { x: normalX, y: normalY },
overlap,
contactPoint: { x: closestX, y: closestY },
};
}
return null;
}
getRectangleBounds(body) {
const halfWidth = body.shape.width / 2;
const halfHeight = body.shape.height / 2;
return {
left: body.position.x - halfWidth,
right: body.position.x + halfWidth,
top: body.position.y - halfHeight,
bottom: body.position.y + halfHeight,
};
}
solveCollisions() {
this.collisionPairs.forEach((collision) => {
this.resolveCollision(collision);
});
}
resolveCollision(collision) {
const { bodyA, bodyB, normal, overlap } = collision;
// Skip if either body is static
if (bodyA.isStatic && bodyB.isStatic) return;
// Calculate relative velocity
const relativeVelocityX = bodyB.velocity.x - bodyA.velocity.x;
const relativeVelocityY = bodyB.velocity.y - bodyA.velocity.y;
const separatingVelocity =
relativeVelocityX * normal.x + relativeVelocityY * normal.y;
// Don't resolve if objects are separating
if (separatingVelocity > 0) return;
// Calculate restitution (bounciness)
const restitution = Math.min(bodyA.restitution, bodyB.restitution);
// Calculate impulse
const newSeparatingVelocity = -separatingVelocity * restitution;
const deltaVelocity = newSeparatingVelocity - separatingVelocity;
const totalInverseMass = bodyA.getInverseMass() + bodyB.getInverseMass();
if (totalInverseMass <= 0) return;
const impulse = deltaVelocity / totalInverseMass;
const impulsePerIMass = { x: impulse * normal.x, y: impulse * normal.y };
// Apply impulse
if (!bodyA.isStatic) {
bodyA.velocity.x -= impulsePerIMass.x * bodyA.getInverseMass();
bodyA.velocity.y -= impulsePerIMass.y * bodyA.getInverseMass();
}
if (!bodyB.isStatic) {
bodyB.velocity.x += impulsePerIMass.x * bodyB.getInverseMass();
bodyB.velocity.y += impulsePerIMass.y * bodyB.getInverseMass();
}
// Position correction to prevent overlap
const correctionPercent = 0.2;
const slop = 0.01;
const correction =
(Math.max(overlap - slop, 0) / totalInverseMass) * correctionPercent;
if (!bodyA.isStatic) {
bodyA.position.x -= correction * bodyA.getInverseMass() * normal.x;
bodyA.position.y -= correction * bodyA.getInverseMass() * normal.y;
}
if (!bodyB.isStatic) {
bodyB.position.x += correction * bodyB.getInverseMass() * normal.x;
bodyB.position.y += correction * bodyB.getInverseMass() * normal.y;
}
// Trigger collision callbacks
bodyA.onCollision?.(bodyB, collision);
bodyB.onCollision?.(bodyA, collision);
}
solveConstraints() {
// Implement constraint solving (springs, joints, etc.)
}
// Utility methods
applyForce(body, force, point = null) {
body.force.x += force.x;
body.force.y += force.y;
// Apply torque if point is specified
if (point) {
const torque =
(point.x - body.position.x) * force.y -
(point.y - body.position.y) * force.x;
body.torque += torque;
}
}
raycast(start, end, filter = null) {
// Implement raycasting
const results = [];
this.bodies.forEach((body) => {
if (filter && !filter(body)) return;
const intersection = this.rayIntersectBody(start, end, body);
if (intersection) {
results.push(intersection);
}
});
// Sort by distance
results.sort((a, b) => a.distance - b.distance);
return results;
}
rayIntersectBody(start, end, body) {
// Simplified ray-body intersection
// In a real implementation, you'd have specific methods for each shape type
return null;
}
}
// Physics Body class
class PhysicsBody {
constructor(gameObject, shape, options = {}) {
this.gameObject = gameObject;
this.shape = shape;
this.position = { x: gameObject.position.x, y: gameObject.position.y };
this.previousPosition = { x: this.position.x, y: this.position.y };
this.velocity = { x: 0, y: 0 };
this.acceleration = { x: 0, y: 0 };
this.force = { x: 0, y: 0 };
this.mass = options.mass || 1;
this.inverseMass = this.mass > 0 ? 1 / this.mass : 0;
this.restitution = options.restitution || 0.6;
this.friction = options.friction || 0.3;
this.isStatic = options.isStatic || false;
this.isSensor = options.isSensor || false;
this.useGravity = options.useGravity !== false;
this.rotation = 0;
this.angularVelocity = 0;
this.torque = 0;
this.onCollision = options.onCollision;
}
getInverseMass() {
return this.isStatic ? 0 : this.inverseMass;
}
setStatic(isStatic) {
this.isStatic = isStatic;
this.inverseMass = isStatic ? 0 : this.mass > 0 ? 1 / this.mass : 0;
return this;
}
setMass(mass) {
this.mass = mass;
this.inverseMass = this.isStatic ? 0 : mass > 0 ? 1 / mass : 0;
return this;
}
applyForce(force, point = null) {
this.force.x += force.x;
this.force.y += force.y;
if (point) {
const torque =
(point.x - this.position.x) * force.y -
(point.y - this.position.y) * force.x;
this.torque += torque;
}
return this;
}
applyImpulse(impulse) {
if (this.isStatic) return this;
this.velocity.x += impulse.x * this.inverseMass;
this.velocity.y += impulse.y * this.inverseMass;
return this;
}
getBounds() {
switch (this.shape.type) {
case 'circle':
return {
x: this.position.x - this.shape.radius,
y: this.position.y - this.shape.radius,
width: this.shape.radius * 2,
height: this.shape.radius * 2,
};
case 'rectangle':
return {
x: this.position.x - this.shape.width / 2,
y: this.position.y - this.shape.height / 2,
width: this.shape.width,
height: this.shape.height,
};
default:
return { x: 0, y: 0, width: 0, height: 0 };
}
}
}
// Shape definitions
class CircleShape {
constructor(radius) {
this.type = 'circle';
this.radius = radius;
}
}
class RectangleShape {
constructor(width, height) {
this.type = 'rectangle';
this.width = width;
this.height = height;
}
}
// Spatial Hash for efficient collision detection
class SpatialHash {
constructor(cellSize) {
this.cellSize = cellSize;
this.grid = new Map();
}
clear() {
this.grid.clear();
}
getHash(x, y) {
const cellX = Math.floor(x / this.cellSize);
const cellY = Math.floor(y / this.cellSize);
return `${cellX},${cellY}`;
}
insert(body) {
const bounds = body.getBounds();
const minCellX = Math.floor(bounds.x / this.cellSize);
const minCellY = Math.floor(bounds.y / this.cellSize);
const maxCellX = Math.floor((bounds.x + bounds.width) / this.cellSize);
const maxCellY = Math.floor((bounds.y + bounds.height) / this.cellSize);
for (let x = minCellX; x <= maxCellX; x++) {
for (let y = minCellY; y <= maxCellY; y++) {
const hash = `${x},${y}`;
if (!this.grid.has(hash)) {
this.grid.set(hash, []);
}
this.grid.get(hash).push(body);
}
}
}
getPotentialCollisions() {
const pairs = [];
const checked = new Set();
this.grid.forEach((bodies) => {
for (let i = 0; i < bodies.length; i++) {
for (let j = i + 1; j < bodies.length; j++) {
const bodyA = bodies[i];
const bodyB = bodies[j];
const pairKey = `${Math.min(bodyA.id, bodyB.id)}-${Math.max(bodyA.id, bodyB.id)}`;
if (!checked.has(pairKey)) {
checked.add(pairKey);
pairs.push([bodyA, bodyB]);
}
}
}
});
return pairs;
}
}
// Physics component for game objects
class PhysicsComponent {
constructor(shape, options = {}) {
this.shape = shape;
this.options = options;
this.body = null;
}
onAdd() {
this.body = new PhysicsBody(this.gameObject, this.shape, this.options);
this.body.id = Math.random(); // Simple ID for spatial hashing
// Add to physics engine
if (this.gameObject.engine?.physics) {
this.gameObject.engine.physics.addBody(this.body);
}
}
onRemove() {
if (this.body && this.gameObject.engine?.physics) {
this.gameObject.engine.physics.removeBody(this.body);
}
}
update(deltaTime) {
if (this.body) {
// Sync game object with physics body
this.gameObject.position.x = this.body.position.x;
this.gameObject.position.y = this.body.position.y;
this.gameObject.rotation = this.body.rotation;
}
}
applyForce(force, point = null) {
if (this.body) {
this.body.applyForce(force, point);
}
}
applyImpulse(impulse) {
if (this.body) {
this.body.applyImpulse(impulse);
}
}
setVelocity(x, y) {
if (this.body) {
this.body.velocity.x = x;
this.body.velocity.y = y;
}
}
setStatic(isStatic) {
if (this.body) {
this.body.setStatic(isStatic);
}
}
}
// Example usage of physics system
function createPhysicsExample() {
// Create a physics-enabled game object
const ball = new GameObject(400, 100);
ball.width = 32;
ball.height = 32;
// Add physics component
ball.addComponent(
'physics',
new PhysicsComponent(new CircleShape(16), {
mass: 1,
restitution: 0.8,
useGravity: true,
onCollision: (otherBody, collision) => {
console.log('Ball collided with something!');
},
})
);
// Create static ground
const ground = new GameObject(400, 550);
ground.width = 800;
ground.height = 50;
ground.addComponent(
'physics',
new PhysicsComponent(new RectangleShape(800, 50), {
isStatic: true,
restitution: 0.3,
})
);
console.log('Physics example created - ball will fall and bounce on ground');
return { ball, ground };
}
// Uncomment to test physics
// const physicsExample = createPhysicsExample();
Audio Management and Asset Loading
Comprehensive Audio System
// Audio Manager for game audio
class AudioManager {
constructor() {
this.audioContext = null;
this.masterGain = null;
this.sounds = new Map();
this.music = new Map();
this.currentMusic = null;
// Volume controls
this.masterVolume = 1.0;
this.sfxVolume = 1.0;
this.musicVolume = 1.0;
// Audio pools for efficient playback
this.audioPools = new Map();
this.initialize();
}
async initialize() {
try {
// Create audio context
this.audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
// Create master gain node
this.masterGain = this.audioContext.createGain();
this.masterGain.connect(this.audioContext.destination);
this.masterGain.gain.value = this.masterVolume;
// Handle audio context state
if (this.audioContext.state === 'suspended') {
// Audio context is suspended, will need user interaction to resume
this.setupUserInteractionResume();
}
console.log('Audio Manager initialized');
} catch (error) {
console.error('Failed to initialize Audio Manager:', error);
}
}
setupUserInteractionResume() {
const resumeAudio = () => {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
// Remove listeners after first interaction
document.removeEventListener('click', resumeAudio);
document.removeEventListener('keydown', resumeAudio);
document.removeEventListener('touchstart', resumeAudio);
};
document.addEventListener('click', resumeAudio);
document.addEventListener('keydown', resumeAudio);
document.addEventListener('touchstart', resumeAudio);
}
// Load audio file
async loadSound(name, url, isMusic = false) {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
const audioData = {
buffer: audioBuffer,
isMusic,
volume: 1.0,
loop: false,
};
if (isMusic) {
this.music.set(name, audioData);
} else {
this.sounds.set(name, audioData);
// Create audio pool for sound effects
this.createAudioPool(name, audioData, 5);
}
console.log(`Loaded ${isMusic ? 'music' : 'sound'}: ${name}`);
} catch (error) {
console.error(`Failed to load audio "${name}":`, error);
}
}
createAudioPool(name, audioData, poolSize) {
const pool = [];
for (let i = 0; i < poolSize; i++) {
const source = this.createAudioSource(audioData);
pool.push(source);
}
this.audioPools.set(name, pool);
}
createAudioSource(audioData) {
const source = this.audioContext.createBufferSource();
const gainNode = this.audioContext.createGain();
source.buffer = audioData.buffer;
source.loop = audioData.loop;
// Connect audio graph
source.connect(gainNode);
gainNode.connect(this.masterGain);
return {
source,
gainNode,
isPlaying: false,
startTime: 0,
pauseTime: 0,
};
}
// Play sound effect
playSound(name, options = {}) {
const audioData = this.sounds.get(name);
if (!audioData) {
console.warn(`Sound "${name}" not found`);
return null;
}
const pool = this.audioPools.get(name);
if (!pool) {
console.warn(`Audio pool for "${name}" not found`);
return null;
}
// Find available audio source from pool
let audioSource = pool.find((source) => !source.isPlaying);
if (!audioSource) {
// All sources are busy, create a new one
audioSource = this.createAudioSource(audioData);
pool.push(audioSource);
}
// Configure audio source
const volume = (options.volume || 1.0) * audioData.volume * this.sfxVolume;
audioSource.gainNode.gain.value = volume;
const playbackRate = options.playbackRate || 1.0;
audioSource.source.playbackRate.value = playbackRate;
const loop = options.loop || audioData.loop;
audioSource.source.loop = loop;
// Play audio
try {
audioSource.source.start(0);
audioSource.isPlaying = true;
audioSource.startTime = this.audioContext.currentTime;
// Mark as not playing when finished
audioSource.source.onended = () => {
audioSource.isPlaying = false;
// Create new source for reuse
const newSource = this.createAudioSource(audioData);
const index = pool.indexOf(audioSource);
if (index > -1) {
pool[index] = newSource;
}
};
return audioSource;
} catch (error) {
console.error(`Failed to play sound "${name}":`, error);
return null;
}
}
// Play music
playMusic(name, options = {}) {
const audioData = this.music.get(name);
if (!audioData) {
console.warn(`Music "${name}" not found`);
return null;
}
// Stop current music
this.stopMusic();
// Create music source
const musicSource = this.createAudioSource({
...audioData,
loop: options.loop !== false, // Default to loop
});
// Configure music
const volume =
(options.volume || 1.0) * audioData.volume * this.musicVolume;
musicSource.gainNode.gain.value = volume;
// Fade in if specified
if (options.fadeIn) {
musicSource.gainNode.gain.value = 0;
musicSource.gainNode.gain.linearRampToValueAtTime(
volume,
this.audioContext.currentTime + options.fadeIn
);
}
// Play music
try {
musicSource.source.start(0);
musicSource.isPlaying = true;
musicSource.startTime = this.audioContext.currentTime;
this.currentMusic = musicSource;
return musicSource;
} catch (error) {
console.error(`Failed to play music "${name}":`, error);
return null;
}
}
// Stop music
stopMusic(fadeOut = 0) {
if (!this.currentMusic || !this.currentMusic.isPlaying) return;
if (fadeOut > 0) {
// Fade out
this.currentMusic.gainNode.gain.linearRampToValueAtTime(
0,
this.audioContext.currentTime + fadeOut
);
setTimeout(() => {
if (this.currentMusic) {
this.currentMusic.source.stop();
this.currentMusic.isPlaying = false;
this.currentMusic = null;
}
}, fadeOut * 1000);
} else {
// Immediate stop
this.currentMusic.source.stop();
this.currentMusic.isPlaying = false;
this.currentMusic = null;
}
}
// Pause/Resume music
pauseMusic() {
if (this.currentMusic && this.currentMusic.isPlaying) {
this.currentMusic.pauseTime = this.audioContext.currentTime;
this.currentMusic.source.stop();
this.currentMusic.isPlaying = false;
}
}
resumeMusic() {
if (
this.currentMusic &&
!this.currentMusic.isPlaying &&
this.currentMusic.pauseTime > 0
) {
// Create new source from pause point
const newSource = this.audioContext.createBufferSource();
newSource.buffer = this.currentMusic.source.buffer;
newSource.loop = this.currentMusic.source.loop;
newSource.connect(this.currentMusic.gainNode);
const offset = this.currentMusic.pauseTime - this.currentMusic.startTime;
newSource.start(0, offset);
this.currentMusic.source = newSource;
this.currentMusic.isPlaying = true;
this.currentMusic.startTime = this.audioContext.currentTime - offset;
this.currentMusic.pauseTime = 0;
}
}
// Volume controls
setMasterVolume(volume) {
this.masterVolume = Math.max(0, Math.min(1, volume));
if (this.masterGain) {
this.masterGain.gain.value = this.masterVolume;
}
}
setSFXVolume(volume) {
this.sfxVolume = Math.max(0, Math.min(1, volume));
}
setMusicVolume(volume) {
this.musicVolume = Math.max(0, Math.min(1, volume));
if (this.currentMusic) {
const audioData = Array.from(this.music.values()).find(
(data) => data.buffer === this.currentMusic.source.buffer
);
if (audioData) {
const newVolume = audioData.volume * this.musicVolume;
this.currentMusic.gainNode.gain.value = newVolume;
}
}
}
// 3D Positional Audio
createPositionalSound(name, position, options = {}) {
const audioData = this.sounds.get(name);
if (!audioData) {
console.warn(`Sound "${name}" not found`);
return null;
}
// Create 3D audio source
const source = this.audioContext.createBufferSource();
const panner = this.audioContext.createPanner();
const gainNode = this.audioContext.createGain();
source.buffer = audioData.buffer;
// Configure panner
panner.panningModel = 'HRTF';
panner.distanceModel = options.distanceModel || 'inverse';
panner.refDistance = options.refDistance || 1;
panner.maxDistance = options.maxDistance || 10000;
panner.rolloffFactor = options.rolloffFactor || 1;
// Set position
panner.setPosition(position.x, position.y, position.z || 0);
// Connect audio graph
source.connect(panner);
panner.connect(gainNode);
gainNode.connect(this.masterGain);
// Configure volume
const volume = (options.volume || 1.0) * audioData.volume * this.sfxVolume;
gainNode.gain.value = volume;
const positionalSource = {
source,
panner,
gainNode,
isPlaying: false,
position: { ...position },
};
// Play audio
try {
source.start(0);
positionalSource.isPlaying = true;
source.onended = () => {
positionalSource.isPlaying = false;
};
return positionalSource;
} catch (error) {
console.error(`Failed to play positional sound "${name}":`, error);
return null;
}
}
// Update listener position for 3D audio
setListenerPosition(position, orientation = null) {
if (!this.audioContext.listener) return;
this.audioContext.listener.setPosition(
position.x,
position.y,
position.z || 0
);
if (orientation) {
this.audioContext.listener.setOrientation(
orientation.forward.x,
orientation.forward.y,
orientation.forward.z,
orientation.up.x,
orientation.up.y,
orientation.up.z
);
}
}
// Audio effects
createReverb(impulseResponse) {
const convolver = this.audioContext.createConvolver();
convolver.buffer = impulseResponse;
return convolver;
}
createDelay(delayTime = 0.3, feedback = 0.3) {
const delay = this.audioContext.createDelay();
const feedbackGain = this.audioContext.createGain();
const wetGain = this.audioContext.createGain();
delay.delayTime.value = delayTime;
feedbackGain.gain.value = feedback;
wetGain.gain.value = 0.3;
// Connect delay feedback loop
delay.connect(feedbackGain);
feedbackGain.connect(delay);
delay.connect(wetGain);
return { delay, wetGain, feedbackGain };
}
// Update method for continuous effects
update() {
// Update any time-based audio effects
}
// Cleanup
cleanup() {
this.stopMusic();
// Stop all playing sounds
this.audioPools.forEach((pool) => {
pool.forEach((audioSource) => {
if (audioSource.isPlaying) {
audioSource.source.stop();
}
});
});
// Close audio context
if (this.audioContext) {
this.audioContext.close();
}
}
}
// Asset Manager for loading game assets
class AssetManager {
constructor() {
this.images = new Map();
this.audio = new Map();
this.fonts = new Map();
this.data = new Map();
this.loadQueue = [];
this.isLoading = false;
this.totalAssets = 0;
this.loadedAssets = 0;
this.onProgress = null;
this.onComplete = null;
this.onError = null;
}
// Add assets to load queue
addImage(name, url) {
this.loadQueue.push({ type: 'image', name, url });
return this;
}
addAudio(name, url, isMusic = false) {
this.loadQueue.push({ type: 'audio', name, url, isMusic });
return this;
}
addFont(name, url) {
this.loadQueue.push({ type: 'font', name, url });
return this;
}
addData(name, url) {
this.loadQueue.push({ type: 'data', name, url });
return this;
}
// Load all queued assets
async loadAll() {
if (this.isLoading) return;
this.isLoading = true;
this.totalAssets = this.loadQueue.length;
this.loadedAssets = 0;
const promises = this.loadQueue.map((asset) => this.loadAsset(asset));
try {
await Promise.all(promises);
console.log('All assets loaded successfully');
this.onComplete?.();
} catch (error) {
console.error('Asset loading failed:', error);
this.onError?.(error);
} finally {
this.isLoading = false;
this.loadQueue = [];
}
}
async loadAsset(asset) {
try {
switch (asset.type) {
case 'image':
await this.loadImageAsset(asset);
break;
case 'audio':
await this.loadAudioAsset(asset);
break;
case 'font':
await this.loadFontAsset(asset);
break;
case 'data':
await this.loadDataAsset(asset);
break;
}
this.loadedAssets++;
this.onProgress?.(this.loadedAssets / this.totalAssets);
} catch (error) {
console.error(`Failed to load ${asset.type} "${asset.name}":`, error);
throw error;
}
}
async loadImageAsset(asset) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
this.images.set(asset.name, image);
console.log(`Loaded image: ${asset.name}`);
resolve();
};
image.onerror = () => {
reject(new Error(`Failed to load image: ${asset.url}`));
};
image.src = asset.url;
});
}
async loadAudioAsset(asset) {
// Delegate to audio manager if available
// For now, just store the URL
this.audio.set(asset.name, {
url: asset.url,
isMusic: asset.isMusic,
});
console.log(`Queued audio: ${asset.name}`);
}
async loadFontAsset(asset) {
const font = new FontFace(asset.name, `url(${asset.url})`);
try {
await font.load();
document.fonts.add(font);
this.fonts.set(asset.name, font);
console.log(`Loaded font: ${asset.name}`);
} catch (error) {
throw new Error(`Failed to load font: ${asset.url}`);
}
}
async loadDataAsset(asset) {
try {
const response = await fetch(asset.url);
const data = await response.json();
this.data.set(asset.name, data);
console.log(`Loaded data: ${asset.name}`);
} catch (error) {
throw new Error(`Failed to load data: ${asset.url}`);
}
}
// Get loaded assets
getImage(name) {
const image = this.images.get(name);
if (!image) {
console.warn(`Image "${name}" not found`);
}
return image;
}
getAudio(name) {
const audio = this.audio.get(name);
if (!audio) {
console.warn(`Audio "${name}" not found`);
}
return audio;
}
getFont(name) {
const font = this.fonts.get(name);
if (!font) {
console.warn(`Font "${name}" not found`);
}
return font;
}
getData(name) {
const data = this.data.get(name);
if (!data) {
console.warn(`Data "${name}" not found`);
}
return data;
}
// Check if asset is loaded
isImageLoaded(name) {
return this.images.has(name);
}
isAudioLoaded(name) {
return this.audio.has(name);
}
// Create sprite sheet from loaded image
createSpriteSheet(imageName, frameWidth, frameHeight, frames = null) {
const image = this.getImage(imageName);
if (!image) return null;
const spriteSheet = {
image,
frameWidth,
frameHeight,
framesPerRow: Math.floor(image.width / frameWidth),
totalFrames:
frames ||
Math.floor((image.width / frameWidth) * (image.height / frameHeight)),
frames: [],
};
// Generate frame rectangles
for (let i = 0; i < spriteSheet.totalFrames; i++) {
const col = i % spriteSheet.framesPerRow;
const row = Math.floor(i / spriteSheet.framesPerRow);
spriteSheet.frames.push({
x: col * frameWidth,
y: row * frameHeight,
width: frameWidth,
height: frameHeight,
});
}
return spriteSheet;
}
// Get loading progress
getProgress() {
return {
loaded: this.loadedAssets,
total: this.totalAssets,
percentage:
this.totalAssets > 0 ? this.loadedAssets / this.totalAssets : 0,
isLoading: this.isLoading,
};
}
}
// Usage example
async function initializeAudio() {
const audioManager = new AudioManager();
const assetManager = new AssetManager();
// Set up progress callback
assetManager.onProgress = (progress) => {
console.log(`Loading progress: ${(progress * 100).toFixed(1)}%`);
};
assetManager.onComplete = () => {
console.log('All assets loaded!');
};
// Add some example assets
assetManager
.addImage('player', '/images/player.png')
.addImage('enemy', '/images/enemy.png')
.addAudio('jump', '/audio/jump.wav')
.addAudio('bgm', '/audio/background.mp3', true);
// Load all assets
await assetManager.loadAll();
return { audioManager, assetManager };
}
// Initialize audio system
// initializeAudio().then(({ audioManager, assetManager }) => {
// console.log('Audio and asset systems ready');
// window.audioManager = audioManager;
// window.assetManager = assetManager;
// });
console.log('Game development systems loaded');
Conclusion
JavaScript game development has matured into a powerful platform for creating sophisticated games that run directly in browsers. Through the Canvas API, WebGL, physics engines, and comprehensive audio systems, developers can build everything from simple 2D arcade games to complex 3D experiences. The key to successful game development is understanding performance optimization, implementing efficient game loops, and creating modular, reusable systems.
When building games with JavaScript, focus on creating solid foundations with proper entity-component systems, efficient rendering pipelines, and robust input handling. Implement physics systems thoughtfully, optimize for different devices and browsers, and always consider the user experience across various platforms. Modern web games can achieve near-native performance while providing the accessibility and reach that only web platforms can offer.