← Back to Blog

HTML5 Canvas经典坦克大战游戏开发:单文件游戏架构实践

架构特点

  • 单文件设计: 所有代码集中在一个HTML文件中
  • 面向对象: 使用ES6类语法组织代码结构
  • 模块化思维: 清晰的功能分离和职责划分
  • 零依赖: 无需任何外部库或框架

游戏系统设计

核心类结构

// 游戏主类
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() {
        // 键盘事件处理
        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) {
        // 更新玩家
        this.player.update(deltaTime, this.keys);
        
        // 更新敌人
        this.enemies.forEach(enemy => {
            enemy.update(deltaTime, this.player, this.obstacles);
        });
        
        // 更新子弹
        this.bullets.forEach(bullet => {
            bullet.update(deltaTime);
        });
        
        // 处理碰撞
        this.handleCollisions();
        
        // 清理无效对象
        this.cleanup();
        
        // 检查游戏状态
        this.checkGameState();
    }

    render() {
        // 清除画布
        this.ctx.fillStyle = '#000';
        this.ctx.fillRect(0, 0, this.width, this.height);
        
        // 渲染障碍物
        this.obstacles.forEach(obstacle => obstacle.render(this.ctx));
        
        // 渲染坦克
        this.player.render(this.ctx);
        this.enemies.forEach(enemy => enemy.render(this.ctx));
        
        // 渲染子弹
        this.bullets.forEach(bullet => bullet.render(this.ctx));
        
        // 渲染UI
        this.renderUI();
    }
}

坦克类实现

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:上, 1:右, 2:下, 3:左
        this.color = color;
        this.health = 1;
        this.fireRate = 500; // 毫秒
        this.lastFireTime = 0;
    }

    move(direction, deltaTime, obstacles = []) {
        const speed = this.speed * (deltaTime / 1000);
        let newX = this.x;
        let newY = this.y;
        
        // 计算新位置
        switch(direction) {
            case 0: newY -= speed; break; // 上
            case 1: newX += speed; break; // 右
            case 2: newY += speed; break; // 下
            case 3: newX -= speed; break; // 左
        }
        
        // 边界检查
        if (newX < 0 || newX + this.width > 800 ||
            newY < 0 || newY + this.height > 600) {
            return false;
        }
        
        // 碰撞检测
        const tempTank = { 
            x: newX, 
            y: newY, 
            width: this.width, 
            height: this.height 
        };
        
        if (this.checkCollisions(tempTank, obstacles)) {
            return false;
        }
        
        // 更新位置和方向
        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;
        
        // 计算子弹起始位置和方向
        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();
        
        // 移动到坦克中心
        ctx.translate(this.x + this.width/2, this.y + this.height/2);
        
        // 根据方向旋转
        ctx.rotate(this.direction * Math.PI / 2);
        
        // 绘制坦克主体
        ctx.fillStyle = this.color;
        ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
        
        // 绘制炮管
        ctx.fillStyle = '#444';
        ctx.fillRect(-2, -this.height/2 - 8, 4, 12);
        
        // 绘制履带
        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();
        
        // 绘制血量条
        if (this.health < this.maxHealth) {
            this.drawHealthBar(ctx);
        }
    }

    drawHealthBar(ctx) {
        const barWidth = this.width;
        const barHeight = 4;
        const barY = this.y - 8;
        
        // 背景
        ctx.fillStyle = '#FF0000';
        ctx.fillRect(this.x, barY, barWidth, barHeight);
        
        // 血量
        ctx.fillStyle = '#00FF00';
        const healthWidth = (this.health / this.maxHealth) * barWidth;
        ctx.fillRect(this.x, barY, healthWidth, barHeight);
    }
}

// 玩家坦克类
class PlayerTank extends Tank {
    constructor(x, y) {
        super(x, y, '#00FF00');
        this.maxHealth = 3;
        this.health = this.maxHealth;
        this.lives = 3;
    }

    update(deltaTime, keys) {
        // 处理移动输入
        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);
        }
        
        // 处理射击输入
        if (keys['Space']) {
            const bullet = this.fire(Date.now());
            if (bullet) {
                game.bullets.push(bullet);
            }
        }
    }
}

