UNIHIKER K10小游戏——贪吃蛇

2026-10-151

项目简介

这是一个跑在 DFRobot UNIHIKER K10 上的贪吃蛇游戏。

UNIHIKER K10小游戏——贪吃蛇_image_1.webp

听起来平平无奇——直到你发现 K10 全身上下只有 A、B 两个按键,而经典贪吃蛇需要上下左右四个方向键。

这个项目要解决的问题就是:如何用两个按键,做出一个手感不别扭的贪吃蛇?

我的答案参考了早年诺基亚手机上贪吃蛇的做法——A 键顺时针旋转(上→右→下→左→上),左边、右边各转一次,连续转三次就掉头(但被防掉头逻辑拦住,不会自杀)。玩五分钟就能形成肌肉记忆,手感意外地顺。
UNIHIKER K10小游戏——贪吃蛇_image_2.webp

项目还顺手解决了 K10 做实时游戏时的两个经典坑:整屏闪烁 和 按键迟钝。这两个坑的解法(弃用 canvasClear()、按键检测上双核)在 K10 上做任何交互类项目都能复用。

适合谁看:手上有 K10、想玩玩屏上实时交互的朋友;对 ESP32-S3 双核任务分工、FreeRTOS 在 Arduino 环境里怎么用感兴趣的朋友。
UNIHIKER K10小游戏——贪吃蛇_image_3.webp


一、硬件 & 软件清单

硬件

名称 型号 / 规格 数量 说明
主控板 DFRobot UNIHIKER K10 1 ESP32-S3,2.8" 240×320 彩屏,板载 A/B 双键、麦克风、扬声器、摄像头
数据线 USB Type-C 1 供电 + 烧录,建议用带数据传输的线(纯充电线识别不到串口)

就这两样。不需要额外接线、不需要面包板、不需要任何外设——这正是 K10 好玩的地方,开机即用。

软件

名称 版本 / 说明
Arduino IDE 1.8.x 或 2.x 均可
K10 开发板包 unihiker_k10(DFRobot 官方 Arduino 包,含屏幕、按键、传感器驱动)
ESP32 核心包 esp32 by Espressif(K10 包会依赖它)
依赖库 FreeRTOS(ESP32 核心包自带,无需额外安装)

用到的板载资源

资源 用途
2.8" LCD(240×320) 游戏画面显示
按键 A 顺时针转向 / 结束后重开
按键 B 加速冲刺(按住生效)
按键 AB 组合 暂停 / 继续

📌 提示:K10 的官方 Arduino API 里没有开放触摸接口,所以本项目完全靠按键操作。屏幕虽然是触摸屏,但只能通过按键玩,这点要提前有心理准备。


二、制作过程

整个项目只有一个 .ino 文件,烧进去就能跑。下面是完整流程。

第 1 步:搭环境

  1. 安装 Arduino IDE。
  2. 在 开发板管理器 里搜索安装 esp32 by Espressif。
  3. 在 库管理器 里搜索安装 DFRobot UNIHIKER K10 包(搜 unihiker 也能找到)。
  4. 开发板选择 UNIHIKER K10,端口选好(插上设备后如果看不到端口,换一根带数据功能的 USB 线)。

第 2 步:建工程、粘贴代码

新建一个 sketch,把附件里的 k10_snake_game.ino 全部内容粘进去,注意要粘全,别重复粘两遍(Arduino 会把同一个 .ino 里的内容当一份编译,重复粘贴必然报一堆 redefinition 错)。

第 3 步:编译烧录

点上传,等进度条走完。第一次编译会慢一些(要编译 ESP32 核心),之后会快很多。

第 4 步:开玩

上电后先出现一个开机说明页,列出按键映射:

SNAKE
K10 EDITION

A  =  turn
B  =  turbo
AB =  pause

press A to start

按一下 A 开始游戏。之后:

操作 效果
点按 A 顺时针转 90°(上→右→下→左→上)
按住 B 加速冲刺,松开恢复(有风险,慎用)
AB 同按 暂停 / 继续
结束后点 A 重开一局

第 5 步:想调手感?改这几个数

代码顶部有一段"可调参数",想改难度改这里就行:

