| 本帖最后由 豆爸 于 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 2  ESP32-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的刷新频率。 三、软件介绍 
 
 2、安装ESP32开发板卡软件包 
 选择开发板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[0]);
// 输入状态变量
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[i]);
    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 存储历史最高分,游戏结束后对比并更新;
 
 附件:
 
  Whac_A_Mole.zip 
 
 
 
 
 
 
 |