零知派ESP32--基于HW-504从零搭建双人对战贪吃蛇教程

发布时间:2026/6/27 22:11:02
零知派ESP32--基于HW-504从零搭建双人对战贪吃蛇教程 项目概述本项目基于 零知派ESP32使用 ST7789 240×240 TFT 显示屏和摇杆控制器实现贪吃蛇游戏的单人模式和双人对战模式。单人模式经典贪吃蛇边界死亡、自撞死亡吃好食物得分并生成坏食物速度随分数递增对战模式双人通过 MQTT 联网对战穿墙、无自撞、加速/刹车机制碰撞对方蛇身即获胜项目亮点亮点说明双人 MQTT 联网对战基于 EMQX 公共 Broker无需自建服务器加速/刹车机制同方向长按 500ms 加速前进缩尾反方向长按 500ms 刹车原地缩尾串扰消除摇杆物理串扰通过校准中心值 偏差比较自动修正局部重绘非全屏刷新仅擦除/绘制变化区域提高帧率ADC1 专用引脚摇杆接 ADC1 通道避免与 WiFi 冲突对战同步MQTT 消息同步双方蛇身、食物位置支持碰撞判定项目难点及解决方案难点 1摇杆串扰现象推动一个方向时另一轴也会产生读数偏移导致方向误判。解决方案启动时采样 20 次取平均校准摇杆物理中心值 (joyCenterX/Y)对角线方向同时触发时比较两轴偏移量取偏移更大的方向方向阈值相对于校准中心动态计算±1000消除左右/上下灵敏度差异难点 2WiFi 与 ADC 冲突现象ESP32 的 ADC2 与 WiFi 共用硬件模块WiFi 开启后 ADC2 读数异常。解决方案摇杆 X/Y 使用 ADC1 引脚GPIO 33、32SW 用 GPIO 27普通 GPIOINPUT_PULLUP。难点 3加速/刹车防误触现象摇杆轻微抖动导致意外触发加速/刹车。解决方案使用 500ms 长按确认机制boostArmed/brakeArmed标志位激活后持续生效不再响应反方向等噪音松开摇杆回到中心区才取消加速/刹车状态并复位标志目录一、硬件系统部分1.1 硬件清单1.2 接线方案1.3 硬件连接图1.4 实物连接图二、软件架构设计2.1 系统初始化2.2 主循环逻辑三、代码拆分讲解3.1 核心常量与配置3.2 数据结构3.3 摇杆读取与方向处理3.4 MQTT 通信3.5 加速/刹车机制3.6 食物与坏食物机制3.7 局部重绘3.8 蛇身皮肤3.9 按键消抖四、操作过程及数据展示4.1 操作步骤4.2 演示视频五、技术原理5.1 工作原理5.2 工作模式配置六、常见问题指引Q1摇杆方向不准/串扰严重Q2WiFi 连接失败Q3MQTT 连不上Q4对战双方看不到对方Q5屏幕闪烁 / 刷新慢Q6加速/刹车无法触发Q7对战模式进入后立刻游戏结束一、硬件系统部分1.1 硬件清单组件数量说明零知派ESP32 开发板1写入程序零知派ESP32扩展板1方便接线ST7789 TFT 显示屏1240×240SPI 接口双轴摇杆模块1操控游戏方向杜邦线若干母对母Micro USB 数据线1供电 烧录第二套相同硬件1双人对战需要两个设备1.2 接线方案摇杆模块摇杆引脚ESP32 GPIO说明VRXGPIO 33X 轴ADC1VRYGPIO 32Y 轴ADC1SWGPIO 27按键内置上拉VCC3.3V供电GNDGND接地1.3 硬件连接图注意GPIO 33、32 是 ADC1 通道不会与 WiFi 冲突。GPIO 27 作为 SW 输入必须使用INPUT_PULLUP摇杆按键按下 LOW。1.4 实物连接图二、软件架构设计2.1 系统初始化setup() ├── Serial.begin(115200) ├── tft.init() ├── tft.setRotation(3) ├── tft.fillScreen(BG_COLOR) ├── drawBorder() // 蓝色边框 ├── pinMode(JOY_VRX, INPUT) ├── pinMode(JOY_VRY, INPUT) ├── pinMode(JOY_SW, INPUT_PULLUP) ├── 随机种子生成 (analogRead(34)×20 micros()) ├── 摇杆中心校准 (20 次采样平均) ├── msgNonce esp_random() // 消息唯一 ID ├── mqttClient.setServer(...) ├── mqttClient.setCallback(...) ├── mqttClient.setKeepAlive(60) ├── wifiClient.setTimeout(800) └── drawMenu() // 显示菜单2.2 主循环逻辑loop() ├── MQTT 处理对战模式每 50ms │ ├── reconnectMQTT() // 每 5s 尝试重连 │ └── mqttClient.loop() // 处理回调 │ └── switch(phase) ├── PHASE_MENU → handleMenu() │ ├── 摇杆左/右切换模式 (Single / Battle) │ ├── 摇杆上/下切换皮肤 (SOLID / GRADIENT) │ └── SW 确认 │ ├── 对战模式phase PHASE_CONNECT → setupWiFi() │ └── 单人模式phase PHASE_PLAY → initGame() │ ├── PHASE_CONNECT → handleConnecting() │ ├── 每 3s 刷新 Connecting WiFi... 文字 │ └── WiFi 连接成功 → phase PHASE_PLAY → initGame() │ ├── PHASE_PLAY → handleGame() │ ├── readControls() // 读取摇杆 → 方向/加速/刹车 │ ├── 检查移动计时器 │ │ ├── 刹车模式interval base × 3, applyBrake() │ │ ├── 加速模式interval base, applyBoost() │ │ └── 常规移动interval base, moveSnake() │ ├── draw() // 局部重绘 │ └── publishPosition() // 对战模式每 60ms 广播 │ └── PHASE_OVER → handleGameOver() └── SW 确认 → 返回菜单三、代码拆分讲解3.1 核心常量与配置#define GRID_W 23 // 网格宽度格数 #define GRID_H 23 // 网格高度 #define CELL_SIZE 10 // 每格像素 #define BORDER_WIDTH 5 // 边框宽度 #define GOOD_FOOD_COUNT 5 // 好食物数量 #define MAX_BAD_FOOD 20 // 最大坏食物数量 #define JOY_THRESH 1000 // 摇杆方向判定阈值 #define MENU_THRESH 800 // 菜单选择阈值更灵敏 #define HOLD_MS 500 // 长按确认时间 #define SW_DEBOUNCE_MS 50 // 按键消抖时间 #define MIN_SNAKE_LEN 2 // 最小蛇长度刹车/加速下限网格 23×23每格 10px加上 5px 边框正好填满 240×240 屏幕。3.2 数据结构enum Dir { STOP, UP, DOWN, LEFT, RIGHT }; Dir dir STOP, nextDir STOP; // 当前方向 / 缓冲方向 int joyCenterX 2048, joyCenterY 2048; // 摇杆校准中心 uint32_t msgNonce; // 消息唯一 ID替代 playerID bool battleMode; // 对战模式标志 uint8_t snakeX[MAX_LEN], snakeY[MAX_LEN]; // 蛇身坐标 (0蛇头) uint8_t oppX[MAX_LEN], oppY[MAX_LEN]; // 对手蛇身坐标 int oppLen, prevOppLen; // 对手长度 上一帧长度擦除用 bool oppActive; // 对手是否活跃 uint8_t foodX[GOOD_FOOD_COUNT], foodY[GOOD_FOOD_COUNT]; // 好食物 uint8_t badFoodX[MAX_BAD_FOOD], badFoodY[MAX_BAD_FOOD]; // 坏食物 int badFoodCount; int prevTailX, prevTailY; // 上一帧蛇尾擦除用 int extraEraseX, extraEraseY; // 额外擦除坐标坏食物/加速缩尾 bool justAte; // 刚吃食物尾部不擦除 bool boostMode, brakeMode; // 加速/刹车状态 bool boostArmed, brakeArmed; // 长按预激活标志 unsigned long boostStart, brakeStart;关键设计nextDir和dir分离使方向变更与移动解耦防止一帧内多次转向prevTailX/Y记录上一帧蛇尾坐标仅擦除变化区域extraEraseX/Y处理加速缩尾和坏食物移除时的额外擦除boostArmed/brakeArmedHOLD_MS实现 500ms 长按确认3.3 摇杆读取与方向处理void readControls() { int devX vrx - joyCenterX; int devY vry - joyCenterY; // 松杆 → 取消所有状态 if (!left !right !up !down) { boostMode brakeMode false; boostArmed brakeArmed false; return; } // 对角线串扰消除比较 absDevX vs absDevY取大者 Dir joyDir (absDevX absDevY) ? 横向 : 纵向; // 对战模式 // 同方向 → 预激活加速500ms 后 boostModetrue // 反方向 → 预激活刹车500ms 后 brakeModetrue // 单人模式禁止反方向转头不支持加速/刹车 }串扰消除逻辑双轴同时触发时比较abs(devX)和abs(devY)偏移量更大的轴被认为是真实意图避免了对角线推动时的方向卡顿和误判3.4 MQTT 通信消息格式POS|nonce|len|x1,y1|x2,y2|...|G5|fx1,fy1|...|Bn|bx1,by1|... LOSE|nonce每条消息携带消息唯一 ID (msgNonce) 防止处理自己的消息。接收处理解析POS消息 → 更新对手蛇身位置、好食物位置、坏食物位置检查对手蛇头是否碰撞我方蛇身 → 若碰撞则我方胜利 (gameWon true)解析LOSE消息对方触发 → 显示 “YOU WIN!”设置redrawPending true触发全量重绘发送处理(publishPosition, 第 264-286 行)每 60ms 广播一次MQTT_PUB_INTERVAL最多发送 100 节蛇身同时发送食物位置保证双方食物一致碰撞判定(moveSnake, 第 729-748 行)我方蛇头 vs 对手蛇身 → 若 i0头碰头比较长度长者胜否则撞击方失败失败方自动发送LOSE消息通知对方3.5 加速/刹车机制同方向按住 ≥ 500ms → boostMode true ├── applyBoost(): 正常前进 尾部减一节 ├── 速度 baseInterval (150ms最快) └── 释放摇杆 → 取消 反方向按住 ≥ 500ms → brakeMode true ├── applyBrake(): 头部暂停 尾部减一节 (原地缩回) ├── 速度 baseInterval × 3 (450ms最慢) └── 释放摇杆 → 取消单人模式下不启用加速/刹车boostMode和brakeMode始终为false。3.6 食物与坏食物机制好食物场上固定 5 个吃掉后蛇身 1继承上一帧尾部位置得分 1单人模式移动速度加快moveInterval - 3最低 60ms单人模式生成一个新坏食物不超过 20 个上限立即在空位生成新好食物坏食物单人模式初始 0 个每次吃好食物或坏食物时可能增加不超过 20对战模式初始 3 个固定数量被吃后不会补充吃到坏食物蛇身 -1不低于 2移除该坏食物3.7 局部重绘单人模式 1. 若刚吃掉食物 (justAtetrue)保留尾部蛇已变长 否则擦除上一帧蛇尾 (prevTailX, prevTailY) 2. 擦除 extraEraseX/Y坏食物移除或加速缩尾需要额外擦除 3. 绘制蛇身 (1..snakeLen-1) 4. 绘制蛇头含白色眼睛 5. 绘制所有食物 6. 更新记分 对战模式 1. 同上擦除蛇尾和额外擦除 2. 擦除上一帧对手蛇身 (prevOppX/Y) 3. 绘制我方蛇身 蛇头 4. 绘制对手蛇身橙色身体 红色头 5. 重绘蓝色边框对手可能覆盖边框 6. 绘制所有食物 7. 保存当前对手坐标到 prevOpp供下一帧擦除3.8 蛇身皮肤SKIN_SOLID蛇头 TFT_GREEN身体 TFT_DARKGREEN SKIN_GRADIENT蛇头绿色 → 身体渐变为蓝色 (RGB: 0, 255→0, 0→255)3.9 按键消抖bool isSWPressed() { if (digitalRead(JOY_SW) LOW) { if (!swWaiting) { swWaiting true; swPressTime now; } } else { if (swWaiting now - swPressTime SW_DEBOUNCE_MS) { swWaiting false; return true; // 上升沿触发消抖通过 } swWaiting false; } return false; }采用上升沿触发 消抖延时按下时记录时间松开时检查是否超过 50ms确保一次完整的按下-松开动作才触发。四、操作过程及数据展示4.1 操作步骤单人模式上电 → 屏幕显示菜单摇杆左/右切换至Mode: Single摇杆上/下切换皮肤SOLID / GRADIENT按下摇杆SW开始游戏摇杆控制方向不可反向转头撞墙或自撞 → GAME OVER → 按 SW 返回菜单对战模式两个 ESP32 分别上电各自摇杆左/右切换至Mode: Battle按 SW → 屏幕显示Connecting WiFi...WiFi 连接成功后自动进入游戏双方通过 MQTT 同步数据同方向按住 ≥ 500ms → 加速前进缩尾反方向按住 ≥ 500ms → 刹车原地缩尾蛇头撞到对方身体 → 对方获胜头碰头时长者胜界面说明菜单SNAKE Mode: Battle ← L/R 切换 Skin: SOLID ← U/D 切换 L/R: Mode U/D: Skin Press SW to start游戏界面单人Score: 5 ← 蓝色边框上的记分 ┌─────────────────────┐ │ · · · · · · │ │ ■■■■ ● ●● │ ← 绿色蛇身 │ ■■■■ ● ● ●● │ │ ● ● ● ● · │ ← 红色好食物 │ · · ● ● ● │ ← 黑色坏食物 └─────────────────────┘游戏界面对战Sc:3 OPP ← 对战模式显示对手状态 ┌─────────────────────┐ │ · · · · · · │ │ ■■■■ ● ●● │ ← 绿色我方 │ ■■■■ ● ● ●● │ ← 灰色对手身体 │ ● ● ● ● · │ ← 黑色对手蛇头 │ · · ● ● ● │ ← 红色方块好食物 └─────────────────────┘4.2 演示视频零知派ESP32--基于HW-504双人对战贪吃蛇教程五、技术原理5.1 工作原理┌───────────┐ 摇杆模拟信号 ┌─────────┐ │ 摇杆模块 │─────────────────→│ ESP32 │ │ (VRX/VRY) │ ADC1 读取 │ ADC1 │ └───────────┘ │ GPIO33/32│ │ │ ┌───────────┐ 数字信号 │ GPIO27 │ │ 摇杆按键 │─────────────────→│ (INPUT │ │ (SW) │ 按下 LOW │ _PULLUP)│ └───────────┘ │ │ │ 逻辑处理 ┌────────────┐ ┌───────────┐ SPI 总线 │ ←──────────→ │ TFT 显示屏 │ │ ST7789 │←─────────────────│ drawBorder/ │ 240×240 │ │ 显示屏 │ GPIO 18/23/4/13/15│ drawSnake/ │ │ └───────────┘ │ drawFood └────────────┘ │ │ │ WiFi MQTT ┌────────────┐ │ ←──────────→ │ EMQX 公共 │ │ broker.emqx.io│ MQTT Broker│ │ 1883 │ │ │ topic: ovoreceive │ └─────────────────┘数据流摇杆模拟信号 → ADC1 转换 →readControls()判断方向/加速/刹车方向 定时器 →moveSnake()更新蛇身坐标含碰撞检测坐标变化 →draw()局部重绘显示屏对战模式下 →publishPosition()每 60ms 通过 MQTT 广播坐标 食物同时接收对方坐标 →mqttCallback()更新对手状态及食物同步碰撞检测头碰身/头碰头→ 触发LOSE消息 → 双方显示游戏结束5.2 工作模式配置配置项单人模式对战模式边界规则死亡穿墙wrap-around自撞规则死亡忽略摇杆反方向忽略刹车500ms 长按同方向长按忽略加速500ms 长按坏食物初始03坏食物补充每次吃食物 1上限 20被吃后不补充基础速度150ms每吃一食物 -3ms最低 60ms固定 150ms加速速度—150ms前进缩尾刹车速度—450ms原地缩尾连接需求无WiFi MQTT运动检测仅判断方向额外检测 boost/brake 状态碰撞判定撞墙 自撞撞对方蛇身 / 头碰头长者胜六、常见问题指引Q1摇杆方向不准/串扰严重代码已在启动时自动校准摇杆中心值。如果仍有问题确保启动时摇杆处于自由状态无人手触碰检查 VRX/VRY 是否接在 ADC1 引脚GPIO 33、32阈值 1000 过于灵敏可适当增大JOY_THRESHsnake_game.ino:40Q2WiFi 连接失败确认 Wi-Fi 名称和密码正确snake_game.ino:7-8ESP32 仅支持 2.4GHz 频段如果路由器在中国某些公共 Wi-Fi 可能要求 Portal 认证Q3MQTT 连不上检查网络连通性ESP32 能否连接broker.emqx.io查看串口输出Serial.begin(115200)已启用某些网络环境防火墙屏蔽 1883 端口可尝试切换 WebSocket需修改代码Q4对战双方看不到对方确认两个 ESP32 连接到同一 MQTT Broker检查 Topic 一致均为ovoreceive检查msgNonce不同系统已用esp_random()随机生成MQTT 消息有 60ms 节流位置更新最多每秒 16 次Q5屏幕闪烁 / 刷新慢使用局部重绘已实现避免全屏fillScreenSPI 频率设置为 40MHz理论上足够Q6加速/刹车无法触发需要同方向加速或反方向刹车按住 ≥ 500ms不是快速拨动需要摇杆偏离中心区阈值 ±1000加速/刹车激活后摇杆抖动不会取消必须释放回中心区单人模式不启用加速/刹车Q7对战模式进入后立刻游戏结束检查双方是否连接到同一 MQTT 主题检查msgNonce是否相同极小概率重启即可解决确认代码中 SSID 和密码正确且 ESP32 接入同一网络