#define CELL        12           // 每格像素大小(越小格子越密)
#define START_SPEED 220          // 初始速度:数值越大越慢
#define MIN_SPEED   80           // 速度上限(最快能多快)
#define SPEED_STEP  6            // 每吃一个食物加快多少 ms
#define TURBO_SPEED 70           // 按住 B 的冲刺速度

想玩更从容的:START_SPEED 调到 280,SPEED_STEP 调到 3。
想玩更刺激的:START_SPEED 调到 160,SPEED_STEP 调到 10。


三、技术原理

只讲三个核心点,讲清楚就够用了。

3.1 两个按键怎么控制四个方向

核心是一句向量旋转:

int tx = -dirY;      // 顺时针旋转 90 度
int ty =  dirX;

拿方向向量代进去验证一下就明白了:

当前方向 dirX, dirY 旋转后 tx, ty 结果
上 (0, -1) (1, 0) 右 ✔
右 (1, 0) (0, 1) 下 ✔
下 (0, 1) (-1, 0) 左 ✔
左 (-1, 0) (0, -1) 上 ✔

完美顺时针循环,一行代码搞定。

那怎么掉头?连续按三次 A 就是掉头(转 270°)。但贪吃蛇不能 180° 掉头——那会瞬间撞到自己的脖子。所以有这一道保险:

bool isOpposite(int nx, int ny) {
  return (nx == -dirX && ny == -dirY);   // 新方向正好是当前方向的相反数?
}

只要检测到是反向,就直接丢弃这次转向请求,蛇继续按原方向走。这样连续狂按 A 也不会自杀。

3.2 其他几个小设计

  • 自动加速:每吃一个食物,speedMs 减 SPEED_STEP,但要 check 不低于 MIN_SPEED。蛇越长跑越快,难度自然上涨。
  • 食物生成避开蛇身:spawnFood() 里最多试 300 次随机位置,只要不落在蛇身上就用;实在找不到(蛇快占满屏了)就凑合放。
  • 撞自己检测的细节:判断是否撞到尾巴时要区分——如果这一口会吃到食物,尾巴不会移动,所以尾巴也算障碍;反之尾巴正要挪走,那一格是安全的。代码里用 checkLen = willEat ? snakeLen : snakeLen - 1 处理。
  • 开机等待防抖:进游戏前有 delay(300),避免你为了开始游戏按的那一下 A 被当成第一次转向。

四、代码注释说明

完整代码见附件 k10_snake_game.ino(约 500 行,含详细中文注释)。这里把文件结构列一下,方便对照阅读:

位置 内容
文件头注释 操作说明、游戏规则、API 兼容性警告、两个性能问题的记录
可调参数 格子大小、速度相关的一堆宏,改难度就改这里
游戏状态 蛇数组、方向向量、分数、速度、标志位
颜色定义 全部 RGB888 常量,改配色一目了然
handleButtons() 按键处理(跑在 core 0),只改标志位
keyTask() 按键采样任务,5ms 一轮
moveSnake() 移动一步 + 撞墙/撞自己判定 + 吃食物长大
draw() / drawSnake() / drawFood() / drawPanel() 绘制相关
drawOverlay() 暂停 / Game Over 覆盖层
showStartScreen() 开机说明页
setup() / loop() 初始化和主循环

代码里特别标注的两处:

  1. API 兼容性警告:代码用了 7 参数版 canvasText(文本, x, y, 颜色, 字体, number, flag),这个版本在 DFRobot 社区例程里广泛使用且实测可用,但官方 API 文档只写了 3 参数版。万一你编译报 no matching function for call to canvasText,说明库版本较老——文件头注释里写了改法(改用行号定位,行号 ≈ y / 20)。
  2. 跨核铁律:handleButtons() 上方专门写了注释说明"这里只改标志位,不动 snake[],真正改数组的操作交给主循环"——这条是双核编程的安全底线,别拆。

五、图片 / 视频展示

UNIHIKER K10小游戏——贪吃蛇_image_4.webp


六、完整代码

