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决策系统
性能优化实践
- 应用了对象池等内存管理技术
- 实施了脏矩形等渲染优化策略
- 平衡了功能复杂度与运行效率
未来扩展方向
- 多人模式: 本地双人对战
- 关卡编辑器: 自定义地图功能
- 道具系统: 武器升级和特殊道具
- 存档功能: LocalStorage保存游戏进度
- 移动适配: 触屏控制和响应式设计
总结
这个HTML5 Canvas坦克大战项目成功展示了使用原生Web技术开发完整游戏的可行性。通过精心设计的架构和高效的实现策略,在保持代码简洁的同时实现了丰富的游戏功能。
项目不仅是对经典游戏的致敬,更是现代Web游戏开发技术的实践应用,为轻量级游戏开发提供了完整的解决方案参考。