本帖最后由 豆爸 于 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 
 
 
 
 
 
 
 |