回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

二哈识图2打造体感游戏小鸟躲避障碍物 简单

头像 党最需要的栋梁 2025.12.20 29 0

[项目] 体感跃动:基于物体追踪的HTML小鸟躲避游戏

在开始之前我想说:大家一定一定一定一定一定要更新固件,一步一步的跟着官方教程来,别想自己琢磨,我的一大半时间都在反思哪里程序编错了。

【演示视频】

 

 

 

 

【项目背景】
deep seek制作HTML网页炉火纯青了已经,我们不妨用它做个小游戏。提示词很简单,我稍后附上。
image.png

体感交互为传统游戏带来了全新的操控维度与沉浸体验。本项目灵感源于经典的“Flappy Bird”游戏,但摒弃了传统的键盘或触摸操作,创新性地利用二哈识图2(HuskyLens)AI视觉传感器与行空板K10,将现实世界中物体的移动实时映射为游戏内角色的控制。玩家通过移动一个被追踪的实物(如彩色积木、特定卡片),即可控制屏幕中的小鸟上下飞行、躲避障碍。该项目不仅是一个有趣的游戏,更是一个融合了计算机视觉、嵌入式硬件通信(串口)和现代Web前端技术(Web Serial API)的综合性实践案例,生动展示了如何将AI识别能力转化为直观的互动应用。

【功能和亮点】
屏幕截图 2025-12-21 224321.jpg

1.  创新的体感操控:完全脱离传统输入设备,通过追踪真实物体的垂直(Y轴)移动来控制游戏角色,实现“所见即所控”的自然交互。
2.  硬件与Web的无缝集成:利用行空板K10作为串口网关,接收二哈识图2的识别数据,并通过Web Serial API与浏览器中的HTML5游戏进行实时通信,构建了从硬件传感器到网页应用的完整链路。
3.  完整的游戏化设计:游戏具备完整的逻辑,包括随机生成的障碍物、递增的游戏速度、得分与最高分记录系统,以及游戏状态(开始、暂停、结束)管理,提供流畅的游戏体验。
4.  实时数据可视化仪表盘:游戏界面侧边实时显示来自硬件的串口原始数据、解析后的物体Y坐标、映射后的小鸟位置百分比等关键信息,便于调试和观察数据流转。
5.  多重控制模式与兼容性:在串口控制之外,保留了键盘(空格键/方向键)和鼠标/触摸控制作为备用方案,确保在各种环境下均可游玩,提升了项目的可用性和演示稳定性。
6.  响应式前端界面:采用现代CSS技术构建,界面美观且适配不同尺寸的屏幕,从桌面浏览器到移动设备都能获得良好的视觉体验。
7.  模块化与可扩展的代码结构:JavaScript代码将游戏逻辑、渲染绘图、串口通信等模块清晰分离,并提供了易于修改的数据解析函数,方便开发者适配不同的硬件数据格式或扩展新功能。

【硬件清单】

材料清单

  • 行空板k10 X1 链接
  • 二哈识图2 AI视觉传感器(HuskyLens 2) X1 链接
  • micro:bit掌控IO扩展板(兼容行空板M10/K10、micro:bit、掌控) X1 链接
  • USB3.0转Type-C数据线 X1 链接

步骤1 二哈识图2更新固件并设置:

1.更新固件:
https://wiki.dfrobot.com.cn/_SKU_SEN0638_Gravity_HUSKYLENS_2_AI_Camera_Vision_Sensor#7.%20%E5%9B%BA%E4%BB%B6%E6%9B%B4%E6%96%B0

image.png

2.开启二哈识图2,选择“物体追踪”功能。
 将镜头对准你想要用作控制器的物体(确保颜色或形状与背景对比明显),触摸屏框选目标物对其进行识别学习。
 确保其输出信息中包含物体框的中心点Y坐标。

步骤2 软件环境准备

