HTML5 Canvas Classic Tank Battle Game Development: Single-File Game Architecture Practice
Architecture Characteristics
- Single-file Design: All code centralized in one HTML file
- Object-oriented: Using ES6 class syntax for code structure organization
- Modular Thinking: Clear functional separation and responsibility division
- Zero Dependencies: No external libraries or frameworks required
Game System Design
Core Class Structure
// Main game class
class TankWarGame {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.width = 800;
this.height = 600;
this.player = new PlayerTank(100, 500);
this.enemies = [];
this.bullets = [];
this.obstacles = [];
this.gameState = 'playing'; // playing, paused, gameOver
this.score = 0;
this.level = 1;
this.enemiesRemaining = 20;
this.keys = {};
this.lastTime = 0;
this.init();
}
init() {
this.setupCanvas();
this.bindEvents();
this.createObstacles();
this.spawnEnemies();
this.gameLoop();
}
setupCanvas() {
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.style.border = '2px solid #333';
this.canvas.style.background = '#000';
}
bindEvents() {
// Keyboard event handling
document.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
});
document.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
}
gameLoop(currentTime = 0) {
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
if (this.gameState === 'playing') {
this.update(deltaTime);
}
this.render();
requestAnimationFrame((time) => this.gameLoop(time));
}
update(deltaTime) {
// Update player
this.player.update(deltaTime, this.keys);
// Update enemies
this.enemies.forEach(enemy => {
enemy.update(deltaTime, this.player, this.obstacles);
});
// Update bullets
this.bullets.forEach(bullet => {
bullet.update(deltaTime);
});
// Handle collisions
this.handleCollisions();
// Clean up invalid objects
this.cleanup();
// Check game state
this.checkGameState();
}
render() {
// Clear canvas
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.width, this.height);
// Render obstacles
this.obstacles.forEach(obstacle => obstacle.render(this.ctx));
// Render tanks
this.player.render(this.ctx);
this.enemies.forEach(enemy => enemy.render(this.ctx));
// Render bullets
this.bullets.forEach(bullet => bullet.render(this.ctx));
// Render UI
this.renderUI();
}
}
Tank Class Implementation
class Tank {
constructor(x, y, color = '#00FF00') {
this.x = x;
this.y = y;
this.width = 32;
this.height = 32;
this.speed = 100; // pixels per second
this.direction = 0; // 0:up, 1:right, 2:down, 3:left
this.color = color;
this.health = 1;
this.fireRate = 500; // milliseconds
this.lastFireTime = 0;
}
move(direction, deltaTime, obstacles = []) {
const speed = this.speed * (deltaTime / 1000);
let newX = this.x;
let newY = this.y;
// Calculate new position
switch(direction) {
case 0: newY -= speed; break; // up
case 1: newX += speed; break; // right
case 2: newY += speed; break; // down
case 3: newX -= speed; break; // left
}
// Boundary check
if (newX < 0 || newX + this.width > 800 ||
newY < 0 || newY + this.height > 600) {
return false;
}
// Collision detection
const tempTank = {
x: newX,
y: newY,
width: this.width,
height: this.height
};
if (this.checkCollisions(tempTank, obstacles)) {
return false;
}
// Update position and direction
this.x = newX;
this.y = newY;
this.direction = direction;
return true;
}
checkCollisions(rect, obstacles) {
return obstacles.some(obstacle =>
this.isColliding(rect, obstacle)
);
}
isColliding(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
fire(currentTime) {
if (currentTime - this.lastFireTime < this.fireRate) {
return null;
}
this.lastFireTime = currentTime;
// Calculate bullet starting position and direction
let bulletX = this.x + this.width / 2;
let bulletY = this.y + this.height / 2;
return new Bullet(bulletX, bulletY, this.direction, this);
}
render(ctx) {
ctx.save();
// Move to tank center
ctx.translate(this.x + this.width/2, this.y + this.height/2);
// Rotate based on direction
ctx.rotate(this.direction * Math.PI / 2);
// Draw tank body
ctx.fillStyle = this.color;
ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// Draw cannon
ctx.fillStyle = '#444';
ctx.fillRect(-2, -this.height/2 - 8, 4, 12);
// Draw tracks
ctx.fillStyle = '#333';
ctx.fillRect(-this.width/2 - 2, -this.height/2, 2, this.height);
ctx.fillRect(this.width/2, -this.height/2, 2, this.height);
ctx.restore();
// Draw health bar
if (this.health < this.maxHealth) {
this.drawHealthBar(ctx);
}
}
drawHealthBar(ctx) {
const barWidth = this.width;
const barHeight = 4;
const barY = this.y - 8;
// Background
ctx.fillStyle = '#FF0000';
ctx.fillRect(this.x, barY, barWidth, barHeight);
// Health
ctx.fillStyle = '#00FF00';
const healthWidth = (this.health / this.maxHealth) * barWidth;
ctx.fillRect(this.x, barY, healthWidth, barHeight);
}
}
// Player tank class
class PlayerTank extends Tank {
constructor(x, y) {
super(x, y, '#00FF00');
this.maxHealth = 3;
this.health = this.maxHealth;
this.lives = 3;
}
update(deltaTime, keys) {
// Handle movement input
if (keys['ArrowUp'] || keys['KeyW']) {
this.move(0, deltaTime, game.obstacles);
}
if (keys['ArrowRight'] || keys['KeyD']) {
this.move(1, deltaTime, game.obstacles);
}
if (keys['ArrowDown'] || keys['KeyS']) {
this.move(2, deltaTime, game.obstacles);
}
if (keys['ArrowLeft'] || keys['KeyA']) {
this.move(3, deltaTime, game.obstacles);
}
// Handle shooting input
if (keys['Space']) {
const bullet = this.fire(Date.now());
if (bullet) {
game.bullets.push(bullet);
}
}
}
}
// Enemy tank class
class EnemyTank extends Tank {
constructor(x, y) {
super(x, y, '#FF0000');
this.aiState = 'patrol';
this.targetDirection = Math.floor(Math.random() * 4);
this.stateChangeTime = 0;
this.moveTime = 0;
this.fireInterval = 1000 + Math.random() * 2000; // Random firing interval
this.lastAIUpdate = 0;
}
update(deltaTime, player, obstacles) {
const currentTime = Date.now();
// AI decision update (reduce update frequency for performance)
if (currentTime - this.lastAIUpdate > 100) {
this.updateAI(deltaTime, player, obstacles);
this.lastAIUpdate = currentTime;
}
// Movement
this.executeMovement(deltaTime, obstacles);
// Firing
this.handleFiring(currentTime, player);
}
updateAI(deltaTime, player, obstacles) {
const distanceToPlayer = Math.sqrt(
Math.pow(player.x - this.x, 2) +
Math.pow(player.y - this.y, 2)
);
// State machine
switch(this.aiState) {
case 'patrol':
if (distanceToPlayer < 150) {
this.aiState = 'attack';
this.targetDirection = this.getDirectionToPlayer(player);
}
break;
case 'attack':
if (distanceToPlayer > 200) {
this.aiState = 'patrol';
} else {
// Face player
this.targetDirection = this.getDirectionToPlayer(player);
}
break;
}
// Random direction change (patrol state)
if (this.aiState === 'patrol' && Math.random() < 0.02) {
this.targetDirection = Math.floor(Math.random() * 4);
}
}
getDirectionToPlayer(player) {
const dx = player.x - this.x;
const dy = player.y - this.y;
if (Math.abs(dx) > Math.abs(dy)) {
return dx > 0 ? 1 : 3; // right or left
} else {
return dy > 0 ? 2 : 0; // down or up
}
}
executeMovement(deltaTime, obstacles) {
if (!this.move(this.targetDirection, deltaTime, obstacles)) {
// Hit a wall, change direction
this.targetDirection = Math.floor(Math.random() * 4);
}
}
handleFiring(currentTime, player) {
if (this.aiState === 'attack' &&
currentTime - this.lastFireTime > this.fireInterval) {
// Check if can shoot player (simple ray casting)
if (this.canShootPlayer(player)) {
const bullet = this.fire(currentTime);
if (bullet) {
game.bullets.push(bullet);
}
}
}
}
canShootPlayer(player) {
// Simplified ray casting
const dx = player.x - this.x;
const dy = player.y - this.y;
// Check if player is in firing direction
switch(this.direction) {
case 0: return dy < 0 && Math.abs(dx) < 50; // up
case 1: return dx > 0 && Math.abs(dy) < 50; // right
case 2: return dy > 0 && Math.abs(dx) < 50; // down
case 3: return dx < 0 && Math.abs(dy) < 50; // left
}
return false;
}
}
Bullet System
class Bullet {
constructor(x, y, direction, owner) {
this.x = x;
this.y = y;
this.width = 4;
this.height = 4;
this.speed = 300;
this.direction = direction;
this.owner = owner;
this.active = true;
}
update(deltaTime) {
if (!this.active) return;
const speed = this.speed * (deltaTime / 1000);
// Move based on direction
switch(this.direction) {
case 0: this.y -= speed; break; // up
case 1: this.x += speed; break; // right
case 2: this.y += speed; break; // down
case 3: this.x -= speed; break; // left
}
// Boundary check
if (this.x < 0 || this.x > 800 || this.y < 0 || this.y > 600) {
this.active = false;
}
}
render(ctx) {
if (!this.active) return;
ctx.fillStyle = '#FFFF00';
ctx.fillRect(this.x - this.width/2, this.y - this.height/2,
this.width, this.height);
}
checkCollision(target) {
if (!this.active || this.owner === target) return false;
return this.x < target.x + target.width &&
this.x + this.width > target.x &&
this.y < target.y + target.height &&
this.y + this.height > target.y;
}
}
Game Mechanics Implementation
Collision Detection System
class CollisionManager {
static handleBulletCollisions(bullets, targets) {
bullets.forEach(bullet => {
if (!bullet.active) return;
// Check collision with obstacles
game.obstacles.forEach(obstacle => {
if (bullet.checkCollision(obstacle)) {
bullet.active = false;
// Can add explosion effect
this.createExplosion(bullet.x, bullet.y);
}
});
// Check collision with tanks
targets.forEach(target => {
if (bullet.checkCollision(target)) {
bullet.active = false;
target.health--;
// Create hit effect
this.createHitEffect(target.x, target.y);
// Check if target is destroyed
if (target.health <= 0) {
this.destroyTarget(target);
}
}
});
});
}
static createExplosion(x, y) {
// Simple explosion effect
const explosion = new ParticleEffect(x, y, 'explosion');
game.effects.push(explosion);
}
static createHitEffect(x, y) {
// Hit effect
const hitEffect = new ParticleEffect(x, y, 'hit');
game.effects.push(hitEffect);
}
static destroyTarget(target) {
if (target instanceof EnemyTank) {
// Enemy tank destroyed
game.score += 100;
game.enemiesRemaining--;
// Remove from array
const index = game.enemies.indexOf(target);
if (index > -1) {
game.enemies.splice(index, 1);
}
// Create explosion effect
this.createExplosion(target.x, target.y);
} else if (target instanceof PlayerTank) {
// Player tank destroyed
target.lives--;
target.health = target.maxHealth;
if (target.lives <= 0) {
game.gameState = 'gameOver';
} else {
// Reset player position
target.x = 100;
target.y = 500;
}
}
}
}
Level System
class LevelManager {
static levels = [
{ enemies: 5, speed: 1.0, fireRate: 1.0 },
{ enemies: 8, speed: 1.2, fireRate: 1.2 },
{ enemies: 12, speed: 1.4, fireRate: 1.4 },
{ enemies: 16, speed: 1.6, fireRate: 1.6 },
{ enemies: 20, speed: 1.8, fireRate: 1.8 }
];
static loadLevel(levelNumber) {
const level = this.levels[levelNumber - 1] || this.levels[this.levels.length - 1];
// Clear existing enemies
game.enemies = [];
game.bullets = [];
// Set level parameters
game.enemiesRemaining = level.enemies;
game.level = levelNumber;
// Spawn enemies
this.spawnEnemies(level);
// Reset player
game.player.x = 100;
game.player.y = 500;
game.player.health = game.player.maxHealth;
}
static spawnEnemies(levelConfig) {
const spawnPositions = [
{ x: 50, y: 50 },
{ x: 400, y: 50 },
{ x: 750, y: 50 },
{ x: 750, y: 300 },
{ x: 400, y: 300 }
];
for (let i = 0; i < Math.min(5, levelConfig.enemies); i++) {
const pos = spawnPositions[i];
const enemy = new EnemyTank(pos.x, pos.y);
// Apply level difficulty
enemy.speed *= levelConfig.speed;
enemy.fireInterval /= levelConfig.fireRate;
game.enemies.push(enemy);
}
// Set remaining enemy count
game.enemiesToSpawn = levelConfig.enemies - game.enemies.length;
}
static checkLevelComplete() {
if (game.enemies.length === 0 && game.enemiesToSpawn === 0) {
// Level complete
game.level++;
setTimeout(() => {
this.loadLevel(game.level);
}, 2000);
return true;
}
return false;
}
}
UI System and Game State
User Interface Rendering
class UIRenderer {
static render(ctx, game) {
// Set font
ctx.font = '16px Arial';
ctx.fillStyle = '#FFFFFF';
// Render score
ctx.fillText(`Score: ${game.score}`, 10, 25);
// Render level
ctx.fillText(`Level: ${game.level}`, 10, 45);
// Render lives
ctx.fillText(`Lives: ${game.player.lives}`, 10, 65);
// Render remaining enemies
ctx.fillText(`Enemies: ${game.enemiesRemaining}`, 10, 85);
// Render game state information
if (game.gameState === 'paused') {
this.renderPauseScreen(ctx);
} else if (game.gameState === 'gameOver') {
this.renderGameOverScreen(ctx);
}
}
static renderPauseScreen(ctx) {
// Semi-transparent background
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, 800, 600);
// Pause text
ctx.font = '48px Arial';
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'center';
ctx.fillText('PAUSED', 400, 300);
ctx.font = '24px Arial';
ctx.fillText('Press P to continue', 400, 350);
ctx.textAlign = 'left';
}
static renderGameOverScreen(ctx) {
// Semi-transparent background
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(0, 0, 800, 600);
// Game over text
ctx.font = '48px Arial';
ctx.fillStyle = '#FF0000';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', 400, 250);
ctx.font = '24px Arial';
ctx.fillStyle = '#FFFFFF';
ctx.fillText(`Final Score: ${game.score}`, 400, 300);
ctx.fillText('Press R to restart', 400, 350);
ctx.textAlign = 'left';
}
}
Performance Optimization Strategies
1. Object Pool Pattern
class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
// Pre-create objects
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}
get() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}
// Usage example
const bulletPool = new ObjectPool(
() => new Bullet(0, 0, 0, null),
(bullet) => { bullet.active = false; },
20
);
2. Rendering Optimization
class RenderOptimizer {
static dirtyRectangles = [];
static addDirtyRect(x, y, width, height) {
this.dirtyRectangles.push({ x, y, width, height });
}
static clearDirtyRects(ctx) {
this.dirtyRectangles.forEach(rect => {
ctx.clearRect(rect.x, rect.y, rect.width, rect.height);
});
this.dirtyRectangles = [];
}
static isInViewport(obj, viewX, viewY, viewWidth, viewHeight) {
return obj.x + obj.width >= viewX &&
obj.x <= viewX + viewWidth &&
obj.y + obj.height >= viewY &&
obj.y <= viewY + viewHeight;
}
}
Project Features
1. Single-file Architecture Advantages
- Simple Deployment: Only one HTML file needed to run
- Zero Dependencies: No external libraries or frameworks required
- Easy Sharing: Convenient for code reading and learning
- Fast Loading: Reduced HTTP requests
2. Classic Game Mechanics
- Instant Response: Smooth real-time control experience
- Intelligent AI: Enemy tanks with basic tactical AI
- Progressive Difficulty: Multi-level difficulty escalation design
- Visual Feedback: Rich hit and explosion effects
3. Extensible Design
- Modular Code: Clear class structure for easy extension
- Event System: Support for custom game events
- Configuration-driven: Level data separated from logic
Technical Insights
Canvas 2D Programming
- Mastered advanced usage of Canvas drawing API
- Understood optimization strategies for game rendering loops
- Learned graphic transformation and animation implementation
Game Development Fundamentals
- Implemented complete game loop architecture
- Designed efficient collision detection system
- Built state management and AI decision system
Performance Optimization Practice
- Applied memory management techniques like object pools
- Implemented rendering optimizations like dirty rectangles
- Balanced functional complexity with runtime efficiency
Future Extension Directions
- Multiplayer Mode: Local two-player battles
- Level Editor: Custom map functionality
- Power-up System: Weapon upgrades and special items
- Save Function: LocalStorage for game progress
- Mobile Adaptation: Touch controls and responsive design
Summary
This HTML5 Canvas tank battle project successfully demonstrates the feasibility of developing complete games using native Web technologies. Through carefully designed architecture and efficient implementation strategies, it achieves rich game functionality while maintaining code simplicity.
The project is not only a tribute to classic games but also a practical application of modern Web game development technologies, providing a complete solution reference for lightweight game development.