/*
 * ============================================================
 *  UNIHIKER K10 贪吃蛇 (Snake Game)
 * ============================================================
 *
 *  硬件:DFRobot UNIHIKER K10(ESP32-S3,2.8寸 240x320 彩屏)
 *  依赖:unihiker_k10.h(K10 官方 Arduino 包自带)
 *
 *  ------------------------------------------------------------
 *  操作说明(K10 只有 A/B 两个按键,所以做了巧妙的映射):
 *
 *    【按键 A】顺时针转向  —— 上→右→下→左→上,循环
 *    【按键 B】加速冲刺(按住不放,速度翻倍)
 *    【AB 同按】暂停 / 继续
 *
 *  为什么这样设计?
 *    经典贪吃蛇需要 4 个方向键,K10 只有 2 个。用"A 键旋转"
 *    是移动端贪吃蛇的经典做法(类似早年诺基亚手机),
 *    习惯了会非常顺手:想左转就按一下 A。
 *
 *  ------------------------------------------------------------
 *  游戏规则:
 *    - 吃到红色食物 +10 分,蛇身变长
 *    - 撞墙 或 撞到自己身体 = 游戏结束
 *    - 蛇越长速度越快(自动加速,增加难度)
 *    - 最高分记录在内存中,重启后清零
 *
 *  ------------------------------------------------------------
 *  ⚠️ 重要:API 兼容性说明(万一编译报错的解决办法)
 *
 *  本代码用到了 canvasText 的【7 参数完整版】:
 *      canvasText(文本, x, y, 颜色, 字体枚举, number, flag)
 *
 *  但官方 API 文档里只写了 3 参数版:
 *      canvasText(文本, 行号, 颜色)
 *
 *  7 参数版在 DFRobot 社区官方例程中广泛使用,实测可用;
 *  如果你编译报错 "no matching function for call to canvasText",
 *  说明你的库版本较老,按下面方式改即可:
 *
 *    把   k10.canvas->canvasText("文字", x, y, 颜色, eCNAndENFont16, 100, true);
 *    改成 先算行号再调用(行号 = y / 20 左右)
 *        k10.canvas->canvasText("文字", y / 20, 颜色);
 *
 *  ------------------------------------------------------------
 *  🔧 两个关键性能问题的解法(改版记录)
 *
 *  【问题 1】整屏闪烁
 *    原因:canvasClear() 会把画布清空,与随后的重绘之间出现空屏,
 *          肉眼看到的就是闪一下。
 *    解法:完全弃用 canvasClear(),改用 canvasRectangle 铺底色覆盖。
 *          效果一样,但不会有"清空→重绘"的时间差。
 *
 *  【问题 2】按键迟钝,要长按才有反应
 *    原因:主循环一轮要重绘整屏 + updateCanvas 推屏,耗时几百毫秒,
 *          按键只在每轮开头检测一次 → 采样率只有几 Hz,
 *          你短按一下,那一瞬间根本没被采样到。
 *    解法:把按键检测放进独立的 FreeRTOS 任务,钉在 core 0,
 *          每 5ms 采样一次(200Hz)。绘制留在 core 1。
 *          这跟雷达项目用双核解决卡顿是同一个思路。
 *
 *    ⚠️ 跨核注意:keyTask 运行在 core 0,只修改 volatile 标志位,
 *       不直接改 snake[] 数组;真正改数组的操作(resetGame、应用转向)
 *       都由 core 1 的主循环执行,避免数据竞争。
 *
 *  ------------------------------------------------------------
 *  📐 已确认的 API 签名(来自官方文档,可放心使用):
 *
 *    canvasClear()                                    // 清屏,无参数(本作已弃用)
 *    canvasLine(x1, y1, x2, y2, color)                // 画线
 *    canvasPoint(x, y, color)                         // 画点
 *    canvasCircle(x, y, r, color, bg_color, fill)     // 圆心+半径
 *    canvasRectangle(x, y, w, h, color, bg_color, fill) // ⚠️ 是宽高不是对角坐标!
 *    updateCanvas()                                   // 刷新到屏幕
 *    canvasSetLineWidth(w)                            // 线宽
 * ============================================================
 */

#include "unihiker_k10.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

