HTML5 网页小游戏开发实战:从零制作飞机大战(附完整源码)

前言

最近在学习 HTML、CSS 和 JavaScript 时,我想尝试做一个能够直接在浏览器中运行的小游戏。

相比普通的静态网页,小游戏需要处理更多交互逻辑,例如角色移动、碰撞检测、动画更新和分数统计,因此非常适合用来练习 JavaScript 编程。

这次选择开发一款经典的飞机大战小游戏。

我们将使用 HTML5 Canvas 绘制游戏画面,使用 JavaScript 实现游戏逻辑,并通过 CSS 美化页面。

整个项目不依赖任何第三方框架,适合前端初学者学习。

一、项目功能介绍

本次实现的飞机大战包含以下功能:

  1. 玩家通过方向键或 WASD 控制飞机移动。
  2. 按空格键发射子弹。
  3. 敌机从屏幕顶部随机出现。
  4. 子弹击中敌机后获得分数。
  5. 玩家与敌机碰撞时扣除生命值。
  6. 支持游戏开始、结束和重新开始。
  7. 支持手机触屏拖动飞机和自动射击。

完成后,我们将得到一个可以直接在电脑和手机浏览器中运行的小游戏。

二、项目技术栈

技术 作用
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、网页游戏、飞机大战、前端开发、项目实战

项目难度: 入门

运行环境: 现代桌面或移动浏览器

posted @ 2026-10-17 10:34  athelkw  阅读(1)  评论(0)    收藏  举报