软件环境准备:
在一台安装有Google Chrome 的电脑上,将本项目提供的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;
            font-family: 'Arial Rounded MT Bold', 'Arial', sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        
        .header {
            text-align: center;
            margin-bottom: 20px;
            width: 100%;
            max-width: 800px;
        }
        
        h1 {
            color: #4cc9f0;
            text-shadow: 0 0 10px rgba(76, 201, 240, 0.5);
            margin-bottom: 10px;
            font-size: 2.5rem;
        }
        
        .subtitle {
            color: #b8b8d1;
            margin-bottom: 15px;
            font-size: 1.1rem;
        }
        
        .game-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 100%;
            max-width: 800px;
            gap: 20px;
        }
        
        .control-panel {
            background: rgba(30, 30, 46, 0.8);
            border-radius: 15px;
            padding: 20px;
            width: 100%;
            border: 2px solid #4cc9f0;
            box-shadow: 0 0 15px rgba(76, 201, 240, 0.3);
        }
        
        .serial-controls {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin-bottom: 20px;
            align-items: center;
        }
        
        .serial-info {
            background: rgba(20, 20, 35, 0.9);
            border-radius: 10px;
            padding: 15px;
            margin-top: 10px;
        }
        
        .info-line {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            padding-bottom: 8px;
            border-bottom: 1px solid #33334d;
        }
        
        .info-line:last-child {
            margin-bottom: 0;
            border-bottom: none;
        }
        
        .info-label {
            color: #b8b8d1;
        }
        
        .info-value {
            color: #4cc9f0;
            font-weight: bold;
        }
        
        button {
            background: linear-gradient(to right, #4361ee, #3a0ca3);
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 50px;
            cursor: pointer;
            font-size: 1rem;
            font-weight: bold;
            transition: all 0.3s ease;
            box-shadow: 0 4px 10px rgba(67, 97, 238, 0.3);
        }
        
        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 15px rgba(67, 97, 238, 0.5);
            background: linear-gradient(to right, #4895ef, #4361ee);
        }
        
        button:active {
            transform: translateY(1px);
        }
        
        button:disabled {
            background: #555;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        
        #gameCanvas {
            border-radius: 10px;
            border: 3px solid #4cc9f0;
            box-shadow: 0 0 20px rgba(76, 201, 240, 0.4);
            background-color: #0d1b2a;
            display: block;
        }
        
        .instructions {
            background: rgba(30, 30, 46, 0.8);
            border-radius: 15px;
            padding: 20px;
            width: 100%;
            margin-top: 10px;
            border-left: 5px solid #f72585;
        }
        
        .instructions h3 {
            color: #f72585;
            margin-bottom: 10px;
        }
        
        .instructions ol {
            padding-left: 20px;
            line-height: 1.6;
        }
        
        .instructions li {
            margin-bottom: 8px;
        }
        
        .instructions code {
            background: rgba(20, 20, 35, 0.9);
            padding: 2px 6px;
            border-radius: 4px;
            font-family: monospace;
            color: #90e0ef;
        }
        
        .status {
            display: inline-block;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            margin-right: 8px;
            background-color: #f72585;
        }
        
        .status.connected {
            background-color: #4ade80;
            box-shadow: 0 0 8px #4ade80;
        }
        
        .game-stats {
            display: flex;
            justify-content: space-around;
            width: 100%;
            margin-top: 10px;
        }
        
        .stat-box {
            background: rgba(30, 30, 46, 0.8);
            border-radius: 10px;
            padding: 15px;
            text-align: center;
            flex: 1;
            margin: 0 5px;
            border-top: 4px solid #4361ee;
        }
        
        .stat-value {
            font-size: 2rem;
            font-weight: bold;
            color: #4cc9f0;
        }
        
        .stat-label {
            color: #b8b8d1;
            font-size: 0.9rem;
            margin-top: 5px;
        }
        
        @media (max-width: 768px) {
            .serial-controls {
                flex-direction: column;
                align-items: stretch;
            }
            
            button {
                width: 100%;
            }
            
            h1 {
                font-size: 2rem;
            }
            
            #gameCanvas {
                width: 95%;
                height: auto;
            }
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>🐦 物体追踪体感小鸟游戏</h1>
        <p class="subtitle">通过二哈识图2 + 行空板K10控制 | 使用Web Serial API读取串口数据</p>
    </div>
    
    <div class="game-container">
        <div class="control-panel">
            <h2>📡 串口连接控制</h2>
            <div class="serial-controls">
                <button id="connectBtn">
                    <span class="status" id="statusIndicator"></span>
                    连接串口
                </button>
                <button id="disconnectBtn" disabled>断开连接</button>
                <button id="startGameBtn" disabled>开始游戏</button>
                <button id="pauseGameBtn" disabled>暂停游戏</button>
                <button id="resetGameBtn">重置游戏</button>
            </div>
            
            <div class="serial-info">
                <div class="info-line">
                    <span class="info-label">连接状态:</span>
                    <span class="info-value" id="connectionStatus">未连接</span>
                </div>
                <div class="info-line">
                    <span class="info-label">串口数据:</span>
                    <span class="info-value" id="serialData">等待数据...</span>
                </div>
                <div class="info-line">
                    <span class="info-label">小鸟Y坐标:</span>
                    <span class="info-value" id="birdPosition">50%</span>
                </div>
                <div class="info-line">
                    <span class="info-label">物体追踪Y值:</span>
                    <span class="info-value" id="trackingY">-</span>
                </div>
            </div>
        </div>
        
        <canvas id="gameCanvas" width="800" height="500"></canvas>
        
        <div class="game-stats">
            <div class="stat-box">
                <div class="stat-value" id="score">0</div>
                <div class="stat-label">得分</div>
            </div>
            <div class="stat-box">
                <div class="stat-value" id="highScore">0</div>
                <div class="stat-label">最高分</div>
            </div>
            <div class="stat-box">
                <div class="stat-value" id="obstaclesPassed">0</div>
                <div class="stat-label">通过障碍</div>
            </div>
            <div class="stat-box">
                <div class="stat-value" id="gameSpeed">1.0</div>
                <div class="stat-label">游戏速度</div>
            </div>
        </div>
        
        <div class="instructions">
            <h3>🎮 游戏使用说明</h3>
            <ol>
                <li><strong>硬件连接</strong>: 确保二哈识图2已通过串口连接到行空板K10,并正确输出物体追踪的Y坐标数据。</li>
                <li><strong>数据格式</strong>: 行空板K10需要通过串口输出Y坐标值(如 <code>Y:125</code> 或 <code>{"y": 125}</code>)。你可能需要根据实际情况修改代码中的<code>parseSerialData</code>函数。</li>
                <li><strong>开始游戏</strong>: 点击"连接串口"选择行空板设备,连接成功后点击"开始游戏"。</li>
                <li><strong>游戏控制</strong>: 移动被追踪的物体(如彩色小球)来控制小鸟上下飞行,躲避障碍物。</li>
                <li><strong>游戏规则</strong>: 每通过一个障碍物得1分,游戏速度会逐渐增加。碰撞到障碍物或上下边缘则游戏结束。</li>
            </ol>
            <p style="margin-top: 10px; color: #f72585; font-weight: bold;">⚠️ 注意: Web Serial API仅在现代浏览器(Chrome/Edge 89+)中可用。如无法连接,请检查浏览器版本和权限设置。</p>
        </div>
    </div>

    <script>
        // 游戏状态变量
        let game = {
            running: false,
            score: 0,
            highScore: parseInt(localStorage.getItem('highScore')) || 0,
            obstaclesPassed: 0,
            speed: 1.0,
            birdY: 250,
            birdSize: 30,
            birdVelocity: 0,
            gravity: 0.2,
            lift: -5,
            obstacles: [],
            obstacleWidth: 60,
            gap: 150,
            frameCount: 0,
            obstacleFrequency: 120
        };
        
        // 串口相关变量
        let port = null;
        let reader = null;
        let readCancelled = false;
        let trackingY = 0;
        let lastTrackingY = 0;
        
        // DOM元素
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const connectBtn = document.getElementById('connectBtn');
        const disconnectBtn = document.getElementById('disconnectBtn');
        const startGameBtn = document.getElementById('startGameBtn');
        const pauseGameBtn = document.getElementById('pauseGameBtn');
        const resetGameBtn = document.getElementById('resetGameBtn');
        const statusIndicator = document.getElementById('statusIndicator');
        const connectionStatus = document.getElementById('connectionStatus');
        const serialData = document.getElementById('serialData');
        const birdPosition = document.getElementById('birdPosition');
        const trackingYElement = document.getElementById('trackingY');
        const scoreElement = document.getElementById('score');
        const highScoreElement = document.getElementById('highScore');
        const obstaclesPassedElement = document.getElementById('obstaclesPassed');
        const gameSpeedElement = document.getElementById('gameSpeed');
        
        // 初始化游戏
        function initGame() {
            game.running = false;
            game.score = 0;
            game.obstaclesPassed = 0;
            game.speed = 1.0;
            game.birdY = canvas.height / 2;
            game.birdVelocity = 0;
            game.obstacles = [];
            game.frameCount = 0;
            
            updateStats();
            draw();
        }
        
        // 更新统计显示
        function updateStats() {
            scoreElement.textContent = game.score;
            highScoreElement.textContent = game.highScore;
            obstaclesPassedElement.textContent = game.obstaclesPassed;
            gameSpeedElement.textContent = game.speed.toFixed(1);
            birdPosition.textContent = Math.round((game.birdY / canvas.height) * 100) + '%';
        }
        
        // 绘制游戏
        function draw() {
            // 清空画布
            ctx.fillStyle = '#0d1b2a';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 绘制背景网格
            drawGrid();
            
            // 绘制小鸟
            drawBird();
            
            // 绘制障碍物
            drawObstacles();
            
            // 绘制游戏状态信息
            drawGameInfo();
            
            // 如果游戏运行中,更新游戏状态
            if (game.running) {
                updateGame();
                requestAnimationFrame(draw);
            }
        }
        
        // 绘制背景网格
        function drawGrid() {
            ctx.strokeStyle = 'rgba(76, 201, 240, 0.1)';
            ctx.lineWidth = 1;
            
            // 垂直线
            for (let x = 0; x < canvas.width; x += 50) {
                ctx.beginPath();
                ctx.moveTo(x, 0);
                ctx.lineTo(x, canvas.height);
                ctx.stroke();
            }
            
            // 水平线
            for (let y = 0; y < canvas.height; y += 50) {
                ctx.beginPath();
                ctx.moveTo(0, y);
                ctx.lineTo(canvas.width, y);
                ctx.stroke();
            }
        }
        
        // 绘制小鸟
        function drawBird() {
            // 小鸟主体
            ctx.fillStyle = '#4cc9f0';
            ctx.beginPath();
            ctx.ellipse(100, game.birdY, game.birdSize, game.birdSize * 0.8, 0, 0, Math.PI * 2);
            ctx.fill();
            
            // 小鸟眼睛
            ctx.fillStyle = 'white';
            ctx.beginPath();
            ctx.arc(115, game.birdY - 5, 8, 0, Math.PI * 2);
            ctx.fill();
            
            ctx.fillStyle = 'black';
            ctx.beginPath();
            ctx.arc(118, game.birdY - 5, 4, 0, Math.PI * 2);
            ctx.fill();
            
            // 小鸟翅膀
            ctx.fillStyle = '#4895ef';
            ctx.beginPath();
            ctx.ellipse(90, game.birdY + 10, 15, 10, Math.PI / 4, 0, Math.PI * 2);
            ctx.fill();
            
            // 小鸟嘴巴
            ctx.fillStyle = '#f72585';
            ctx.beginPath();
            ctx.moveTo(130, game.birdY);
            ctx.lineTo(150, game.birdY - 5);
            ctx.lineTo(150, game.birdY + 5);
            ctx.closePath();
            ctx.fill();
        }
        
        // 绘制障碍物
        function drawObstacles() {
            game.obstacles.forEach(obstacle => {
                // 上障碍物
                ctx.fillStyle = '#4361ee';
                ctx.fillRect(obstacle.x, 0, game.obstacleWidth, obstacle.top);
                
                // 下障碍物
                ctx.fillRect(obstacle.x, canvas.height - obstacle.bottom, game.obstacleWidth, obstacle.bottom);
                
                // 障碍物边缘高光
                ctx.fillStyle = '#3a0ca3';
                ctx.fillRect(obstacle.x, obstacle.top - 10, game.obstacleWidth, 10);
                ctx.fillRect(obstacle.x, canvas.height - obstacle.bottom, game.obstacleWidth, 10);
                
                // 障碍物通过区域(间隙)
                ctx.fillStyle = 'rgba(76, 201, 240, 0.2)';
                ctx.fillRect(obstacle.x, obstacle.top, game.obstacleWidth, canvas.height - obstacle.top - obstacle.bottom);
            });
        }
        
        // 绘制游戏信息
        function drawGameInfo() {
            ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
            ctx.font = '16px Arial';
            ctx.fillText(`得分: ${game.score}`, 20, 30);
            ctx.fillText(`最高分: ${game.highScore}`, 20, 60);
            ctx.fillText(`速度: ${game.speed.toFixed(1)}x`, 20, 90);
            
            // 显示追踪数据
            ctx.fillText(`物体Y: ${trackingY}`, canvas.width - 150, 30);
            ctx.fillText(`小鸟Y: ${Math.round(game.birdY)}`, canvas.width - 150, 60);
            
            // 游戏状态提示
            if (!game.running && game.score === 0) {
                ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
                ctx.font = 'bold 24px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('连接串口并点击"开始游戏"', canvas.width / 2, canvas.height / 2);
                ctx.font = '18px Arial';
                ctx.fillText('用追踪物体控制小鸟上下飞行', canvas.width / 2, canvas.height / 2 + 30);
                ctx.textAlign = 'left';
            }
            
            if (!game.running && game.score > 0) {
                ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
                ctx.font = 'bold 32px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('游戏结束!', canvas.width / 2, canvas.height / 2 - 30);
                ctx.font = '24px Arial';
                ctx.fillText(`最终得分: ${game.score}`, canvas.width / 2, canvas.height / 2 + 10);
                ctx.fillText('点击"重置游戏"重新开始', canvas.width / 2, canvas.height / 2 + 50);
                ctx.textAlign = 'left';
            }
        }
        
        // 更新游戏状态
        function updateGame() {
            game.frameCount++;
            
            // 应用重力
            game.birdVelocity += game.gravity;
            game.birdY += game.birdVelocity * game.speed;
            
            // 基于物体追踪控制小鸟
            controlBirdFromTracking();
            
            // 边界检测
            if (game.birdY < game.birdSize) {
                game.birdY = game.birdSize;
                game.birdVelocity = 0;
            }
            
            if (game.birdY > canvas.height - game.birdSize) {
                game.birdY = canvas.height - game.birdSize;
                game.birdVelocity = 0;
            }
            
            // 生成新障碍物
            if (game.frameCount % Math.floor(game.obstacleFrequency / game.speed) === 0) {
                const top = Math.floor(Math.random() * (canvas.height - game.gap - 100)) + 50;
                const bottom = canvas.height - game.gap - top;
                game.obstacles.push({
                    x: canvas.width,
                    top: top,
                    bottom: bottom,
                    passed: false
                });
            }
            
            // 更新障碍物位置
            for (let i = game.obstacles.length - 1; i >= 0; i--) {
                const obstacle = game.obstacles[i];
                obstacle.x -= 3 * game.speed;
                
                // 检测是否通过障碍物
                if (!obstacle.passed && obstacle.x + game.obstacleWidth < 100) {
                    obstacle.passed = true;
                    game.score++;
                    game.obstaclesPassed++;
                    
                    // 每得5分增加速度
                    if (game.score % 5 === 0) {
                        game.speed += 0.1;
                    }
                    
                    updateStats();
                    
                    // 更新最高分
                    if (game.score > game.highScore) {
                        game.highScore = game.score;
                        localStorage.setItem('highScore', game.highScore.toString());
                        updateStats();
                    }
                }
                
                // 检测碰撞
                if (
                    100 + game.birdSize > obstacle.x &&
                    100 - game.birdSize < obstacle.x + game.obstacleWidth &&
                    (game.birdY - game.birdSize < obstacle.top || 
                     game.birdY + game.birdSize > canvas.height - obstacle.bottom)
                ) {
                    gameOver();
                    return;
                }
                
                // 移除屏幕外的障碍物
                if (obstacle.x + game.obstacleWidth < 0) {
                    game.obstacles.splice(i, 1);
                }
            }
        }
        
        // 基于物体追踪控制小鸟
        function controlBirdFromTracking() {
            // 如果有有效的追踪数据,使用它控制小鸟
            if (trackingY > 0) {
                // 将追踪Y坐标映射到画布范围
                // 假设追踪Y范围是0-240(二哈识图2常见范围),映射到画布高度
                const mappedY = (trackingY / 240) * canvas.height;
                
                // 平滑移动:小鸟向目标位置移动
                const targetY = mappedY;
                const diff = targetY - game.birdY;
                
                // 根据距离调整移动速度
                if (Math.abs(diff) > 5) {
                    game.birdVelocity = diff * 0.05;
                } else {
                    game.birdVelocity *= 0.9; // 减速
                }
                
                // 更新显示
                trackingYElement.textContent = trackingY;
            }
        }
        
        // 游戏结束
        function gameOver() {
            game.running = false;
            pauseGameBtn.disabled = true;
            startGameBtn.disabled = false;
            startGameBtn.textContent = '重新开始';
            draw();
        }
        
        // 串口数据解析函数 - 根据你的行空板输出格式修改此函数
        function parseSerialData(dataString) {
            // 移除换行符和空格
            dataString = dataString.trim();
            
            // 示例解析逻辑 - 根据你的实际数据格式修改
            // 格式1: "Y:125" (直接包含Y坐标)
            if (dataString.startsWith('Y:')) {
                const yValue = parseInt(dataString.substring(2));
                if (!isNaN(yValue) && yValue >= 0 && yValue <= 240) {
                    return yValue;
                }
            }
            
            // 格式2: "{"y": 125}" (JSON格式)
            if (dataString.startsWith('{')) {
                try {
                    const data = JSON.parse(dataString);
                    if (data.y !== undefined) {
                        const yValue = parseInt(data.y);
                        if (!isNaN(yValue) && yValue >= 0 && yValue <= 240) {
                            return yValue;
                        }
                    }
                } catch (e) {
                    console.log('JSON解析错误:', e);
                }
            }
            
            // 格式3: 纯数字 "125"
            const yValue = parseInt(dataString);
            if (!isNaN(yValue) && yValue >= 0 && yValue <= 240) {
                return yValue;
            }
            
            return null; // 无法解析的数据
        }
        
        // 读取串口数据
        async function readSerialData() {
            if (!port || !port.readable) {
                console.error('串口不可读');
                return;
            }
            
            try {
                reader = port.readable.getReader();
                readCancelled = false;
                
                while (!readCancelled && port.readable) {
                    try {
                        const { value, done } = await reader.read();
                        
                        if (done) {
                            console.log('串口读取完成');
                            break;
                        }
                        
                        if (value) {
                            // 将Uint8Array转换为字符串
                            const textDecoder = new TextDecoder();
                            const dataString = textDecoder.decode(value);
                            
                            // 更新数据显示
                            serialData.textContent = dataString.length > 30 ? 
                                dataString.substring(0, 30) + '...' : dataString;
                            
                            // 解析数据获取Y坐标
                            const parsedY = parseSerialData(dataString);
                            if (parsedY !== null) {
                                trackingY = parsedY;
                                lastTrackingY = trackingY;
                            }
                        }
                    } catch (error) {
                        console.error('读取数据时出错:', error);
                        if (!readCancelled) {
                            await disconnectSerial();
                        }
                        break;
                    }
                }
            } catch (error) {
                console.error('初始化读取器时出错:', error);
            } finally {
                if (reader) {
                    reader.releaseLock();
                    reader = null;
                }
            }
        }
        
        // 连接串口
        async function connectSerial() {
            try {
                // 请求串口访问权限
                port = await navigator.serial.requestPort();
                
                // 打开串口,设置波特率(根据你的行空板设置调整)
                await port.open({ baudRate: 115200 });
                
                // 更新UI状态
                statusIndicator.classList.add('connected');
                connectionStatus.textContent = '已连接';
                connectionStatus.style.color = '#4ade80';
                connectBtn.disabled = true;
                disconnectBtn.disabled = false;
                startGameBtn.disabled = false;
                
                console.log('串口连接成功');
                
                // 开始读取数据
                readSerialData();
                
            } catch (error) {
                console.error('连接串口时出错:', error);
                alert('连接串口失败: ' + error.message);
                await disconnectSerial();
            }
        }
        
        // 断开串口连接
        async function disconnectSerial() {
            readCancelled = true;
            
            if (reader) {
                try {
                    await reader.cancel();
                } catch (error) {
                    console.error('取消读取时出错:', error);
                }
                reader.releaseLock();
                reader = null;
            }
            
            if (port) {
                try {
                    await port.close();
                } catch (error) {
                    console.error('关闭串口时出错:', error);
                }
                port = null;
            }
            
            // 更新UI状态
            statusIndicator.classList.remove('connected');
            connectionStatus.textContent = '未连接';
            connectionStatus.style.color = '#f72585';
            connectBtn.disabled = false;
            disconnectBtn.disabled = true;
            startGameBtn.disabled = true;
            serialData.textContent = '等待数据...';
            trackingYElement.textContent = '-';
            
            console.log('串口已断开');
        }
        
        // 开始游戏
        function startGame() {
            if (!game.running) {
                game.running = true;
                startGameBtn.disabled = true;
                pauseGameBtn.disabled = false;
                
                // 如果游戏已结束,重置游戏
                if (game.score > 0) {
                    initGame();
                }
                
                requestAnimationFrame(draw);
            }
        }
        
        // 暂停游戏
        function pauseGame() {
            game.running = !game.running;
            pauseGameBtn.textContent = game.running ? '暂停游戏' : '继续游戏';
            startGameBtn.disabled = game.running;
            
            if (game.running) {
                requestAnimationFrame(draw);
            }
        }
        
        // 事件监听器
        connectBtn.addEventListener('click', connectSerial);
        disconnectBtn.addEventListener('click', disconnectSerial);
        startGameBtn.addEventListener('click', startGame);
        pauseGameBtn.addEventListener('click', pauseGame);
        resetGameBtn.addEventListener('click', initGame);
        
        // 键盘控制备用方案(如果串口不可用)
        document.addEventListener('keydown', (e) => {
            if (e.code === 'Space' || e.code === 'ArrowUp') {
                game.birdVelocity = game.lift;
            }
        });
        
        // 触摸控制(移动设备)
        canvas.addEventListener('touchstart', (e) => {
            e.preventDefault();
            game.birdVelocity = game.lift;
        });
        
        canvas.addEventListener('mousedown', (e) => {
            game.birdVelocity = game.lift;
        });
        
        // 初始化游戏
        initGame();
        updateStats();
        
        // 检查浏览器是否支持Web Serial API
        if (!('serial' in navigator)) {
            alert('您的浏览器不支持Web Serial API。请使用Chrome 89+或Edge 89+版本。');
            connectBtn.disabled = true;
            connectBtn.textContent = '浏览器不支持';
        }
        
        console.log('游戏初始化完成。请确保行空板K10通过串口输出物体追踪Y坐标数据。');
        console.log('根据你的行空板输出格式,可能需要修改代码中的parseSerialData函数。');
    </script>
</body>
</html>

步骤3 【程序编写】

【程序编写】image.png

 


   

步骤4 运行方式:文件代码和脚本全部打包,快去试试吧

双击打开本地的HTML文件。
点击页面上“连接串口”按钮,在浏览器弹出的设备选择器中,选择对应的行空板K10串口。
连接成功后,移动被二哈识图2追踪的物体,即可开始控制小鸟。点击“开始游戏”按钮,挑战自我!

评论

user-avatar