UNIHIKER_K10 k10;
uint8_t screen_dir = 2;          // 屏幕方向:2 = 竖屏 240x320

// ======================== 可调参数 ========================
#define CELL        12           // 每个格子的像素大小
#define COLS        (240 / CELL) // 横向格子数 = 20
#define ROWS        (320 / CELL) // 纵向格子数 = 26(底部留一行给分数)

#define MAX_SNAKE   200          // 蛇的最大长度(格子数)

#define START_SPEED 220          // 初始移动间隔(ms),越大越慢
#define MIN_SPEED   80           // 最快速度上限
#define SPEED_STEP  6            // 每吃一个食物加快多少 ms
#define TURBO_SPEED 70           // 按住 B 键时的冲刺间隔(ms)
// ==========================================================

// ======================== 游戏状态 ========================
struct Point { int x, y; };

Point snake[MAX_SNAKE];          // 蛇身:snake[0] 是蛇头
int   snakeLen = 4;              // 当前长度

int   dirX = 1, dirY = 0;        // 当前方向向量(初始向右)
int   nextDirX = 1, nextDirY = 0;// 下一帧要转向的方向(防一帧内多次转向)

Point food;                      // 食物位置
int   score = 0;                 // 当前分数
int   bestScore = 0;             // 最高分(仅内存)
int   speedMs = START_SPEED;     // 当前速度(移动间隔 ms)

bool  gameOver = false;          // 是否已结束
bool  paused = false;            // 是否暂停

unsigned long lastMove = 0;      // 上次移动的时间戳
bool  lastBtnA = false;          // A 键上一次的状态(用于边沿检测)
bool  lastBtnAB = false;         // AB 组合键上一次的状态

// ---- 跨核通信标志(core 0 按键任务 -> core 1 主循环) ----
volatile bool hasPendingDir = false;   // 是否有待应用的转向
volatile int  pendingDirX = 0;         // 待应用的转向向量
volatile int  pendingDirY = 0;
volatile bool requestRestart = false;  // 请求重新开始

// ---- 屏幕底色状态:结束页要变纯黑,重开时要变回深蓝 ----
bool bgIsBlack = false;                // 当前屏幕底色是不是纯黑

// ======================== 颜色定义(RGB888) ========================
#define C_BG        0x0A0A12     // 深空蓝黑(背景)
#define C_GRID      0x141420     // 网格线(比背景稍亮)
#define C_SNAKE_HD  0x00E676     // 蛇头:亮绿
#define C_SNAKE_BD  0x00963F     // 蛇身:深绿
#define C_FOOD      0xFF3B30     // 食物:红
#define C_FOOD_HL   0xFFB3AD     // 食物高光
#define C_TEXT      0xFFFFFF     // 文字:白
#define C_TEXT_DIM  0x88909C     // 次要文字:灰
#define C_ACCENT    0xFFD60A     // 强调色:黄
#define C_PANEL     0x1A1A24     // 面板背景
#define C_MASK      0x06060C     // 覆盖层遮罩(接近纯黑,用于暂停/结束整屏压暗)
#define C_DIVIDER   0x33384A     // 分隔线 / 卡片边框辅助色
// ==============================================================

// 判断某点是否在蛇身上(用于避免食物生成在蛇身上)
bool onSnake(int x, int y) {
  for (int i = 0; i < snakeLen; i++)
    if (snake[i].x == x && snake[i].y == y) return true;
  return false;
}

// ---------- 随机生成食物(避开蛇身) ----------
void spawnFood() {
  for (int tries = 0; tries < 300; tries++) {
    int fx = random(0, COLS);
    int fy = random(0, ROWS);
    if (!onSnake(fx, fy)) { food.x = fx; food.y = fy; return; }
  }
  // 极端情况(蛇几乎占满屏幕):逐个格子找空位
  for (int y = 0; y < ROWS; y++)
    for (int x = 0; x < COLS; x++)
      if (!onSnake(x, y)) { food.x = x; food.y = y; return; }
}

