无人机小游戏

【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键: 重新开始游戏

触摸屏控制

  • 触摸屏幕左侧:向左移动
  • 触摸屏幕右侧:向右移动
  • 触摸屏幕上部:向上移动
  • 触摸屏幕下部:向下移动
  • 触摸屏幕顶部区域:加速飞行

代码优化建议

  1. 性能优化

    • 对于大型游戏,可以考虑使用对象池来复用障碍物对象,减少内存分配和垃圾回收
    • 可以实现视口裁剪,只渲染可见区域内的对象
  2. 功能扩展

    • 添加音效和背景音乐增强游戏体验
    • 实现不同类型的障碍物和道具系统
    • 添加关卡难度设计和最高分记录
  3. 视觉增强

    • 为无人机添加动画效果和粒子系统
    • 实现更复杂的背景层次和光照效果

完整代码

以下是完整的游戏代码,您可以直接复制到 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 创建一个完整的小游戏。核心技术点包括:

  1. Canvas 绘图基础:使用 Canvas API 绘制游戏元素
  2. 游戏循环架构:基于 requestAnimationFrame 实现流畅的游戏更新和渲染
  3. 碰撞检测算法:实现基本的矩形碰撞检测
  4. 用户交互处理:同时支持键盘和触摸屏控制
  5. 游戏难度平衡:实现随时间递增的难度系统
  6. 响应式设计:适配不同屏幕尺寸

这个游戏虽然简单,但包含了游戏开发的基本要素。您可以基于这个框架进一步扩展,添加更多功能和视觉效果,打造更丰富的游戏体验。

希望这篇文章对您学习游戏开发有所帮助!如果有任何问题或建议,欢迎在评论区留言讨论。

参考资料


如果觉得本文不错,请给个👍,支持一下作者!您的支持是我创作的最大动力!

相关推荐
lky不吃香菜4 小时前
深度学习入门:从“流水线工人”到“变形金刚”的架构漫游指南
人工智能·机器学习
zhurui_xiaozhuzaizai4 小时前
flash-attention连环问答--softmax 、safe softmax 、online softmax
人工智能
skywalk81634 小时前
在SCNet使用异构海光DCU 部署文心21B大模型报错HIP out of memory(未调通)
人工智能
ASKED_20194 小时前
深度强化学习之123-概念梳理
人工智能
攻城狮7号4 小时前
OpenAI 的 Sora 2来了:一场创意革命与失控的狂欢
人工智能·大模型·openai·ai视频·sora 2
胖头鱼的鱼缸(尹海文)5 小时前
数据库管理-第376期 Oracle AI DB 23.26新特性一览(20251016)
数据库·人工智能·oracle
瑞禧生物ruixibio5 小时前
4-ARM-PEG-Pyrene(2)/Biotin(2),多功能化聚乙二醇修饰荧光标记生物分子的设计与应用探索
arm开发·人工智能
大千AI助手5 小时前
Huber损失函数:稳健回归的智慧之选
人工智能·数据挖掘·回归·损失函数·mse·mae·huber损失函数