// 敌方坦克类
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; // 随机射击间隔
        this.lastAIUpdate = 0;
    }

    update(deltaTime, player, obstacles) {
        const currentTime = Date.now();
        
        // AI决策更新(降低更新频率以提升性能)
        if (currentTime - this.lastAIUpdate > 100) {
            this.updateAI(deltaTime, player, obstacles);
            this.lastAIUpdate = currentTime;
        }
        
        // 移动
        this.executeMovement(deltaTime, obstacles);
        
        // 射击
        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)
        );
        
        // 状态机
        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 {
                    // 朝向玩家
                    this.targetDirection = this.getDirectionToPlayer(player);
                }
                break;
        }
        
        // 随机改变方向(巡逻状态)
        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; // 右或左
        } else {
            return dy > 0 ? 2 : 0; // 下或上
        }
    }

    executeMovement(deltaTime, obstacles) {
        if (!this.move(this.targetDirection, deltaTime, obstacles)) {
            // 撞墙了,改变方向
            this.targetDirection = Math.floor(Math.random() * 4);
        }
    }

    handleFiring(currentTime, player) {
        if (this.aiState === 'attack' && 
            currentTime - this.lastFireTime > this.fireInterval) {
            
            // 检查是否可以射击到玩家(简单的射线检测)
            if (this.canShootPlayer(player)) {
                const bullet = this.fire(currentTime);
                if (bullet) {
                    game.bullets.push(bullet);
                }
            }
        }
    }

    canShootPlayer(player) {
        // 简化的射线检测
        const dx = player.x - this.x;
        const dy = player.y - this.y;
        
        // 检查玩家是否在射击方向上
        switch(this.direction) {
            case 0: return dy < 0 && Math.abs(dx) < 50; // 上
            case 1: return dx > 0 && Math.abs(dy) < 50; // 右
            case 2: return dy > 0 && Math.abs(dx) < 50; // 下
            case 3: return dx < 0 && Math.abs(dy) < 50; // 左
        }
        return false;
    }
}

子弹系统

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);
        
        // 根据方向移动
        switch(this.direction) {
            case 0: this.y -= speed; break; // 上
            case 1: this.x += speed; break; // 右
            case 2: this.y += speed; break; // 下
            case 3: this.x -= speed; break; // 左
        }
        
        // 边界检查
        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;
    }
}

游戏机制实现

碰撞检测系统

class CollisionManager {
    static handleBulletCollisions(bullets, targets) {
        bullets.forEach(bullet => {
            if (!bullet.active) return;
            
            // 检查与障碍物的碰撞
            game.obstacles.forEach(obstacle => {
                if (bullet.checkCollision(obstacle)) {
                    bullet.active = false;
                    // 可以添加爆炸效果
                    this.createExplosion(bullet.x, bullet.y);
                }
            });
            
            // 检查与坦克的碰撞
            targets.forEach(target => {
                if (bullet.checkCollision(target)) {
                    bullet.active = false;
                    target.health--;
                    
                    // 创建击中效果
                    this.createHitEffect(target.x, target.y);
                    
                    // 检查目标是否被摧毁
                    if (target.health <= 0) {
                        this.destroyTarget(target);
                    }
                }
            });
        });
    }

    static createExplosion(x, y) {
        // 简单的爆炸效果
        const explosion = new ParticleEffect(x, y, 'explosion');
        game.effects.push(explosion);
    }

    static createHitEffect(x, y) {
        // 击中效果
        const hitEffect = new ParticleEffect(x, y, 'hit');
        game.effects.push(hitEffect);
    }

    static destroyTarget(target) {
        if (target instanceof EnemyTank) {
            // 敌方坦克被摧毁
            game.score += 100;
            game.enemiesRemaining--;
            
            // 从数组中移除
            const index = game.enemies.indexOf(target);
            if (index > -1) {
                game.enemies.splice(index, 1);
            }
            
            // 创建爆炸效果
            this.createExplosion(target.x, target.y);
            
        } else if (target instanceof PlayerTank) {
            // 玩家坦克被摧毁
            target.lives--;
            target.health = target.maxHealth;
            
            if (target.lives <= 0) {
                game.gameState = 'gameOver';
            } else {
                // 重置玩家位置
                target.x = 100;
                target.y = 500;
            }
        }
    }
}