// ---------- 重置游戏 ----------
void resetGame() {
  snakeLen = 4;
  // 蛇初始横在屏幕中部,朝右
  int sy = ROWS / 2;
  int sx = COLS / 2 - 2;
  for (int i = 0; i < snakeLen; i++) {
    snake[i].x = sx - i;
    snake[i].y = sy;
  }
  dirX = 1; dirY = 0;
  nextDirX = 1; nextDirY = 0;
  score = 0;
  speedMs = START_SPEED;
  gameOver = false;
  paused = false;

  // 重开时把屏幕底色切回游戏用的深蓝黑(结束页曾把它改成纯黑)
  if (bgIsBlack) {
    bgIsBlack = false;
    k10.setScreenBackground(C_BG);
  }

  spawnFood();
}

// ---------- 判断新方向是否与当前方向相反 ----------
// (不允许 180 度掉头,否则会立刻撞到自己)
bool isOpposite(int nx, int ny) {
  return (nx == -dirX && ny == -dirY);
}

// ---------- 处理按键(跑在 core 0) ----------
// 注意:这个函数只改"标志位"和"下一帧方向",不动 snake[] 数组,
//       避免与 core 1 的绘制产生数据竞争。
//       真正会改 snake[] 的操作(resetGame)只置一个标志,交给主循环执行。
void handleButtons() {
  bool btnA  = k10.buttonA->isPressed();
  bool btnAB = k10.buttonAB->isPressed();

  // 边沿检测:只在"刚按下"的那一刻响应,避免长按连续触发
  bool aJustPressed  = btnA  && !lastBtnA;
  bool abJustPressed = btnAB && !lastBtnAB;

  lastBtnA  = btnA;
  lastBtnAB = btnAB;

  // AB 同按:暂停 / 继续(游戏结束后则不响应暂停)
  if (abJustPressed && !gameOver) {
    paused = !paused;
  }

  // A 键:顺时针转向  上→右→下→左→上
  // 用向量旋转法:右(1,0) → 下(0,1) → 左(-1,0) → 上(0,-1)
  if (aJustPressed && !gameOver && !paused) {
    int tx = -dirY;      // 顺时针旋转 90 度
    int ty =  dirX;
    if (!isOpposite(tx, ty)) {
      pendingDirX = tx;
      pendingDirY = ty;
      hasPendingDir = true;      // 交给主循环去应用
    }
  }

  // 游戏结束后:A 键请求重新开始(同样交给主循环)
  if (aJustPressed && gameOver) {
    requestRestart = true;
  }
}

// ---------- 按键任务(跑在 core 0,高频采样) ----------
// 这是解决"按键迟钝"的关键:
//   主循环要重绘整屏 + 推屏,一轮几百毫秒,按键检测频率只有几 Hz;
//   放到独立任务里每 5ms 采一次(200Hz),短按也能被立刻抓住。
void keyTask(void* param) {
  for (;;) {
    handleButtons();
    vTaskDelay(pdMS_TO_TICKS(5));   // 5ms 采样一次
  }
}

// ---------- 蛇移动一步 ----------
void moveSnake() {
  // 采用下一帧方向
  if (!isOpposite(nextDirX, nextDirY)) {
    dirX = nextDirX;
    dirY = nextDirY;
  }

  // 计算新蛇头位置
  int hx = snake[0].x + dirX;
  int hy = snake[0].y + dirY;

  // 撞墙检测
  if (hx < 0 || hx >= COLS || hy < 0 || hy >= ROWS) {
    if (score > bestScore) bestScore = score;
    gameOver = true;
    return;
  }

  // 撞自己检测(注意:尾巴那格即将移动,但如果吃到食物就不移动)
  bool willEat = (hx == food.x && hy == food.y);
  int  checkLen = willEat ? snakeLen : snakeLen - 1;
  for (int i = 0; i < checkLen; i++) {
    if (snake[i].x == hx && snake[i].y == hy) {
      if (score > bestScore) bestScore = score;
      gameOver = true;
      return;
    }
  }

  // 蛇身整体前移(从尾往头挪)
  if (!willEat) {
    for (int i = snakeLen - 1; i > 0; i--) snake[i] = snake[i - 1];
  } else {
    if (snakeLen < MAX_SNAKE) {
      for (int i = snakeLen; i > 0; i--) snake[i] = snake[i - 1];
      snakeLen++;
    } else {
      for (int i = snakeLen - 1; i > 0; i--) snake[i] = snake[i - 1];
    }
  }

  snake[0].x = hx;
  snake[0].y = hy;

  // 吃到食物
  if (willEat) {
    score += 10;
    if (speedMs > MIN_SPEED) speedMs -= SPEED_STEP;   // 自动加速
    spawnFood();
  }
}

