豆爸 发表于 2025-10-11 16:41:59

FireBeetle 2 ESP32-C5基于AD Key按键的打地鼠游戏

本帖最后由 豆爸 于 2025-10-11 21:48 编辑

一、项目概述

1、项目背景

本项目以 ESP32-C5 开发板 为核心,结合 128×64 OLED 显示屏与 ADC 矩阵键盘,开发一款经典的 “打地鼠” 游戏。项目旨在通过实践掌握以下技术:

[*]ESP32-C5 的 GPIO 配置、I2C 通信(OLED 驱动)与 ADC 外设应用(键盘读取);
[*]入式系统中的 状态机设计(游戏启动、运行、结束三状态切换);
[*]图形化界面(OLED)的动态绘制与交互逻辑实现;
[*]实时事件处理(按键防抖、地鼠限时显示、击中判断)。


2、功能简介

本项目实现了完整的打地鼠游戏流程,核心功能包括:
1. 多状态界面切换:启动界面(操作提示)→ 游戏界面(网格、地鼠、分数)→ 结束界面(成绩统计);
2. 随机地鼠生成:3×3 网格中随机生成地鼠,限时 1.5~2.5 秒显示;
3. 按键交互:通过 ADC 键盘(K0~K9)控制游戏(K0 启动 / 重启,K1~K9 对应网格位置打地鼠);
4. 成绩统计:实时计算击中数、总地鼠数与准确率,游戏结束后展示最终成绩;
5. 回合制游戏:固定 10 回合,回合结束自动进入游戏结束界面。

二、硬件介绍
1、FireBeetle 2 ESP32-C5开发板
FireBeetle 2ESP32-C5 是一款搭载乐鑫 ESP32-C5 模组的低功耗 IoT 开发板,面向智能家居和广泛物联网场景,集高性能计算、多协议支持与智能电源管理于一体,为各种部署需求提供高可靠性、高灵活性与长续航的解决方案。2、Fermion: 10位AD按键板
10位AD按键板可以作为开发板的AD按键输入,通过一个模拟口就可以扩展出10个按键,大幅度节省开发板的IO扣。同时还可以通过两边的焊盘,连接其他按键,适应项目结构。3、 Rravity OLED-12864 显示屏
Rravity OLED-12864 显示屏是一款无需背景光源,自发光式的显示模块。模块采用蓝色背景,显示尺寸控制在0.96英寸,采用OLED专用驱动芯片SSD1306控制。该模块支持通过I2C接口与控制器通信,支持高传输速率,能够实现60Hz的刷新频率。三、软件介绍1、开发工具:Arduino IDE2.3.6
从官网下载并安装Arduino IDE 2.3.6版本,https://downloads.arduino.cc/ard ... 6_Windows_64bit.exe
2、安装ESP32开发板卡软件包
打开Arduino IDE,进入文件 -> 首选项,在附加开发板管理器网址中输入:https://jihulab.com/esp-mirror/e ... esp32_index_cn.json选择开发板esp32 by Espressif Systems v3.3.0-alpha1-cn版本进行安装。3、安装库:
1. U8g2lib.h:OLED 显示屏驱动库,支持多种分辨率与通信方式,提供丰富的图形绘制接口(如线段、圆形、文字); 四、主要代码及说明
1. 游戏状态机模块
这部分代码负责管理游戏的状态流转,是整个游戏的核心控制逻辑:// 游戏状态定义
enum GameState { START_SCREEN, PLAYING, GAME_OVER };
GameState gameState = START_SCREEN;

// 状态处理函数
void loop() {
// 读取按键
int key = readADCKey();

// 处理按键
if (key != -1) {
    handleKeyPress(key);
}

// 更新游戏状态
if (gameState == PLAYING) {
    updateGame();
}

// 绘制游戏界面
drawGame();

delay(50); // 短暂延迟以减少闪烁
}

void handleKeyPress(int key) {
switch (gameState) {
    case START_SCREEN:
      if (key == 0) { // K0 - 开始游戏
      startGame();
      }
      break;
      
    case PLAYING:
      if (key >= 1 && key <= 9) { // K1-K9 - 打地鼠
      handleMoleHit(key);
      }
      break;
      
    case GAME_OVER:
      if (key == 0) { // K0 - 重新开始游戏
      resetGame();
      gameState = START_SCREEN;
      }
      break;
}

// 标记按键已处理
keyProcessed = true;
}核心说明:

