HTML5 网页小游戏开发实战:从零制作飞机大战(附完整源码)
前言
最近在学习 HTML、CSS 和 JavaScript 时,我想尝试做一个能够直接在浏览器中运行的小游戏。
相比普通的静态网页,小游戏需要处理更多交互逻辑,例如角色移动、碰撞检测、动画更新和分数统计,因此非常适合用来练习 JavaScript 编程。
这次选择开发一款经典的飞机大战小游戏。
我们将使用 HTML5 Canvas 绘制游戏画面,使用 JavaScript 实现游戏逻辑,并通过 CSS 美化页面。
整个项目不依赖任何第三方框架,适合前端初学者学习。
一、项目功能介绍
本次实现的飞机大战包含以下功能:
- 玩家通过方向键或 WASD 控制飞机移动。
- 按空格键发射子弹。
- 敌机从屏幕顶部随机出现。
- 子弹击中敌机后获得分数。
- 玩家与敌机碰撞时扣除生命值。
- 支持游戏开始、结束和重新开始。
- 支持手机触屏拖动飞机和自动射击。
完成后,我们将得到一个可以直接在电脑和手机浏览器中运行的小游戏。
二、项目技术栈
| 技术 | 作用 |
|---|---|
| HTML5 | 搭建网页结构 |
| CSS3 | 美化游戏界面 |
| JavaScript | 实现游戏逻辑 |
| Canvas API | 绘制飞机、子弹和敌机 |
| requestAnimationFrame | 实现游戏动画 |
本项目不需要安装 Node.js,也不需要使用 Vue、React 等框架。
三、创建项目
首先新建一个文件夹:
airplane-game/
然后在文件夹中创建一个文件:
airplane-game/
└── index.html
为了方便初学者复制和运行,我们将 HTML、CSS、JavaScript 全部写在同一个文件中。
后续项目功能增加时,可以再将样式和脚本拆分到独立文件中。
四、完整游戏源码
将以下代码复制到 index.html 文件中:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>HTML5 飞机大战</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: #101827;
color: white;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
h1 {
margin: 12px;
font-size: 26px;
}
.game-box {
position: relative;
width: min(95vw, 420px);
}
canvas {
display: block;
width: 100%;
height: auto;
background: #071124;
border: 2px solid #3b82f6;
border-radius: 12px;
touch-action: none;
}
.panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
display: flex;
justify-content: space-between;
font-weight: bold;
pointer-events: none;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
background: rgba(0, 0, 0, 0.75);
border-radius: 12px;
}
.overlay.hidden {
display: none;
}
button {
padding: 12px 28px;
border: none;
border-radius: 8px;
background: #3b82f6;
color: white;
font-size: 18px;
cursor: pointer;
}
button:hover {
background: #2563eb;
}
.tips {
text-align: center;
color: #aab8d0;
font-size: 14px;
line-height: 1.8;
}
</style>
</head>
<body>
<h1>✈ HTML5 飞机大战</h1>
<div class="game-box">
<canvas id="game" width="420" height="600"></canvas>
<div class="panel">
<span id="score">得分:0</span>
<span id="lives">生命:3</span>
</div>
<div class="overlay" id="overlay">
<h2 id="message">飞机大战</h2>
<button id="startBtn">开始游戏</button>
</div>
</div>
<p class="tips">
电脑:方向键 / WASD 移动,空格发射<br>
手机:触屏拖动飞机,自动发射
</p>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreText = document.getElementById("score");
const livesText = document.getElementById("lives");
const overlay = document.getElementById("overlay");
const message = document.getElementById("message");
const startBtn = document.getElementById("startBtn");
const W = canvas.width;
const H = canvas.height;
let player;
let bullets = [];
let enemies = [];
let particles = [];
let score = 0;
let lives = 3;
let running = false;
let lastTime = 0;
let shootTimer = 0;
let enemyTimer = 0;
let invincible = 0;
const keys = {};
let touching = false;
const stars = Array.from({ length: 65 }, () => ({
x: Math.random() * W,
y: Math.random() * H,
size: Math.random() * 2 + 0.5,
speed: Math.random() * 1.5 + 0.5
}));
function resetGame() {
player = {
x: W / 2 - 18,
y: H - 90,
w: 36,
h: 44,
speed: 300
};
bullets = [];
enemies = [];
particles = [];
score = 0;
lives = 3;
shootTimer = 0;
enemyTimer = 0;
invincible = 0;
updatePanel();
}
function updatePanel() {
scoreText.textContent = "得分:" + score;
livesText.textContent = "生命:" + lives;
}
function startGame() {
resetGame();
running = true;
overlay.classList.add("hidden");
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function gameOver() {
running = false;
message.textContent = "游戏结束!得分:" + score;
startBtn.textContent = "重新开始";
overlay.classList.remove("hidden");
}
function shoot() {
bullets.push({
x: player.x + player.w / 2 - 3,
y: player.y - 10,
w: 6,
h: 16,
speed: 480
});
}
function spawnEnemy() {
const size = 28 + Math.random() * 18;
enemies.push({
x: Math.random() * (W - size),
y: -size,
w: size,
h: size,
speed: 90 + Math.random() * 85 + score * 0.3
});
}
function hit(a, b) {
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
);
}
function explode(x, y) {
for (let i = 0; i < 12; i++) {
particles.push({
x,
y,
vx: (Math.random() - 0.5) * 220,
vy: (Math.random() - 0.5) * 220,
life: 0.5
});
}
}
function update(dt) {
// 玩家移动
if (keys["ArrowLeft"] || keys["a"]) {
player.x -= player.speed * dt;
}
if (keys["ArrowRight"] || keys["d"]) {
player.x += player.speed * dt;
}
if (keys["ArrowUp"] || keys["w"]) {
player.y -= player.speed * dt;
}
if (keys["ArrowDown"] || keys["s"]) {
player.y += player.speed * dt;
}
player.x = Math.max(
0,
Math.min(W - player.w, player.x)
);
player.y = Math.max(
0,
Math.min(H - player.h, player.y)
);
// 发射子弹
shootTimer -= dt;
if ((keys[" "] || touching) && shootTimer <= 0) {
shoot();
shootTimer = 0.18;
}
// 生成敌机
enemyTimer -= dt;
if (enemyTimer <= 0) {
spawnEnemy();
enemyTimer = Math.max(
0.3,
0.85 - score * 0.002
);
}
// 更新子弹
bullets.forEach(b => {
b.y -= b.speed * dt;
});
bullets = bullets.filter(b => b.y + b.h > 0);
// 更新敌机
enemies.forEach(e => {
e.y += e.speed * dt;
});
enemies = enemies.filter(e => e.y < H + e.h);
// 子弹与敌机碰撞
for (let i = enemies.length - 1; i >= 0; i--) {
let destroyed = false;
for (let j = bullets.length - 1; j >= 0; j--) {
if (hit(enemies[i], bullets[j])) {
const e = enemies[i];
explode(
e.x + e.w / 2,
e.y + e.h / 2
);
enemies.splice(i, 1);
bullets.splice(j, 1);
score += 10;
updatePanel();
destroyed = true;
break;
}
}
if (destroyed) continue;
}
// 玩家碰撞检测
if (invincible > 0) {
invincible -= dt;
}
for (let i = enemies.length - 1; i >= 0; i--) {
if (invincible <= 0 && hit(player, enemies[i])) {
explode(
player.x + player.w / 2,
player.y + player.h / 2
);
enemies.splice(i, 1);
lives--;
invincible = 1.5;
updatePanel();
if (lives <= 0) {
gameOver();
return;
}
}
}
// 粒子动画
particles.forEach(p => {
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
});
particles = particles.filter(p => p.life > 0);
// 背景星星
stars.forEach(s => {
s.y += s.speed * 60 * dt;
if (s.y > H) {
s.y = 0;
s.x = Math.random() * W;
}
});
}
function drawPlayer() {
if (
invincible > 0 &&
Math.floor(invincible * 10) % 2 === 0
) {
return;
}
ctx.fillStyle = "#38bdf8";
ctx.beginPath();
ctx.moveTo(player.x + player.w / 2, player.y);
ctx.lineTo(player.x + player.w, player.y + player.h);
ctx.lineTo(player.x + player.w / 2, player.y + 32);
ctx.lineTo(player.x, player.y + player.h);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#facc15";
ctx.fillRect(
player.x + player.w / 2 - 4,
player.y + 20,
8,
12
);
}
function drawEnemy(e) {
ctx.fillStyle = "#fb7185";
ctx.beginPath();
ctx.moveTo(e.x, e.y);
ctx.lineTo(e.x + e.w / 2, e.y + e.h);
ctx.lineTo(e.x + e.w, e.y);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#fecdd3";
ctx.fillRect(
e.x + e.w / 2 - 3,
e.y + 5,
6,
12
);
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = "#071124";
ctx.fillRect(0, 0, W, H);
// 星空背景
ctx.fillStyle = "white";
stars.forEach(s => {
ctx.globalAlpha = 0.4 + s.size / 4;
ctx.fillRect(s.x, s.y, s.size, s.size);
});
ctx.globalAlpha = 1;
// 子弹
ctx.fillStyle = "#facc15";
bullets.forEach(b => {
ctx.fillRect(b.x, b.y, b.w, b.h);
});
// 敌机
enemies.forEach(drawEnemy);
// 玩家
drawPlayer();
// 爆炸粒子
particles.forEach(p => {
ctx.globalAlpha = Math.max(0, p.life * 2);
ctx.fillStyle = "#fb923c";
ctx.fillRect(p.x, p.y, 5, 5);
});
ctx.globalAlpha = 1;
}
function gameLoop(time) {
if (!running) return;
const dt = Math.min((time - lastTime) / 1000, 0.05);
lastTime = time;
update(dt);
draw();
if (running) {
requestAnimationFrame(gameLoop);
}
}
window.addEventListener("keydown", e => {
if (
["ArrowUp", "ArrowDown", "ArrowLeft",
"ArrowRight", " "].includes(e.key)
) {
e.preventDefault();
}
keys[e.key.length === 1
? e.key.toLowerCase()
: e.key] = true;
});
window.addEventListener("keyup", e => {
keys[e.key.length === 1
? e.key.toLowerCase()
: e.key] = false;
});
// 手机触屏控制
function moveTouch(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
player.x = Math.max(
0,
Math.min(W - player.w, x - player.w / 2)
);
player.y = Math.max(
0,
Math.min(H - player.h, y - player.h / 2)
);
}
canvas.addEventListener("pointerdown", e => {
if (!running) return;
touching = true;
canvas.setPointerCapture(e.pointerId);
moveTouch(e);
});
canvas.addEventListener("pointermove", e => {
if (running && touching) {
moveTouch(e);
}
});
canvas.addEventListener("pointerup", () => {
touching = false;
});
canvas.addEventListener("pointercancel", () => {
touching = false;
});
startBtn.addEventListener("click", startGame);
// 初始化画面
resetGame();
draw();
</script>
</body>
</html>
五、运行项目
代码保存完成后,找到 index.html 文件,双击即可使用浏览器打开。
推荐使用 Chrome、Edge、Firefox 或 Safari 等现代浏览器。
打开后可以看到深色星空背景、游戏标题和开始按钮。
点击「开始游戏」即可进入游戏。
电脑端操作
| 按键 | 功能 |
|---|---|
| ↑ / W | 向上移动 |
| ↓ / S | 向下移动 |
| ← / A | 向左移动 |
| → / D | 向右移动 |
| 空格 | 发射子弹 |
手机端操作
在手机浏览器中打开游戏后,点击「开始游戏」。
用手指在游戏画面中拖动,即可控制飞机移动;按住屏幕时飞机会自动发射子弹。
由于使用了 touch-action: none,在游戏区域内拖动时不会触发普通的页面滚动。
六、核心代码解析
1. Canvas 绘图原理
Canvas 是 HTML5 提供的二维绘图功能。
首先在 HTML 中创建画布:
<canvas id="game" width="420" height="600"></canvas>
再通过 JavaScript 获取绘图上下文:
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
之后便可以使用 Canvas API 绘制矩形、线条、圆形和其他图形。
例如:
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 50, 50);
这段代码会在画布的指定位置绘制一个红色矩形。
本项目中的飞机、子弹和敌机都是通过类似方式绘制出来的。
2. 游戏循环的实现
网页游戏需要持续更新画面。
本项目使用:
requestAnimationFrame(gameLoop);
它会在浏览器准备绘制下一帧时调用指定函数。
核心游戏循环如下:
function gameLoop(time) {
if (!running) return;
const dt = Math.min((time - lastTime) / 1000, 0.05);
lastTime = time;
update(dt);
draw();
if (running) {
requestAnimationFrame(gameLoop);
}
}
其中:
update()负责更新游戏状态。draw()负责绘制画面。dt表示两帧之间经过的时间。
使用时间差计算位移,可以减少不同刷新率设备之间的速度差异。
3. 飞机移动逻辑
我们使用一个 JavaScript 对象保存玩家飞机的信息:
player = {
x: 192,
y: 510,
w: 36,
h: 44,
speed: 300
};
其中:
x、y:飞机坐标。w、h:飞机宽度和高度。speed:移动速度。
例如,当用户按下右方向键时:
player.x += player.speed * dt;
飞机就会向右移动。
为了防止飞机飞出屏幕,还需要限制坐标范围。
4. 碰撞检测原理
碰撞检测是小游戏开发中非常重要的一部分。
本项目使用的是 AABB(轴对齐包围盒)碰撞检测。
核心代码:
function hit(a, b) {
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
);
}
它通过比较两个矩形的坐标,判断它们是否发生重叠。
这种方式计算简单,适合飞机大战、打砖块等二维小游戏。
不过,它属于近似碰撞检测:即使飞机绘制成三角形,碰撞区域仍然是矩形。
5. 敌机随机生成
敌机从画布顶部随机位置出现。
function spawnEnemy() {
const size = 28 + Math.random() * 18;
enemies.push({
x: Math.random() * (W - size),
y: -size,
w: size,
h: size,
speed: 90 + Math.random() * 85
});
}
通过 Math.random(),可以生成不同位置和速度的敌机,让游戏更有变化。
正式游戏代码还会根据分数略微增加敌机速度,从而提高游戏难度。
6. 游戏状态管理
本项目使用 running 变量表示游戏是否正在运行。
let running = false;
当用户点击开始按钮时:
running = true;
当玩家生命值耗尽时:
running = false;
这样可以控制游戏循环是否继续执行。
对于规模较小的项目,这种方式已经足够。
如果后续加入暂停菜单、关卡切换等功能,可以使用更完整的状态管理方案。
七、开发中需要注意的问题
1. 为什么使用 requestAnimationFrame?
相比简单的 setInterval(),requestAnimationFrame() 更适合与浏览器的绘制节奏配合。
浏览器在后台运行时,也通常会降低或暂停动画回调频率,有助于减少不必要的资源消耗。
2. 为什么要限制 dt?
如果用户切换浏览器标签页,过一段时间再回来,两帧之间的时间差可能突然变大。
如果直接用这个时间差计算位移,敌机或子弹可能瞬间移动很远。
因此代码中使用:
const dt = Math.min(
(time - lastTime) / 1000,
0.05
);
将单次更新的时间差限制在合理范围内。
3. 为什么手机上要转换坐标?
Canvas 的内部尺寸是420×600,但通过 CSS 设置后,实际显示尺寸可能随手机屏幕变化。
所以触摸坐标不能直接当作游戏坐标使用,需要根据画布显示比例进行换算。
4. 游戏是否需要服务器?
本项目是纯前端小游戏,所有逻辑都在浏览器中运行,因此不需要服务器。
如果后续要实现在线排行榜、用户登录、多人对战等功能,就需要引入后端服务和数据库。
八、项目后续优化方向
完成基础版本后,还可以继续添加更多功能。
1. 增加游戏音效
可以通过 Web Audio API 为发射子弹、击中敌机和游戏结束等事件添加音效。
2. 添加不同类型的敌机
例如:
- 普通敌机:速度较慢。
- 快速敌机:移动速度更快。
- 大型敌机:需要多次命中才能击败。
3. 增加道具系统
例如临时护盾、双发子弹和生命恢复道具。
4. 添加本地最高分
使用浏览器的 localStorage 保存最高分:
localStorage.setItem("bestScore", 100);
读取时:
const bestScore =
localStorage.getItem("bestScore");
这样用户关闭网页后,再次打开游戏仍然可以看到之前保存的成绩。
5. 将游戏部署到网上
由于项目只有静态文件,可以使用 GitHub Pages 等静态网站托管服务进行部署。
部署完成后,就可以通过网页链接访问游戏,而不必手动传输 HTML 文件。
九、总结
通过本次 HTML5 飞机大战小游戏的开发,我们学习了:
- HTML 网页结构搭建
- CSS 页面布局与界面美化
- JavaScript 对象和数组
- Canvas 2D 绘图
- requestAnimationFrame 动画循环
- 键盘和触摸事件处理
- 碰撞检测
- 游戏分数与生命值管理
虽然这只是一个基础版本的飞机大战,但已经包含了二维小游戏开发的主要组成部分。
通过这个项目可以发现,小游戏开发不仅需要掌握语法,还需要思考对象之间的关系、游戏规则的实现以及用户交互体验。
后续可以在这个基础上继续增加音效、道具、排行榜和更多关卡,让游戏逐渐变得完整。
希望这篇文章能够帮助正在学习 JavaScript 和 HTML5 的朋友们理解网页小游戏的基本开发流程。
文章分类: 前端开发 / JavaScript
文章标签: HTML5、JavaScript、Canvas、网页游戏、飞机大战、前端开发、项目实战
项目难度: 入门
运行环境: 现代桌面或移动浏览器

浙公网安备 33010602011771号