// ---------- 画一个圆角格子(蛇身单元) ----------
void drawCell(int gx, int gy, uint32_t color) {
  int px = gx * CELL;
  int py = gy * CELL;
  // 用矩形 + 内缩一点,做出格子间的缝隙,视觉上更像"节"
  // 注意:canvasRectangle(x, y, w, h, color, bg_color, fill) 后两个是宽高,不是对角坐标
  k10.canvas->canvasRectangle(px + 1, py + 1, CELL - 2, CELL - 2,
                              color, color, true);
}

// ---------- 绘制整个画面 ----------
// 注意:这里不用 canvasClear()!K10 的 canvasClear 会导致整屏闪一下。
// 改用"铺底色"的方式覆盖,效果一样但不会闪。
void draw() {
  // ---- 0. 铺底色(代替 canvasClear,避免全屏闪烁) ----
  k10.canvas->canvasRectangle(0, 0, 240, 320, C_BG, C_BG, true);

  // ---- 1. 背景网格(淡淡的,纯装饰) ----
  for (int x = 1; x < COLS; x++)
    k10.canvas->canvasLine(x * CELL, 0, x * CELL, ROWS * CELL, C_GRID);
  for (int y = 1; y < ROWS; y++)
    k10.canvas->canvasLine(0, y * CELL, COLS * CELL, y * CELL, C_GRID);

  // ---- 2. 食物 ----
  drawFood();

  // ---- 3. 蛇 ----
  drawSnake();

  // ---- 4. 底部信息栏 ----
  //drawPanel();

  // ---- 5. 覆盖层(暂停 / 结束) ----
  drawOverlay();

  // ---- 6. 一次性推送到屏幕 ----
  k10.canvas->updateCanvas();
}

// ---------- 暂停 / 结束 覆盖层 ----------
// ⚠️ 经验教训:canvasRectangle 的 fill=false(空心矩形)在 K10 上绘制
//    不完整(实测只剩两条边),边框一律改用 4 条 canvasLine 手动画。
// 布局(240×320):
//   卡片 20,92  宽200 高150,水平居中
//   GAME OVER 标题 / 分隔线 / SCORE / BEST / 提示语 排布
// 画一个空心边框:4 条线拼成(canvasRectangle 空心模式不可靠,不用它)
void drawFrame(int x, int y, int w, int h, uint32_t c) {
  k10.canvas->canvasLine(x,     y,     x+w-1, y,     c);  // 上
  k10.canvas->canvasLine(x,     y+h-1, x+w-1, y+h-1, c);  // 下
  k10.canvas->canvasLine(x,     y,     x,     y+h-1, c);  // 左
  k10.canvas->canvasLine(x+w-1, y,     x+w-1, y+h-1, c);  // 右
}