关卡系统

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];
        
        // 清除现有敌人
        game.enemies = [];
        game.bullets = [];
        
        // 设置关卡参数
        game.enemiesRemaining = level.enemies;
        game.level = levelNumber;
        
        // 生成敌人
        this.spawnEnemies(level);
        
        // 重置玩家
        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);
            
            // 应用关卡难度
            enemy.speed *= levelConfig.speed;
            enemy.fireInterval /= levelConfig.fireRate;
            
            game.enemies.push(enemy);
        }
        
        // 设置剩余敌人计数
        game.enemiesToSpawn = levelConfig.enemies - game.enemies.length;
    }

    static checkLevelComplete() {
        if (game.enemies.length === 0 && game.enemiesToSpawn === 0) {
            // 关卡完成
            game.level++;
            setTimeout(() => {
                this.loadLevel(game.level);
            }, 2000);
            
            return true;
        }
        return false;
    }
}

UI系统与游戏状态

用户界面渲染

class UIRenderer {
    static render(ctx, game) {
        // 设置字体
        ctx.font = '16px Arial';
        ctx.fillStyle = '#FFFFFF';
        
        // 渲染分数
        ctx.fillText(`Score: ${game.score}`, 10, 25);
        
        // 渲染关卡
        ctx.fillText(`Level: ${game.level}`, 10, 45);
        
        // 渲染生命值
        ctx.fillText(`Lives: ${game.player.lives}`, 10, 65);
        
        // 渲染剩余敌人
        ctx.fillText(`Enemies: ${game.enemiesRemaining}`, 10, 85);
        
        // 渲染游戏状态信息
        if (game.gameState === 'paused') {
            this.renderPauseScreen(ctx);
        } else if (game.gameState === 'gameOver') {
            this.renderGameOverScreen(ctx);
        }
    }

    static renderPauseScreen(ctx) {
        // 半透明背景
        ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
        ctx.fillRect(0, 0, 800, 600);
        
        // 暂停文字
        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) {
        // 半透明背景
        ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
        ctx.fillRect(0, 0, 800, 600);
        
        // 游戏结束文字
        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';
    }
}

性能优化策略

1. 对象池模式

class ObjectPool {
    constructor(createFn, resetFn, initialSize = 10) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        this.pool = [];
        
        // 预创建对象
        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);
    }
}

// 使用示例
const bulletPool = new ObjectPool(
    () => new Bullet(0, 0, 0, null),
    (bullet) => { bullet.active = false; },
    20
);

2. 渲染优化

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;
    }
}

项目特色

1. 单文件架构优势

  • 简单部署: 只需一个HTML文件即可运行
  • 零依赖: 无需任何外部库或框架
  • 易于分享: 方便代码阅读和学习
  • 快速加载: 减少HTTP请求

2. 经典游戏机制

  • 即时响应: 流畅的实时控制体验
  • 智能AI: 敌方坦克具备基本战术AI
  • 渐进难度: 多关卡难度递增设计
  • 视觉反馈: 丰富的击中和爆炸效果

3. 可扩展设计

  • 模块化代码: 清晰的类结构便于扩展
  • 事件系统: 支持自定义游戏事件
  • 配置驱动: 关卡数据与逻辑分离

技术收获

Canvas 2D编程

  • 掌握了Canvas绘图API的高级用法
  • 理解了游戏渲染循环的优化策略
  • 学会了图形变换和动画实现

游戏开发基础

  • 实现了完整的游戏循环架构
  • 设计了高效的碰撞检测系统
  • 构建了状态管理和AI决策系统

性能优化实践

  • 应用了对象池等内存管理技术
  • 实施了脏矩形等渲染优化策略
  • 平衡了功能复杂度与运行效率

未来扩展方向

  1. 多人模式: 本地双人对战
  2. 关卡编辑器: 自定义地图功能
  3. 道具系统: 武器升级和特殊道具
  4. 存档功能: LocalStorage保存游戏进度
  5. 移动适配: 触屏控制和响应式设计

总结

这个HTML5 Canvas坦克大战项目成功展示了使用原生Web技术开发完整游戏的可行性。通过精心设计的架构和高效的实现策略,在保持代码简洁的同时实现了丰富的游戏功能。

项目不仅是对经典游戏的致敬,更是现代Web游戏开发技术的实践应用,为轻量级游戏开发提供了完整的解决方案参考。