[*]使用枚举类型GameState定义了游戏的三种状态:启动界面、游戏中、游戏结束
[*]在主循环loop()中,根据当前状态执行相应的逻辑
[*]handleKeyPress()函数根据不同游戏状态处理按键输入,实现状态间的切换
[*]这种状态机设计使游戏逻辑清晰,各状态间的耦合度低,便于维护和扩展

2. 地鼠逻辑模块

这部分代码实现了地鼠的生成、超时检测和击中判断:
// 地鼠相关变量
int currentMole = -1;// 使用 1-9 表示位置,-1 表示没有地鼠
unsigned long moleStartTime = 0;
unsigned long moleDuration = 0;
int roundsPlayed = 0;
int totalMoles = 0;
int hitMoles = 0;

void updateGame() {
// 检查地鼠是否需要消失
checkMoleTimeout();

// 如果没有地鼠,生成新的地鼠
if (currentMole == -1 && roundsPlayed < MAX_GAME_ROUNDS) {
    spawnNewMole();
}
}

void spawnNewMole() {
// 随机选择新的地鼠位置 (1-9)
currentMole = random(1, 10);

// 随机设置地鼠显示时间(1500-2500毫秒)
moleDuration = random(1500, 2500);
moleStartTime = millis();

totalMoles++;
}

void checkMoleTimeout() {
if (currentMole != -1 && (millis() - moleStartTime) > moleDuration) {
    currentMole = -1;
   
    // 增加已玩回合数(即使没有击中)
    roundsPlayed++;
   
    // 检查游戏是否结束
    if (roundsPlayed >= MAX_GAME_ROUNDS) {
      gameState = GAME_OVER;
    }
}
}

void handleMoleHit(int position) {
// 检查是否击中地鼠
if (currentMole != -1 && position == currentMole) {
    hitMoles++;
    currentMole = -1;
   
    // 增加已玩回合数
    roundsPlayed++;
   
    // 检查游戏是否结束
    if (roundsPlayed >= MAX_GAME_ROUNDS) {
      gameState = GAME_OVER;
    }
}
}核心说明:

[*]spawnNewMole()函数随机生成地鼠位置和显示时间,增加游戏的不确定性
[*]checkMoleTimeout()函数通过millis()实现非阻塞式计时,检测地鼠是否超时
[*]handleMoleHit()函数判断玩家是否击中地鼠,并更新游戏状态和分数
[*]使用currentMole变量跟踪当前地鼠位置,-1 表示当前没有地鼠

3. 输入处理模块

这部分代码实现了 ADC 键盘的读取和防抖处理:
// ADC键盘参数
const int ADC_PIN = 2;            // ADC键盘连接引脚
const int DEBOUNCE_DELAY = 50;      // 防抖时间
const int ADC_TOLERANCE= 100;   // ADC值容差范围

// 按键ADC值(K0-K9)
const int keyValues[] = {5, 407, 743, 985, 1360, 1759, 2005, 2347, 2755, 3060};
const int KEY_COUNT = sizeof(keyValues) / sizeof(keyValues);

// 输入状态变量
int lastKey = -1;
unsigned long lastKeyTime = 0;
bool keyProcessed = true;

int readADCKey() {
int adcValue = analogRead(ADC_PIN);
unsigned long currentTime = millis();

// 防抖处理
if (currentTime - lastKeyTime < DEBOUNCE_DELAY) {
    return -1;
}

// 查找匹配的按键
int detectedKey = -1;
int minDifference = 10000; // 很大的初始值

for (int i = 0; i < KEY_COUNT; i++) {
    int difference = abs(adcValue - keyValues);
    if (difference <= ADC_TOLERANCE && difference < minDifference) {
      detectedKey = i;
      minDifference = difference;
    }
}

// 按键状态处理
if (detectedKey == -1) {
    if (lastKey != -1) {
      // 按键释放
      lastKey = -1;
      keyProcessed = true;
    }
    return -1;
}

// 检测到新按键
if (detectedKey == lastKey && !keyProcessed) {
    return -1; // 同一个按键且未处理,避免重复触发
}

// 更新按键状态
lastKey = detectedKey;
lastKeyTime = currentTime;
keyProcessed = false;

return detectedKey;
}核心说明:

[*]采用 ADC 方式读取矩阵键盘,只需一个 ADC 引脚即可识别多个按键
[*]实现了软件防抖处理,通过DEBOUNCE_DELAY过滤按键抖动
[*]使用容差范围ADC_TOLERANCE处理硬件差异和环境干扰导致的 ADC 值波动
[*]通过keyProcessed标记确保每个按键按下只被处理一次

4. 显示模块

这部分代码实现了游戏界面的绘制,包括网格、地鼠和分数等:
void drawGame() {
u8g2.clearBuffer();

switch (gameState) {
    case START_SCREEN:
      drawStartScreen();
      break;
    case PLAYING:
      drawPlayingScreen();
      break;
    case GAME_OVER:
      drawGameOverScreen();
      break;
}

u8g2.sendBuffer();
}

void drawPlayingScreen() {
// 绘制网格
drawGrid();

// 如果有地鼠,绘制地鼠
if (currentMole != -1) {
    drawMole(currentMole);
}

// 绘制分数
drawScore();

// 绘制游戏进度
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setCursor(0, GRID_SIZE * CELL_HEIGHT + 36);
u8g2.print("回合: ");
u8g2.print(roundsPlayed);
u8g2.print("/");
u8g2.print(MAX_GAME_ROUNDS);
}

void drawMole(int position) {
// 将 1-9 的位置转换为 0-8 的行列索引
int pos = position - 1; // 转换为 0-8
int row = pos / GRID_SIZE;
int col = pos % GRID_SIZE;

int centerX = col * CELL_WIDTH + CELL_WIDTH / 2;
int centerY = row * CELL_HEIGHT + CELL_HEIGHT / 2;

// 绘制地鼠(简单的圆形)
u8g2.drawDisc(centerX, centerY, MOLE_RADIUS);

// 绘制眼睛(两个小圆点)
u8g2.drawDisc(centerX - 2, centerY - 2, 1);
u8g2.drawDisc(centerX + 2, centerY - 2, 1);

// 绘制鼻子(小圆点)
u8g2.drawDisc(centerX, centerY + 1, 1);
}核心说明:

[*]使用 U8g2 库实现 OLED 屏幕的绘制功能,支持图形和文字显示
[*]drawGame()函数根据当前游戏状态调用不同的界面绘制函数
[*]drawMole()函数将地鼠位置 (1-9) 转换为屏幕坐标,并绘制简单的地鼠图形
[*]采用分层绘制策略:先绘制网格,再绘制地鼠,最后绘制分数和状态信息

这些核心模块相互配合,构成了完整的打地鼠游戏系统。每个模块职责明确,通过变量和函数调用进行交互,使整个代码结构清晰,易于理解和维护。


五、效果演示


1、游戏开始效果

2、游戏进行中效果

3、游戏结束效果

六、项目总结与展望
1、项目总结

本项目成功实现了基于 ESP32-C5 的打地鼠游戏,完成了所有核心功能:
1. 硬件层面:实现了 OLED(I2C)与 ADC 键盘的稳定通信,引脚配置合理;
2. 软件层面:采用状态机设计降低模块耦合,游戏逻辑清晰(地鼠生成、击中判断、分数统计);
3. 交互层面:界面友好,操作提示明确,实时反馈游戏状态,用户体验流畅。通过项目实践,深入掌握了 ESP32 外设(ADC、I2C)应用、嵌入式状态机设计与 OLED 图形绘制,解决了按键防抖、中文显示等实际问题。

2、改进方向

1. 难度递增机制:随回合数增加,地鼠显示时间逐渐缩短,提升游戏挑战性;
2. 声音反馈:增加蜂鸣器模块,击中地鼠时发出提示音,增强交互体验;
3. 高分记录功能:使用 ESP32 的 EEPROM 存储历史最高分,游戏结束后对比并更新;

附件:







页: [1]
查看完整版本: FireBeetle 2 ESP32-C5基于AD Key按键的打地鼠游戏