void drawOverlay() {
  if (paused && !gameOver) {
    // 暂停:整屏压暗 + 中间信息卡
    k10.canvas->canvasRectangle(0, 0, 240, 320, C_MASK, C_MASK, true);
    k10.canvas->canvasRectangle(40, 126, 160, 78, C_PANEL, C_PANEL, true);
    drawFrame(40, 126, 160, 78, C_ACCENT);
    k10.canvas->canvasText("PAUSED",
                           66, 142, C_ACCENT,
                           k10.canvas->eCNAndENFont24, 120, true);
    k10.canvas->canvasText("press AB",
                           80, 180, C_TEXT_DIM,
                           k10.canvas->eCNAndENFont16, 100, true);
  }

  if (gameOver) {
    // 0) 关键:把底层屏幕背景也刷成纯黑。
    //    只铺 Canvas 是不够的 —— setScreenBackground() 设的那层底色
    //    (C_BG 深蓝黑)会在 Canvas 之外/刷新间隙露出来,看着就是"蓝底"。
    //    所以一旦结束,连屏幕底色一起改成纯黑。
    if (!bgIsBlack) {
      bgIsBlack = true;
      k10.setScreenBackground(0x000000);
    }

    // 1) 整屏纯黑,彻底盖住网格和蛇(用背景同款铺法,不用 canvasClear)
    k10.canvas->canvasRectangle(0, 0, 240, 320, 0x000000, 0x000000, true);


    // 2) 标题
    k10.canvas->canvasText("GAME OVER",
                           38, 110, C_FOOD,
                           k10.canvas->eCNAndENFont24, 164, true);

    // 3) 分隔线
    k10.canvas->canvasLine(38, 148, 202, 148, C_TEXT_DIM);

    // 4) 本局得分 + 历史最佳(破纪录时高亮提示)
    k10.canvas->canvasText((String("SCORE   ") + String(score)),
                           44, 162, C_TEXT,
                           k10.canvas->eCNAndENFont16, 152, true);

    bool newBest = (score >= bestScore && score > 0);
    k10.canvas->canvasText((String("BEST    ") + String(bestScore)),
                           44, 186, C_ACCENT,
                           k10.canvas->eCNAndENFont16, 152, true);
    if (newBest) {
      k10.canvas->canvasText("NEW BEST!",
                              44, 206, C_ACCENT,
                              k10.canvas->eCNAndENFont16, 152, true);
      k10.canvas->canvasText("press A to restart",
                              36, 226, C_TEXT_DIM,
                              k10.canvas->eCNAndENFont16, 168, true);
    } else {
      k10.canvas->canvasText("press A to restart",
                              36, 206, C_TEXT_DIM,
                              k10.canvas->eCNAndENFont16, 168, true);
    }

    // 5) 信息卡:深色填充 + 红色边框(4 条线手绘)
    //k10.canvas->canvasRectangle(20, 102, 200, 160, C_PANEL, C_PANEL, false);
    drawFrame(20, 102, 200, 160, C_FOOD);
  }
}

// ---------- 只重绘食物 ----------
void drawFood() {
  int fx = food.x * CELL;
  int fy = food.y * CELL;
  k10.canvas->canvasRectangle(fx + 1, fy + 1, CELL - 2, CELL - 2,
                              C_FOOD, C_FOOD, true);
  k10.canvas->canvasCircle(fx + CELL / 2 - 2, fy + CELL / 2 - 2, 1,
                           C_FOOD_HL, C_FOOD_HL, true);
}

// ---------- 画蛇(从尾到头,头最后画盖在上面) ----------
void drawSnake() {
  for (int i = snakeLen - 1; i >= 0; i--) {
    uint32_t c = (i == 0) ? C_SNAKE_HD : C_SNAKE_BD;
    drawCell(snake[i].x, snake[i].y, c);
  }
  // 蛇头眼睛
  int hx = snake[0].x * CELL;
  int hy = snake[0].y * CELL;
  int cx = hx + CELL / 2;
  int cy = hy + CELL / 2;
  int e1x, e1y, e2x, e2y;
  if (dirX != 0) {
    e1x = cx + dirX * 2;  e1y = cy - 3;
    e2x = cx + dirX * 2;  e2y = cy + 3;
  } else {
    e1x = cx - 3;  e1y = cy + dirY * 2;
    e2x = cx + 3;  e2y = cy + dirY * 2;
  }
  k10.canvas->canvasCircle(e1x, e1y, 1, 0x000000, 0x000000, true);
  k10.canvas->canvasCircle(e2x, e2y, 1, 0x000000, 0x000000, true);
}

// ---------- 底部信息栏 ----------
void drawPanel() {
  int panelY = ROWS * CELL;
  k10.canvas->canvasRectangle(0, panelY, 240, 320 - panelY, C_PANEL, C_PANEL, true);

  k10.canvas->canvasText((String("SCORE ") + String(score)),
                         2, panelY + 4,
                         0xFFFFFF, k10.canvas->eCNAndENFont16, 100, true);

  k10.canvas->canvasText((String("BEST ") + String(bestScore)),
                         150, panelY + 4,
                         C_ACCENT, k10.canvas->eCNAndENFont16, 80, true);

  // 速度条
  int speedW = map(speedMs, MIN_SPEED, START_SPEED, 240, 70);
  k10.canvas->canvasRectangle(0, 320 - 5, speedW, 5,
                              C_SNAKE_HD, C_SNAKE_HD, true);
}

