1. 项目概述:用前端三件套打造经典飞机大战游戏
作为一名前端开发者,我经常思考如何将基础技术转化为实际应用。飞机大战这个经典游戏项目,恰好能完美串联HTML、CSS和JavaScript的核心知识点。不同于简单的静态页面,游戏开发要求我们处理动态元素、碰撞检测、状态管理等复杂场景,这正是检验前端基本功的绝佳试金石。
这个项目特别适合两类开发者:刚学完前端三件套想找项目练手的新人,以及希望深入理解JavaScript在游戏领域应用的中级开发者。通过构建完整的游戏循环,你将掌握如何用requestAnimationFrame实现流畅动画、用事件监听处理用户输入、用面向对象思想组织代码——这些技能在Web开发中同样至关重要。
完整代码已附在文末,但建议你先跟随本文思路自己实现一遍。我们会从最基础的飞机移动开始,逐步添加敌机生成、碰撞检测、分数计算等功能,最终形成一个可玩性完整的游戏。过程中遇到的每个技术难点,都是提升前端能力的宝贵机会。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 游戏架构设计与核心实现
2.1 HTML骨架与CSS样式设计
游戏界面的HTML结构需要精心设计以保证良好的扩展性。我们采用分层结构,用多个div分别承载背景、玩家飞机、敌机、子弹等元素:
html复制<div id="game-container">
<div id="background"></div>
<div id="player" class="entity"></div>
<div id="enemies-container"></div>
<div id="bullets-container"></div>
<div id="score-display">Score: 0</div>
</div>
CSS部分需要特别注意性能优化。游戏元素使用绝对定位(position: absolute)实现自由移动,避免触发页面重排。对于需要频繁变化的属性(如top/left),我们使用transform属性实现硬件加速:
css复制.entity {
position: absolute;
will-change: transform; /* 提示浏览器优化 */
transition: transform 0.1s linear;
}
#player {
width: 50px;
height: 50px;
background-image: url('player.png');
background-size: contain;
z-index: 100;
}
.enemy {
width: 40px;
height: 40px;
background-image: url('enemy.png');
background-size: contain;
}
关键技巧:使用CSS的will-change属性预先告知浏览器哪些元素会频繁变化,让浏览器提前做好优化准备。同时避免使用box-shadow等耗性能的属性。
2.2 JavaScript游戏主循环实现
游戏的核心是循环更新所有对象状态并重绘画面。现代浏览器推荐使用requestAnimationFrame实现动画循环,它能保证与显示器刷新率同步,避免卡顿:
javascript复制let lastTime = 0;
function gameLoop(timestamp) {
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
update(deltaTime);
render();
requestAnimationFrame(gameLoop);
}
function update(deltaTime) {
updatePlayer();
updateEnemies(deltaTime);
updateBullets();
checkCollisions();
}
function render() {
// 实际项目中这里可能不需要额外操作
// 因为DOM变化会自动触发重绘
}
deltaTime参数至关重要,它表示上一帧到当前帧的时间差(毫秒)。用这个值来计算移动距离,可以保证在不同刷新率的设备上游戏速度一致:
javascript复制function updatePlayer() {
if (keys.ArrowLeft) {
player.x -= player.speed * (deltaTime / 16);
}
// 其他方向处理...
}
2.3 玩家控制与输入处理
流畅的操控体验是游戏成功的关键。我们需要监听键盘事件,但要注意避免默认行为(如方向键滚动页面):
javascript复制const keys = {};
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
// 阻止方向键默认行为
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
对于移动端支持,可以添加触摸事件处理。核心思路是记录触摸位置与玩家中心的偏移量:
javascript复制let touchOffsetX = 0;
let touchOffsetY = 0;
playerElement.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
touchOffsetX = touch.clientX - player.x;
touchOffsetY = touch.clientY - player.y;
});
window.addEventListener('touchmove', (e) => {
const touch = e.touches[0];
player.x = touch.clientX - touchOffsetX;
player.y = touch.clientY - touchOffsetY;
e.preventDefault();
});
3. 游戏核心机制实现
3.1 敌机生成与移动逻辑
敌机系统需要管理生成频率、移动路径和类型变化。我们使用对象池模式优化性能,避免频繁创建销毁DOM元素:
javascript复制class EnemyPool {
constructor() {
this.pool = [];
this.activeEnemies = [];
}
getEnemy() {
let enemy = this.pool.find(e => !e.active);
if (!enemy) {
enemy = createNewEnemy();
this.pool.push(enemy);
}
enemy.active = true;
this.activeEnemies.push(enemy);
return enemy;
}
update(deltaTime) {
this.activeEnemies.forEach(enemy => {
enemy.y += enemy.speed * (deltaTime / 16);
if (enemy.y > canvasHeight) {
this.releaseEnemy(enemy);
}
});
}
}
敌机生成算法需要考虑游戏难度曲线。随着分数增加,我们提高生成频率并增加敌机类型:
javascript复制function spawnEnemy() {
const score = getScore();
const spawnInterval = Math.max(500, 1000 - score * 2);
if (Date.now() - lastSpawnTime > spawnInterval) {
const enemy = enemyPool.getEnemy();
enemy.x = Math.random() * (canvasWidth - enemy.width);
enemy.y = -enemy.height;
enemy.type = score > 1000 ? 'advanced' : 'basic';
lastSpawnTime = Date.now();
}
}
3.2 子弹系统与碰撞检测
子弹系统需要处理发射频率、移动轨迹和碰撞检测。我们同样使用对象池管理子弹:
javascript复制class BulletSystem {
constructor() {
this.cooldown = 0;
this.bullets = [];
}
shoot(playerX, playerY) {
if (this.cooldown <= 0) {
const bullet = createBullet(playerX, playerY);
this.bullets.push(bullet);
this.cooldown = 300; // 发射冷却时间(ms)
}
}
update(deltaTime) {
this.cooldown -= deltaTime;
this.bullets = this.bullets.filter(bullet => {
bullet.y -= bullet.speed * (deltaTime / 16);
return bullet.y > -bullet.height;
});
}
}
碰撞检测采用轴对齐边界框(AABB)算法,这是2D游戏最常用的简单碰撞检测方法:
javascript复制function checkCollision(obj1, obj2) {
return obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y;
}
function checkCollisions() {
bullets.forEach(bullet => {
enemies.forEach(enemy => {
if (checkCollision(bullet, enemy)) {
handleEnemyHit(enemy, bullet);
}
});
});
// 玩家与敌机碰撞检测
enemies.forEach(enemy => {
if (checkCollision(player, enemy)) {
handlePlayerHit();
}
});
}
3.3 游戏状态与分数系统
良好的状态管理让游戏逻辑更清晰。我们使用有限状态机(FSM)管理游戏流程:
javascript复制const GameState = {
MENU: 0,
PLAYING: 1,
GAME_OVER: 2
};
let currentState = GameState.MENU;
function update(deltaTime) {
switch (currentState) {
case GameState.PLAYING:
updateGame(deltaTime);
break;
case GameState.GAME_OVER:
updateGameOver();
break;
}
}
分数系统需要考虑连击奖励和难度系数:
javascript复制let score = 0;
let combo = 0;
let lastHitTime = 0;
function addScore(points) {
const now = Date.now();
if (now - lastHitTime < 2000) { // 2秒内连续击中
combo++;
points *= Math.min(3, 1 + combo * 0.2);
} else {
combo = 0;
}
score += Math.floor(points);
lastHitTime = now;
updateScoreDisplay();
}
4. 性能优化与高级技巧
4.1 渲染性能优化策略
当游戏对象增多时,DOM操作可能成为性能瓶颈。我们可以采用以下优化手段:
- 复合层优化:为频繁移动的元素添加CSS属性,使其提升到单独的复合层
css复制.entity {
transform: translateZ(0);
backface-visibility: hidden;
}
- 离屏Canvas渲染:对于复杂场景,可以先用Canvas绘制,再转为图片显示
javascript复制const bufferCanvas = document.createElement('canvas');
const bufferCtx = bufferCanvas.getContext('2d');
function renderToBuffer() {
bufferCtx.clearRect(0, 0, width, height);
// 绘制所有元素到bufferCanvas
// ...
return bufferCanvas;
}
// 主渲染循环中
gameContainer.style.backgroundImage = `url(${renderToBuffer().toDataURL()})`;
- 对象池模式:前面提到的对象池实现可以大幅减少GC压力
4.2 游戏平衡性调整
好玩的游戏需要精心调整参数。我们可以建立一个配置对象集中管理游戏参数:
javascript复制const GameConfig = {
difficultyCurve: [
{ score: 0, spawnRate: 1.0, enemySpeed: 1.0, enemyHealth: 1 },
{ score: 500, spawnRate: 1.3, enemySpeed: 1.2, enemyHealth: 1 },
{ score: 1000, spawnRate: 1.6, enemySpeed: 1.5, enemyHealth: 2 }
],
getCurrentConfig(score) {
let config = this.difficultyCurve[0];
for (let i = 1; i < this.difficultyCurve.length; i++) {
if (score >= this.difficultyCurve[i].score) {
config = this.difficultyCurve[i];
}
}
return config;
}
};
4.3 特效与音效增强
简单的特效可以大幅提升游戏体验。例如爆炸动画可以用CSS动画实现:
css复制@keyframes explode {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
.explosion {
position: absolute;
background-image: url('explosion.png');
animation: explode 0.5s forwards;
pointer-events: none;
}
音效系统需要注意移动端限制(很多浏览器要求用户交互后才能播放声音):
javascript复制const audioContext = new (window.AudioContext || window.webkitAudioContext)();
function playSound(frequency, duration) {
if (audioContext.state === 'suspended') {
audioContext.resume();
}
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'square';
oscillator.frequency.value = frequency;
gainNode.gain.exponentialRampToValueAtTime(
0.0001, audioContext.currentTime + duration
);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
}
5. 完整代码实现与扩展思路
5.1 项目完整代码结构
以下是完整的HTML文件结构,包含所有核心功能:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>飞机大战</title>
<style>
/* 所有CSS样式 */
body { margin: 0; overflow: hidden; }
#game-container {
position: relative;
width: 100vw;
height: 100vh;
background: #000 url('stars.png') repeat;
}
/* 其他样式规则... */
</style>
</head>
<body>
<div id="game-container">
<!-- 游戏元素将通过JavaScript动态生成 -->
</div>
<script>
// 游戏配置
const Config = { /* ... */ };
// 游戏状态
let state = { /* ... */ };
// 游戏对象
class Player { /* ... */ }
class Enemy { /* ... */ }
class Bullet { /* ... */ }
// 对象池
class ObjectPool { /* ... */ }
// 游戏系统
class InputSystem { /* ... */ }
class CollisionSystem { /* ... */ }
class RenderSystem { /* ... */ }
// 主游戏类
class Game {
constructor() { /* 初始化所有系统 */ }
start() { /* 开始游戏循环 */ }
update(deltaTime) { /* 更新游戏状态 */ }
render() { /* 渲染游戏画面 */ }
}
// 启动游戏
const game = new Game();
game.start();
</script>
</body>
</html>
5.2 项目扩展方向
完成基础版本后,可以考虑以下扩展方向:
- 多关卡系统:设计不同关卡,每关有独特的敌机组合和BOSS战
- 技能系统:玩家可以积累能量释放特殊技能
- 本地存储:使用localStorage保存最高分和游戏设置
- 多人模式:通过WebSocket实现双人合作或对战
- WebAssembly优化:将性能敏感部分用Rust等语言编写,编译为WebAssembly运行
javascript复制// 示例:使用localStorage保存最高分
function saveHighScore(score) {
const highScore = localStorage.getItem('highScore') || 0;
if (score > highScore) {
localStorage.setItem('highScore', score);
}
}
// 示例:WebSocket多人游戏基础
const socket = new WebSocket('wss://game-server.example.com');
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'playerUpdate') {
updateOtherPlayer(data);
}
};
function sendPlayerState() {
socket.send(JSON.stringify({
type: 'playerUpdate',
x: player.x,
y: player.y
}));
}
5.3 调试与问题排查技巧
开发过程中可能会遇到以下典型问题及解决方案:
-
动画卡顿:
- 检查是否使用了性能差的CSS属性(如box-shadow)
- 使用Chrome DevTools的Performance面板分析帧率
- 确保没有不必要的DOM重排
-
碰撞检测不准确:
- 添加调试绘制显示碰撞框
- 检查对象尺寸是否与视觉表现一致
- 考虑使用更精确的碰撞检测算法(如圆形碰撞)
-
移动端触摸延迟:
- 添加touch-action: none CSS属性
- 使用fastclick库消除300ms延迟
- 考虑使用Pointer Events代替Touch Events
javascript复制// 调试碰撞框绘制
function drawDebugColliders() {
const debugElements = [];
function drawBox(obj, color) {
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.border = `1px solid ${color}`;
div.style.pointerEvents = 'none';
div.style.left = `${obj.x}px`;
div.style.top = `${obj.y}px`;
div.style.width = `${obj.width}px`;
div.style.height = `${obj.height}px`;
gameContainer.appendChild(div);
debugElements.push(div);
}
// 每帧清除旧调试元素
debugElements.forEach(el => el.remove());
debugElements.length = 0;
// 绘制所有碰撞框
drawBox(player, 'green');
enemies.forEach(e => drawBox(e, 'red'));
bullets.forEach(b => drawBox(b, 'blue'));
}
6. 实战经验与避坑指南
6.1 常见问题解决方案
问题1:游戏在后台标签页运行时速度异常
这是因为浏览器会降低后台标签页的requestAnimationFrame回调频率。解决方案是使用deltaTime计算移动距离,或者监听visibilitychange事件暂停游戏:
javascript复制document.addEventListener('visibilitychange', () => {
if (document.hidden) {
pauseGame();
} else {
resumeGame();
}
});
问题2:移动端点击延迟
移动浏览器通常有300ms的点击延迟来判断是否是双击。解决方案:
- 在meta标签中设置user-scalable=no(不推荐,影响可访问性)
- 使用touch事件代替click事件
- 引入fastclick库
问题3:游戏画面撕裂
当绘制复杂场景时可能出现画面撕裂。解决方案:
- 使用双缓冲技术
- 将频繁变化的元素放在单独的复合层
- 减少每帧需要重绘的区域
6.2 性能优化实战技巧
-
减少DOM操作:
- 批量修改样式而非逐个修改
- 使用文档片段(document.createDocumentFragment())批量添加节点
- 对隐藏元素进行操作(display: none)
-
内存管理:
- 及时移除不再需要的事件监听器
- 对于频繁创建销毁的对象使用对象池
- 避免在动画循环中创建新对象
-
节流高频操作:
- 对resize、scroll等高频事件进行节流
- 限制每帧的物理计算次数
- 对非关键操作使用setTimeout分帧执行
javascript复制// 示例:节流高频事件
function throttle(fn, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
fn.apply(this, args);
lastCall = now;
}
};
}
window.addEventListener('resize', throttle(handleResize, 200));
6.3 跨浏览器兼容性处理
不同浏览器对某些API的实现可能有差异,需要特别注意:
-
requestAnimationFrame:
javascript复制const requestAnimFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { return window.setTimeout(callback, 1000/60); }; -
全屏API:
javascript复制function requestFullscreen(element) { if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } } -
音频自动播放策略:
- 大多数浏览器要求音频必须在用户交互后播放
- 解决方案是添加一个"点击开始"按钮,在点击事件中初始化音频
javascript复制// 示例:安全的音频播放
const audioElements = [];
function initAudio() {
// 预加载所有音效
const sounds = ['shoot', 'explosion', 'powerup'];
sounds.forEach(name => {
const audio = new Audio(`sounds/${name}.mp3`);
audio.load();
audioElements[name] = audio;
});
}
function playSound(name) {
if (audioElements[name]) {
const clone = audioElements[name].cloneNode();
clone.play().catch(e => console.log('Audio play failed:', e));
}
}
// 在用户交互后调用initAudio()
startButton.addEventListener('click', () => {
initAudio();
startGame();
});
7. 完整项目代码
以下是精简后的完整代码实现,包含所有核心功能:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>飞机大战</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
#game-container {
position: relative; width: 100vw; height: 100vh;
background: #000 url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 800 800"><g fill="rgba(255,255,255,0.6)"><circle cx="400" cy="400" r="1"/><circle cx="200" cy="200" r="1"/><circle cx="600" cy="150" r="1"/><circle cx="300" cy="600" r="1"/><circle cx="700" cy="450" r="1"/><circle cx="100" cy="350" r="1"/><circle cx="500" cy="700" r="1"/></g></svg>');
overflow: hidden;
}
.entity {
position: absolute; will-change: transform;
background-size: contain; background-repeat: no-repeat;
}
#player {
width: 50px; height: 50px; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon points="50,0 100,100 50,80 0,100" fill="%2333ccff"/></svg>');
z-index: 100;
}
.enemy {
width: 40px; height: 40px; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="%23ff3333"/></svg>');
}
.bullet {
width: 4px; height: 16px; background-color: yellow;
}
#score-display {
position: absolute; top: 10px; right: 10px;
color: white; font-family: Arial; font-size: 20px;
text-shadow: 1px 1px 2px black;
}
#start-screen {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7); color: white;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
font-family: Arial; z-index: 200;
}
#start-button {
margin-top: 20px; padding: 10px 20px;
background: #4CAF50; color: white;
border: none; border-radius: 5px;
font-size: 18px; cursor: pointer;
}
</style>
</head>
<body>
<div id="game-container">
<div id="player" class="entity"></div>
<div id="enemies-container"></div>
<div id="bullets-container"></div>
<div id="score-display">Score: 0</div>
<div id="start-screen">
<h1>飞机大战</h1>
<p>使用方向键移动,空格键射击</p>
<button id="start-button">开始游戏</button>
</div>
</div>
<script>
// 游戏状态
const GameState = { MENU: 0, PLAYING: 1, GAME_OVER: 2 };
let currentState = GameState.MENU;
let score = 0;
let gameOver = false;
let animationFrameId;
// 游戏对象
const player = {
x: 0, y: 0, width: 50, height: 50,
speed: 0.3, element: document.getElementById('player')
};
// 对象池
const enemies = [];
const bullets = [];
// 输入状态
const keys = {};
const touchControls = {
active: false, startX: 0, startY: 0, offsetX: 0, offsetY: 0
};
// 初始化游戏
function init() {
const container = document.getElementById('game-container');
const containerRect = container.getBoundingClientRect();
// 设置玩家初始位置
player.x = (containerRect.width - player.width) / 2;
player.y = containerRect.height - player.height - 20;
updateElementPosition(player.element, player.x, player.y);
// 事件监听
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
container.addEventListener('touchstart', handleTouchStart);
container.addEventListener('touchmove', handleTouchMove);
container.addEventListener('touchend', handleTouchEnd);
document.getElementById('start-button').addEventListener('click', startGame);
}
// 开始游戏
function startGame() {
if (currentState === GameState.PLAYING) return;
document.getElementById('start-screen').style.display = 'none';
currentState = GameState.PLAYING;
score = 0;
gameOver = false;
updateScoreDisplay();
// 清空所有敌机和子弹
clearEntities();
// 开始游戏循环
lastTime = performance.now();
animationFrameId = requestAnimationFrame(gameLoop);
}
// 游戏主循环
let lastTime = 0;
let enemySpawnTimer = 0;
function gameLoop(timestamp) {
if (gameOver) return;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
update(deltaTime);
render();
animationFrameId = requestAnimationFrame(gameLoop);
}
// 更新游戏状态
function update(deltaTime) {
updatePlayer(deltaTime);
updateBullets(deltaTime);
updateEnemies(deltaTime);
spawnEnemies(deltaTime);
checkCollisions();
}
// 更新玩家位置
function updatePlayer(deltaTime) {
if (!touchControls.active) {
// 键盘控制
if (keys.ArrowLeft) player.x -= player.speed * deltaTime;
if (keys.ArrowRight) player.x += player.speed * deltaTime;
if (keys.ArrowUp) player.y -= player.speed * deltaTime;
if (keys.ArrowDown) player.y += player.speed * deltaTime;
// 发射子弹
if (keys.Space && Date.now() - (lastShotTime || 0) > 300) {
shoot();
lastShotTime = Date.now();
}
} else {
// 触摸控制
player.x = touchControls.currentX - touchControls.offsetX;
player.y = touchControls.currentY - touchControls.offsetY;
}
// 边界检查
const container = document.getElementById('game-container');
const containerRect = container.getBoundingClientRect();
player.x = Math.max(0, Math.min(containerRect.width - player.width, player.x));
player.y = Math.max(0, Math.min(containerRect.height - player.height, player.y));
}
// 发射子弹
let lastShotTime = 0;
function shoot() {
const bullet = {
x: player.x + player.width / 2 - 2,
y: player.y,
width: 4,
height: 16,
speed: 0.5,
element: document.createElement('div')
};
bullet.element.className = 'bullet';
document.getElementById('bullets-container').appendChild(bullet.element);
bullets.push(bullet);
}
// 更新子弹位置
function updateBullets(deltaTime) {
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
bullet.y -= bullet.speed * deltaTime;
if (bullet.y < -bullet.height) {
// 移除超出屏幕的子弹
bullet.element.remove();
bullets.splice(i, 1);
}
}
}
// 生成敌机
function spawnEnemies(deltaTime) {
enemySpawnTimer += deltaTime;
const spawnInterval = Math.max(200, 1000 - score * 0.5);
if (enemySpawnTimer > spawnInterval) {
const enemy = {
x: Math.random() * (window.innerWidth - 40),
y: -40,
width: 40,
height: 40,
speed: 0.1 + Math.min(0.2, score * 0.0002),
element: document.createElement('div')
};
enemy.element.className = 'enemy';
document.getElementById('enemies-container').appendChild(enemy.element);
enemies.push(enemy);
enemySpawnTimer = 0;
}
}
// 更新敌机位置
function updateEnemies(deltaTime) {
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
enemy.y += enemy.speed * deltaTime;
if (enemy.y > window.innerHeight) {
// 移除超出屏幕的敌机
enemy.element.remove();
enemies.splice(i, 1);
}
}
}
// 碰撞检测
function checkCollisions() {
// 子弹与敌机碰撞
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
if (isColliding(bullet, enemy)) {
// 击中敌机
enemy.element.remove();
enemies.splice(j, 1);
bullet.element.remove();
bullets.splice(i, 1);
score += 100;
updateScoreDisplay();
break;
}
}
}
// 玩家与敌机碰撞
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
if (isColliding(player, enemy)) {
// 游戏结束
gameOver = true;
cancelAnimationFrame(animationFrameId);
showGameOver();
break;
}
}
}
// 碰撞检测辅助函数
function isColliding(obj1, obj2) {
return obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y;
}
// 渲染游戏
function render() {
updateElementPosition(player.element, player.x, player.y);
for (const bullet of bullets) {
updateElementPosition(bullet.element, bullet.x, bullet.y);
}
for (const enemy of enemies) {
updateElementPosition(enemy.element, enemy.x, enemy.y);
}
}
// 更新元素位置
function updateElementPosition(element, x, y) {
element.style.transform = `translate(${x}px, ${y}px)`;
}
// 更新分数显示
function updateScoreDisplay() {
document.getElementById('score-display').textContent = `Score: ${score}`;
}
// 显示游戏结束
function showGameOver() {
const startScreen = document.getElementById('start-screen');
startScreen.querySelector('h1').textContent = '游戏结束';
startScreen.querySelector('p').textContent = `最终得分: ${score}`;
startScreen.querySelector('button').textContent = '再来一次';
startScreen.style.display = 'flex';
}
// 清空所有实体
function clearEntities() {
const enemiesContainer = document.getElementById('enemies-container');
const bulletsContainer = document.getElementById('bullets-container');
enemiesContainer.innerHTML = '';
bulletsContainer.innerHTML = '';
enemies.length = 0;
bullets.length = 0;
}
// 输入处理
function handleKeyDown(e) {
keys[e.code] = true;
// 阻止方向键默认行为
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) {
e.preventDefault();
}
}
function handleKeyUp(e) {
keys[e.code] = false;
}
function handleTouchStart(e) {
const touch = e.touches[0];
touchControls.active = true;
touchControls.startX = touch.clientX;
touchControls.startY = touch.clientY;
touchControls.offsetX = touch.clientX - player.x;
touchControls.offsetY = touch.clientY - player.y;
touchControls.currentX = touch.clientX;
touchControls.currentY = touch.clientY;
// 发射子弹
shoot();
lastShotTime = Date.now();
bulletInterval = setInterval(shoot, 300);
}
function handleTouchMove(e) {
if (!touchControls.active) return;
const touch = e.touches[0];
touchControls.currentX = touch.clientX;
touchControls.currentY = touch.clientY;
e.preventDefault();
}
function handleTouchEnd() {
touchControls.active = false;
clearInterval(bulletInterval);
}
// 启动游戏
init();
</script>
</body>
</html>
