【HTML5 Canvas 实战】从零构建一个无人机飞行小游戏
游戏介绍
今天我将分享一个基于 HTML5 Canvas 和 JavaScript 开发的无人机飞行小游戏。在这个游戏中,玩家需要控制无人机躲避从右侧飞来的障碍物,尽可能长时间地保持飞行,获取更高的分数。游戏支持键盘和触摸屏控制,并具有响应式设计,适配不同设备。
游戏演示
提示:如果没有在线演示地址,可以直接复制本文末尾的完整代码到 HTML 文件中运行体验
游戏特点
- 🎮 直观的控制系统:支持键盘(方向键/WASD)和触摸屏控制
- 🚀 加速功能:按空格键可以让无人机加速飞行
- 🎯 随机生成障碍物:每次游戏体验都不同
- ⏱️ 难度递增:随着游戏进行,障碍物生成速度加快
- 🎨 精美的视觉效果:星空背景、渐变障碍物和流畅的动画
- 📱 响应式设计:适配桌面和移动设备
- 💯 实时计分系统:记录玩家的飞行时长和表现
技术实现
1. 游戏核心架构
游戏采用经典的游戏循环模式,包含更新(Update)和渲染(Render)两个主要阶段:
javascript
// 游戏循环
let lastTime = 0;
function gameLoop(timestamp) {
if (gameState.isGameOver) return;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// 更新和绘制游戏元素
drawBackground();
drone.update();
drone.draw();
updateObstacles(deltaTime);
drawObstacles();
updateScore(deltaTime);
// 检测碰撞
checkCollisions();
// 继续游戏循环
requestAnimationFrame(gameLoop);
}
这种基于 requestAnimationFrame
的游戏循环确保了游戏在不同设备上都能以最佳性能运行。
2. 无人机控制实现
无人机对象封装了移动逻辑和渲染方法:
javascript
// 无人机对象
const drone = {
x: canvas.width / 4,
y: canvas.height / 2,
width: 40,
height: 30,
speed: 5,
acceleration: 2,
rotation: 0,
maxRotation: 15,
// 更新位置和状态
update() {
// 根据按键状态更新位置
if (gameState.keys.up) {
this.y -= this.speed + (gameState.isAccelerating ? this.acceleration : 0);
this.rotation = -this.maxRotation;
} else if (gameState.keys.down) {
this.y += this.speed + (gameState.isAccelerating ? this.acceleration : 0);
this.rotation = this.maxRotation;
}
// ... 左右移动逻辑
// 边界检测
this.x = Math.max(0, Math.min(this.x, canvas.width - this.width));
this.y = Math.max(0, Math.min(this.y, canvas.height - this.height));
},
// 绘制无人机
draw() {
// Canvas 渲染代码
}
};
3. 障碍物生成系统
障碍物系统实现了随机生成和移动逻辑:
javascript
// 创建障碍物
function createObstacle() {
const obstacleWidth = 30 + Math.random() * 40;
const obstacleHeight = 30 + Math.random() * 150;
const x = canvas.width;
const y = Math.random() * (canvas.height - obstacleHeight);
obstacles.push({
x: x,
y: y,
width: obstacleWidth,
height: obstacleHeight,
color: `rgb(${Math.random() * 100}, ${Math.random() * 150 + 100}, ${Math.random() * 255})`
});
}
// 更新障碍物
function updateObstacles(deltaTime) {
obstacles.forEach(obstacle => {
obstacle.x -= gameState.gameSpeed + (gameState.isAccelerating ? 1 : 0);
});
// 移除离开屏幕的障碍物
obstacles = obstacles.filter(obstacle => obstacle.x + obstacle.width > 0);
// 生成新障碍物
const now = Date.now();
if (now - gameState.lastObstacleSpawn > gameState.obstacleSpawnRate) {
createObstacle();
gameState.lastObstacleSpawn = now;
// 逐渐增加游戏难度
gameState.obstacleSpawnRate = Math.max(1000, gameState.obstacleSpawnRate - 10);
gameState.gameSpeed = Math.min(5, gameState.gameSpeed + 0.01);
}
}
4. 碰撞检测机制
使用简单高效的矩形碰撞检测算法:
javascript
// 碰撞检测
function checkCollisions() {
for (const obstacle of obstacles) {
if (drone.x < obstacle.x + obstacle.width &&
drone.x + drone.width > obstacle.x &&
drone.y < obstacle.y + obstacle.height &&
drone.y + drone.height > obstacle.y) {
gameOver();
return true;
}
}
return false;
}
5. 星空背景效果
实现了动态的星空背景,增强视觉体验:
javascript
// 绘制星空背景
const starsCount = 200;
for (let i = 0; i < starsCount; i++) {
const x = (i * 137.5 + gameState.backgroundOffset * 0.1) % canvas.width;
const y = (i * 179.3) % canvas.height;
const size = (i % 3) * 0.5 + 0.5;
const alpha = (Math.sin(Date.now() / 1000 + i) + 1) * 0.3 + 0.2;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
控制方式
键盘控制
- 方向键 ↑ ↓ ← → 或 W A S D: 控制无人机的上下左右移动
- 空格键: 加速飞行
- R键: 重新开始游戏
触摸屏控制
- 触摸屏幕左侧:向左移动
- 触摸屏幕右侧:向右移动
- 触摸屏幕上部:向上移动
- 触摸屏幕下部:向下移动
- 触摸屏幕顶部区域:加速飞行
代码优化建议
-
性能优化
- 对于大型游戏,可以考虑使用对象池来复用障碍物对象,减少内存分配和垃圾回收
- 可以实现视口裁剪,只渲染可见区域内的对象
-
功能扩展
- 添加音效和背景音乐增强游戏体验
- 实现不同类型的障碍物和道具系统
- 添加关卡难度设计和最高分记录
-
视觉增强
- 为无人机添加动画效果和粒子系统
- 实现更复杂的背景层次和光照效果
完整代码
以下是完整的游戏代码,您可以直接复制到 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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #0a0a2a;
color: #ffffff;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.game-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
#gameCanvas {
border: 2px solid #333;
background-color: #1a1a4a;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
}
.game-info {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
max-width: 200px;
}
.score {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 15px;
color: #00ffcc;
}
.controls-info {
font-size: 0.9em;
line-height: 1.4;
}
.controls-info p {
margin-bottom: 5px;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.9);
padding: 30px;
border-radius: 10px;
text-align: center;
display: none;
border: 2px solid #ff4444;
box-shadow: 0 0 30px rgba(255, 68, 68, 0.5);
}
.game-over h2 {
color: #ff4444;
margin-bottom: 20px;
font-size: 2em;
}
.game-over p {
margin-bottom: 20px;
font-size: 1.2em;
}
#finalScore {
color: #00ffcc;
font-weight: bold;
}
#restartButton {
background-color: #00ffcc;
color: #0a0a2a;
border: none;
padding: 10px 20px;
font-size: 1em;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
#restartButton:hover {
background-color: #00cccc;
transform: scale(1.05);
box-shadow: 0 0 15px rgba(0, 255, 204, 0.5);
}
/* 响应式设计 */
@media (max-width: 768px) {
#gameCanvas {
width: 90vw;
height: 70vh;
}
.game-info {
position: relative;
top: 10px;
left: auto;
max-width: 90vw;
margin-bottom: 10px;
}
.game-over {
width: 90vw;
}
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas"></canvas>
<div class="game-info">
<div class="score">得分: <span id="scoreValue">0</span></div>
<div class="controls-info">
<p>控制说明:</p>
<p>↑ ↓ ← → 或 W A S D: 控制无人机移动</p>
<p>空格键: 加速</p>
<p>R: 重新开始</p>
</div>
</div>
<div class="game-over" id="gameOverScreen">
<h2>游戏结束</h2>
<p>最终得分: <span id="finalScore"></span></p>
<button id="restartButton">重新开始</button>
</div>
</div>
<script>
// 游戏主文件
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreValue = document.getElementById('scoreValue');
const gameOverScreen = document.getElementById('gameOverScreen');
const finalScore = document.getElementById('finalScore');
const restartButton = document.getElementById('restartButton');
// 设置canvas尺寸
function setCanvasSize() {
canvas.width = 800;
canvas.height = 600;
// 响应式调整
if (window.innerWidth < 900) {
canvas.width = window.innerWidth - 50;
canvas.height = window.innerHeight * 0.7;
}
}
setCanvasSize();
window.addEventListener('resize', setCanvasSize);
// 游戏状态
let gameState = {
score: 0,
isGameOver: false,
isAccelerating: false,
lastObstacleSpawn: 0,
obstacleSpawnRate: 2000,
gameSpeed: 2,
backgroundOffset: 0,
keys: {
up: false,
down: false,
left: false,
right: false
}
};
// 无人机对象
const drone = {
x: canvas.width / 4,
y: canvas.height / 2,
width: 40,
height: 30,
speed: 5,
acceleration: 2,
rotation: 0,
maxRotation: 15,
draw() {
ctx.save();
ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
ctx.rotate((this.rotation * Math.PI) / 180);
// 绘制无人机机身
ctx.fillStyle = '#00ffcc';
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
// 绘制无人机窗口
ctx.fillStyle = '#33ccff';
ctx.fillRect(-this.width / 4, -this.height / 3, this.width / 2, this.height / 3);
// 绘制螺旋桨
ctx.fillStyle = '#ffffff';
ctx.fillRect(-this.width / 1.5, -this.height / 4, this.width / 3, this.height / 10);
ctx.fillRect(this.width / 6, -this.height / 4, this.width / 3, this.height / 10);
// 绘制尾部
ctx.fillStyle = '#00ccaa';
ctx.beginPath();
ctx.moveTo(this.width / 2, -this.height / 4);
ctx.lineTo(this.width / 1.5, 0);
ctx.lineTo(this.width / 2, this.height / 4);
ctx.closePath();
ctx.fill();
ctx.restore();
},
update() {
// 控制无人机移动
if (gameState.keys.up) {
this.y -= this.speed + (gameState.isAccelerating ? this.acceleration : 0);
this.rotation = -this.maxRotation;
} else if (gameState.keys.down) {
this.y += this.speed + (gameState.isAccelerating ? this.acceleration : 0);
this.rotation = this.maxRotation;
} else {
this.rotation = 0;
}
if (gameState.keys.left) {
this.x -= this.speed + (gameState.isAccelerating ? this.acceleration : 0);
} else if (gameState.keys.right) {
this.x += this.speed + (gameState.isAccelerating ? this.acceleration : 0);
}
// 边界检测
this.x = Math.max(0, Math.min(this.x, canvas.width - this.width));
this.y = Math.max(0, Math.min(this.y, canvas.height - this.height));
}
};
// 障碍物数组
let obstacles = [];
// 创建障碍物
function createObstacle() {
const obstacleWidth = 30 + Math.random() * 40;
const obstacleHeight = 30 + Math.random() * 150;
const x = canvas.width;
const y = Math.random() * (canvas.height - obstacleHeight);
obstacles.push({
x: x,
y: y,
width: obstacleWidth,
height: obstacleHeight,
color: `rgb(${Math.random() * 100}, ${Math.random() * 150 + 100}, ${Math.random() * 255})`
});
}
// 绘制障碍物
function drawObstacles() {
obstacles.forEach(obstacle => {
ctx.save();
// 添加渐变效果
const gradient = ctx.createLinearGradient(obstacle.x, obstacle.y, obstacle.x + obstacle.width, obstacle.y + obstacle.height);
gradient.addColorStop(0, obstacle.color);
gradient.addColorStop(1, '#0a0a2a');
ctx.fillStyle = gradient;
ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height);
// 添加边框
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.strokeRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height);
ctx.restore();
});
}
// 更新障碍物
function updateObstacles(deltaTime) {
obstacles.forEach(obstacle => {
obstacle.x -= gameState.gameSpeed + (gameState.isAccelerating ? 1 : 0);
});
// 移除离开屏幕的障碍物
obstacles = obstacles.filter(obstacle => obstacle.x + obstacle.width > 0);
// 生成新障碍物
const now = Date.now();
if (now - gameState.lastObstacleSpawn > gameState.obstacleSpawnRate) {
createObstacle();
gameState.lastObstacleSpawn = now;
// 逐渐增加游戏难度
gameState.obstacleSpawnRate = Math.max(1000, gameState.obstacleSpawnRate - 10);
gameState.gameSpeed = Math.min(5, gameState.gameSpeed + 0.01);
}
}
// 绘制背景
function drawBackground() {
// 清空画布
ctx.fillStyle = '#0a0a2a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制星空背景
const starsCount = 200;
for (let i = 0; i < starsCount; i++) {
const x = (i * 137.5 + gameState.backgroundOffset * 0.1) % canvas.width;
const y = (i * 179.3) % canvas.height;
const size = (i % 3) * 0.5 + 0.5;
const alpha = (Math.sin(Date.now() / 1000 + i) + 1) * 0.3 + 0.2;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
// 绘制地面
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, canvas.height - 50, canvas.width, 50);
// 绘制地面标记
for (let i = 0; i < canvas.width; i += 50) {
const markX = (i - gameState.backgroundOffset * 0.5) % canvas.width;
ctx.fillStyle = '#333';
ctx.fillRect(markX, canvas.height - 40, 10, 20);
}
gameState.backgroundOffset += gameState.gameSpeed;
}
// 碰撞检测
function checkCollisions() {
for (const obstacle of obstacles) {
if (drone.x < obstacle.x + obstacle.width &&
drone.x + drone.width > obstacle.x &&
drone.y < obstacle.y + obstacle.height &&
drone.y + drone.height > obstacle.y) {
gameOver();
return true;
}
}
return false;
}
// 更新得分
function updateScore(deltaTime) {
gameState.score += deltaTime * 0.1;
scoreValue.textContent = Math.floor(gameState.score);
}
// 游戏结束
function gameOver() {
gameState.isGameOver = true;
finalScore.textContent = Math.floor(gameState.score);
gameOverScreen.style.display = 'block';
}
// 重新开始游戏
function restartGame() {
gameState = {
score: 0,
isGameOver: false,
isAccelerating: false,
lastObstacleSpawn: Date.now(),
obstacleSpawnRate: 2000,
gameSpeed: 2,
backgroundOffset: 0,
keys: {
up: false,
down: false,
left: false,
right: false
}
};
drone.x = canvas.width / 4;
drone.y = canvas.height / 2;
obstacles = [];
gameOverScreen.style.display = 'none';
scoreValue.textContent = '0';
// 立即开始游戏循环
requestAnimationFrame(gameLoop);
}
// 键盘事件监听
function setupEventListeners() {
// 键盘按下
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp':
case 'w':
case 'W':
gameState.keys.up = true;
break;
case 'ArrowDown':
case 's':
case 'S':
gameState.keys.down = true;
break;
case 'ArrowLeft':
case 'a':
case 'A':
gameState.keys.left = true;
break;
case 'ArrowRight':
case 'd':
case 'D':
gameState.keys.right = true;
break;
case ' ':
gameState.isAccelerating = true;
break;
case 'r':
case 'R':
if (gameState.isGameOver) {
restartGame();
}
break;
}
});
// 键盘松开
document.addEventListener('keyup', (e) => {
switch (e.key) {
case 'ArrowUp':
case 'w':
case 'W':
gameState.keys.up = false;
break;
case 'ArrowDown':
case 's':
case 'S':
gameState.keys.down = false;
break;
case 'ArrowLeft':
case 'a':
case 'A':
gameState.keys.left = false;
break;
case 'ArrowRight':
case 'd':
case 'D':
gameState.keys.right = false;
break;
case ' ':
gameState.isAccelerating = false;
break;
}
});
// 重新开始按钮
restartButton.addEventListener('click', restartGame);
// 移动设备触摸控制
canvas.addEventListener('touchstart', handleTouch);
canvas.addEventListener('touchmove', handleTouch);
canvas.addEventListener('touchend', () => {
gameState.keys.up = false;
gameState.keys.down = false;
gameState.keys.left = false;
gameState.keys.right = false;
});
}
// 处理触摸事件
function handleTouch(e) {
e.preventDefault();
if (gameState.isGameOver) return;
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const touchX = touch.clientX - rect.left;
const touchY = touch.clientY - rect.top;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
gameState.keys.left = touchX < centerX - 50;
gameState.keys.right = touchX > centerX + 50;
gameState.keys.up = touchY < centerY - 50;
gameState.keys.down = touchY > centerY + 50;
gameState.isAccelerating = touchY < centerY / 2;
}
// 游戏循环
let lastTime = 0;
function gameLoop(timestamp) {
if (gameState.isGameOver) return;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新和绘制游戏元素
drawBackground();
drone.update();
drone.draw();
updateObstacles(deltaTime);
drawObstacles();
updateScore(deltaTime);
// 检测碰撞
checkCollisions();
// 继续游戏循环
requestAnimationFrame(gameLoop);
}
// 初始化游戏
function initGame() {
setupEventListeners();
gameState.lastObstacleSpawn = Date.now();
requestAnimationFrame(gameLoop);
}
// 开始游戏
initGame();
</script>
</body>
</html>
总结
通过这个项目,我们学习了如何使用 HTML5 Canvas 和 JavaScript 创建一个完整的小游戏。核心技术点包括:
- Canvas 绘图基础:使用 Canvas API 绘制游戏元素
- 游戏循环架构:基于 requestAnimationFrame 实现流畅的游戏更新和渲染
- 碰撞检测算法:实现基本的矩形碰撞检测
- 用户交互处理:同时支持键盘和触摸屏控制
- 游戏难度平衡:实现随时间递增的难度系统
- 响应式设计:适配不同屏幕尺寸
这个游戏虽然简单,但包含了游戏开发的基本要素。您可以基于这个框架进一步扩展,添加更多功能和视觉效果,打造更丰富的游戏体验。
希望这篇文章对您学习游戏开发有所帮助!如果有任何问题或建议,欢迎在评论区留言讨论。
参考资料
如果觉得本文不错,请给个👍,支持一下作者!您的支持是我创作的最大动力!