// ---------- 重绘某格(备用,当前未使用) ----------
void redrawCellBg(int gx, int gy) {
  int px = gx * CELL;
  int py = gy * CELL;
  // 铺背景色
  k10.canvas->canvasRectangle(px, py, CELL, CELL, C_BG, C_BG, true);
  // 补回网格线(只在格子右下边界画,形成网格感)
  k10.canvas->canvasLine(px, py, px, py + CELL, C_GRID);
  k10.canvas->canvasLine(px, py, px + CELL, py, C_GRID);
}

// ---------- 开机等待界面 ----------
void showStartScreen() {
  // 同样避免用 canvasClear(会闪),直接铺底色
  k10.canvas->canvasRectangle(0, 0, 240, 320, C_BG, C_BG, true);

  k10.canvas->canvasText("SNAKE",
                         62, 90, C_SNAKE_HD,
                         k10.canvas->eCNAndENFont24, 180, true);
  k10.canvas->canvasText("K10 EDITION",
                         72, 124, C_TEXT_DIM,
                         k10.canvas->eCNAndENFont16, 140, true);

  k10.canvas->canvasText("A  =  turn",
                         80, 178, C_TEXT, k10.canvas->eCNAndENFont16, 120, true);
  k10.canvas->canvasText("B  =  turbo",
                         80, 202, C_TEXT, k10.canvas->eCNAndENFont16, 120, true);
  k10.canvas->canvasText("AB =  pause",
                         78, 226, C_TEXT, k10.canvas->eCNAndENFont16, 120, true);

  k10.canvas->canvasText("press A to start",
                         62, 276, C_ACCENT, k10.canvas->eCNAndENFont16, 180, true);
  k10.canvas->updateCanvas();

  // 等待按键 A 按下(此时 keyTask 还没启动,用轮询)
  while (!k10.buttonA->isPressed()) delay(20);
  delay(300);      // 防抖,避免刚进游戏就转一次向
}

void setup() {
  Serial.begin(115200);

  k10.begin();
  k10.initScreen(screen_dir);
  k10.creatCanvas();
  k10.setScreenBackground(C_BG);

  randomSeed(esp_random());   // 用硬件随机数做种子

  showStartScreen();
  resetGame();

  lastMove = millis();

  // 把按键检测钉在 core 0,绘制留在 core 1 —— 彻底解决按键迟钝
  xTaskCreatePinnedToCore(keyTask, "key", 4096, NULL, 2, NULL, 0);
}

void loop() {
  // 注意:按键检测已经移到 core 0 的独立任务里(keyTask),
  //       这里只负责"应用按键意图 + 游戏逻辑推进 + 绘制"。

  // ---- 1. 应用按键意图(在 core 1 执行,避免跨核改数组) ----
  if (requestRestart) {
    requestRestart = false;
    resetGame();
    lastMove = millis();
  }

  if (hasPendingDir) {
    hasPendingDir = false;
    if (!isOpposite(pendingDirX, pendingDirY)) {
      nextDirX = pendingDirX;
      nextDirY = pendingDirY;
    }
  }

  // ---- 2. 推进游戏逻辑 ----
  if (!gameOver && !paused) {
    // 判断当前应该用哪个速度:按住 B 就冲刺
    int curSpeed = k10.buttonB->isPressed() ? TURBO_SPEED : speedMs;

    if (millis() - lastMove >= (unsigned long)curSpeed) {
      lastMove = millis();
      moveSnake();
    }
  }

  // ---- 3. 绘制 ----
  draw();

  delay(5);        // 小睡一下,让出 CPU
}

创作许可协议

本项目采用 None(不开放任何权利,保留所有权利) 进行许可。

评论(0)
- 没有更多了 -