基于ESP32-C5开发板驱动OLED井字棋小游戏
本帖最后由 创客编程张 于 2025-10-15 17:19 编辑前两天在B站上刷到了井字棋的起源和发展,忽然来了灵感,正愁没素材呢,这不,咱用新板子配合着OLED制作一个井字棋玩玩。
关于FireBeetle 2 ESP32-C5
FireBeetle 2 ESP32-C5 IO套装包括两部分:Firebeetle 2 ESP32-C5 开发板和其专用的IO扩展底板。IO扩展板方便快速连接各种传感器外设,让Firebeetle 2 ESP32-C5开发板到手即用,无需焊接。
https://mc.dfrobot.com.cn/data/attachment/forum/202508/18/174802zrv7wim8bbbmq7uq.png
FireBeetle 2 ESP32-C5是一款搭载乐鑫 ESP32-C5 模组的低功耗 IoT 开发板,面向智能家居和广泛物联网场景,集高性能计算、多协议支持与智能电源管理于一体,为各种部署需求提供高可靠性、高灵活性与长续航的解决方案。
前期准备
1.软件准备
下载Arduino IDE,打开安装ESP32开发板,然后安装U8g2库(用于驱动OLED显示屏)
2.硬件清单
[*]ESP32-C5
[*]OLED显示屏(128*64)
[*]Keyboard模拟按键
准备完成,我们开始干正事
将OLED连接到开发板的I2C引脚上,将Keyboard模拟按键连接至引脚2
接下来编写程序
首先初始化
#include <Arduino.h>
#include <U8g2lib.h>
#define I2C_SDA 9 // ESP32-C5 专用 I2C 数据引脚(SDA)
#define I2C_SCL 10 // ESP32-C5 专用 I2C 时钟引脚(SCL)
#define AD_KEY_PIN A2// ADKeyboard模拟输入引脚
#define EMPTY 0 // 棋盘空状态
#define PLAYER 1 // 玩家(空心圆)
#define AI 2 // 人机(实心圆)
#define LONG_PRESS_TIME 1000// 新增:长按判定时长(1000ms=1秒,可调整)
// 1. OLED初始化(0.96寸单色I2C,根据屏幕芯片调整)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);变量
int chessBoard = {0};// 3x3棋盘,存储落子状态
int currX = 1, currY = 1; // 当前选中的棋盘坐标(0-2,初始居中)
bool isPlayerTurn = true; // 是否玩家回合(true=玩家,false=人机)
bool gameOver = false; // 游戏是否结束
int winner = EMPTY; // 赢家(0=未分胜负,1=玩家,2=人机)
int winLine = {0}; // 赢棋连线坐标(x1,y1,x2,y2)keyboard模拟按键
unsigned long pressStartTime = 0;// 记录S5按键按下的起始时间(毫秒)
bool isKey5Pressed = false; // 标记S5是否处于按下状态(避免重复触发)
// 3. ADKeyboard按键识别
int readADKey() {
int adVal = analogRead(AD_KEY_PIN);
delay(100);// 消抖:避免按键机械抖动导致的误读数
if (adVal >= 2500 && adVal <= 2599) return 1; // S1(上)
else if (adVal >= 2600 && adVal <= 2699) return 2;// S2(左)
else if (adVal >= 2700 && adVal <= 2799) return 3;// S3(下)
else if (adVal >= 2800 && adVal <= 2899) return 4;// S4(右)
else if (adVal >= 3000 && adVal <= 3099) return 5;// S5(确定/长按重启)
else return -1;// 无按键按下,返回-1棋盘显示
// 4. 棋盘坐标转OLED像素坐标
void chessToOled(int chessX, int chessY, int &oledX, int &oledY) {
oledX = 20 + chessX * 28;// 横向:左偏移20(居中),每个格子宽28像素
oledY = 20 + chessY * 22;// 纵向:上偏移20(留提示栏),每个格子高22像素
}
// 5. 绘制棋盘线(无修改,确保3x3格子清晰)
void drawChessBoard() {
u8g2.drawLine(48, 20, 48, 62);// 第一条竖线(分割第1、2列)
u8g2.drawLine(76, 20, 76, 62);// 第二条竖线(分割第2、3列)
u8g2.drawLine(20, 42, 98, 42);// 第一条横线(分割第1、2行)
u8g2.drawLine(20, 64, 98, 64);// 第二条横线(分割第2、3行)
}
// 6. 绘制棋子(无修改,保留闪烁选中效果)
void drawChess() {
static unsigned long lastFlashTime = 0;
static bool flashFlag = true;// 闪烁标志(300ms切换一次,实现选中闪烁)
if (millis() - lastFlashTime > 300) {
flashFlag = !flashFlag;
lastFlashTime = millis();
}
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int oledX, oledY;
chessToOled(x, y, oledX, oledY);
if (chessBoard == PLAYER) u8g2.drawCircle(oledX, oledY, 8);// 玩家:空心圆
else if (chessBoard == AI) u8g2.drawDisc(oledX, oledY, 8); // 人机:实心圆
else if (x == currX && y == currY && !gameOver && flashFlag) u8g2.drawCircle(oledX, oledY, 8); // 选中空位:闪烁
}
}
}
// 7. 绘制赢棋连线
void drawWinLine() {
if (winner == EMPTY) return;// 无赢家时,不绘制连线
u8g2.drawLine(winLine, winLine, winLine, winLine);// 绘制提前记录的赢棋连线
}
// 8. 绘制顶部提示栏(新增:gameOver时显示“长按S5重启”提示,让用户知晓操作)
void drawTipBar() {
u8g2.setFont(u8g2_font_6x12_tf);// 适配128x64屏幕的小字体,避免文字溢出
if (gameOver) {
// 先显示输赢/平局结果
if (winner == PLAYER) u8g2.drawStr(30, 12, "你赢了!");
else if (winner == AI) u8g2.drawStr(30, 12, "人机赢了!");
else u8g2.drawStr(30, 12, "平局!");
// 新增:显示重启提示,位置在结果下方,用户一眼能看到
u8g2.drawStr(15, 22, "长按S5 重新开始");
} else {
// 非游戏结束时,显示回合提示
if (isPlayerTurn) u8g2.drawStr(20, 12, "你的回合(空心圆)");
else u8g2.drawStr(20, 12, "人机回合(实心圆)");
}
}输赢检查
// 9. 检查是否赢棋
bool checkWin(int player) {
// 检查3行
for (int y = 0; y < 3; y++) {
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(0, y, winLine, winLine);// 记录行连线起点
chessToOled(2, y, winLine, winLine);// 记录行连线终点
return true;
}
}
// 检查3列
for (int x = 0; x < 3; x++) {
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(x, 0, winLine, winLine);// 记录列连线起点
chessToOled(x, 2, winLine, winLine);// 记录列连线终点
return true;
}
}
// 检查左上-右下对角线
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(0, 0, winLine, winLine);// 记录对角线起点
chessToOled(2, 2, winLine, winLine);// 记录对角线终点
return true;
}
// 检查右上-左下对角线
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(2, 0, winLine, winLine);// 记录对角线起点
chessToOled(0, 2, winLine, winLine);// 记录对角线终点
return true;
}
return false;
}
// 10. 检查是否平局
bool checkDraw() {
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (chessBoard == EMPTY) return false;// 只要有一个空位,就不是平局
}
}
return true;// 棋盘满且无赢家,判定为平局
}落子逻辑
// 11. 人机落子逻辑
void aiMove() {
if (gameOver) return;// 游戏结束,不执行AI落子
// 第一步:优先自己赢(试落子,能赢就落)
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (chessBoard == EMPTY) {
chessBoard = AI;// 试落AI棋子
if (checkWin(AI)) { // 试落后果:AI赢
winner = AI; // 标记AI为赢家
gameOver = true; // 标记游戏结束
isPlayerTurn = true;// 重置回合,不影响重启
return;
}
chessBoard = EMPTY;// 回溯:取消试落,避免影响后续判断
}
}
}
// 第二步:防玩家赢(试落玩家棋子,堵玩家赢点)
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (chessBoard == EMPTY) {
chessBoard = PLAYER;// 试落玩家棋子
if (checkWin(PLAYER)) { // 试落后果:玩家要赢
chessBoard = AI; // 落AI棋子,堵住这个赢点
isPlayerTurn = true; // 切换回玩家回合
return;
}
chessBoard = EMPTY;// 回溯:取消试落
}
}
}
// 第三步:随机落子(无赢/防赢机会时,避免AI落子固定)
while (true) {
int x = random(0, 3);
int y = random(0, 3);
if (chessBoard == EMPTY) {
chessBoard = AI;
isPlayerTurn = true;
return;
}
}
}游戏参数重置
void resetGame() {
memset(chessBoard, 0, sizeof(chessBoard));// 重置棋盘:所有位置变为“空”
currX = 1; currY = 1; // 重置选中位置:回到棋盘正中间
isPlayerTurn = true; // 重置回合:玩家先落子(符合常规习惯)
gameOver = false; // 重置游戏状态:从“结束”变为“进行中”
winner = EMPTY; // 重置赢家:无赢家
memset(winLine, 0, sizeof(winLine)); // 重置赢棋连线:清空连线坐标
// 重置长按相关变量:避免重启后残留按键状态,导致误触发
pressStartTime = 0;
isKey5Pressed = false;
}初始化函数
// 12. 初始化函数
void setup() {
Serial.begin(115200);// 初始化串口,方便调试AD按键值(可选关闭)
u8g2.begin(); // 初始化OLED(I2C通信,自动识别设备地址)
u8g2.clearBuffer(); // 清空OLED缓存,避免残留乱码
resetGame(); // 调用新增的重置函数,初始化首次游戏参数(替代原重复代码)
randomSeed(analogRead(A1));// 用空闲模拟引脚做随机种子,让AI落子更随机
主循环
void loop() {
int key = readADKey();// 读取当前按键(先获取按键值,再分状态处理)
if (!gameOver && isPlayerTurn) {
// 状态1:游戏进行中+玩家回合
switch (key) {
case 1:// S1:向上(y坐标-1,避免超出棋盘上边界0)
if (currY > 0) currY--;
break;
case 2:// S2:向左(x坐标-1,避免超出棋盘左边界0)
if (currX > 0) currX--;
break;
case 3:// S3:向下(y坐标+1,避免超出棋盘下边界2)
if (currY < 2) currY++;
break;
case 4:// S4:向右(x坐标+1,避免超出棋盘右边界2)
if (currX < 2) currX++;
break;
case 5:// S5:确定落子(仅当前位置为空时有效,避免重复落子)
if (chessBoard == EMPTY) {
chessBoard = PLAYER;// 落玩家棋子(空心圆)
if (checkWin(PLAYER)) { // 检查玩家是否赢
winner = PLAYER;
gameOver = true;
} else if (checkDraw()) { // 检查是否平局
gameOver = true;
} else {
isPlayerTurn = false; // 切换到人机回合
}
}
break;
}
} else if (!gameOver && !isPlayerTurn) {
// 状态2:游戏进行中+人机回合
delay(500);
aiMove();// 执行AI落子
// 落子后检查结果:人机赢或平局
if (checkWin(AI)) {
winner = AI;
gameOver = true;
} else if (checkDraw()) {
gameOver = true;
}
} else {
// 新增:状态3:游戏结束(赢/输/平局通用),处理S5长按重启
switch (key) {
case 5:// 检测到S5按键按下
if (!isKey5Pressed) {// 仅当按键未被标记为“按下”时,记录起始时间(避免重复记录)
isKey5Pressed = true; // 标记S5已按下
pressStartTime = millis(); // 记录按下的起始时间(单位:毫秒)
}
break;
case -1:// 检测到无按键按下(即S5已松开,判断是否为有效长按)
if (isKey5Pressed) {// 之前S5被按下过,现在松开,开始计算时长
unsigned long pressDuration = millis() - pressStartTime;// 计算按键按下的总时长
if (pressDuration >= LONG_PRESS_TIME) {// 时长≥1秒,判定为有效长按
resetGame();// 调用新增的重置函数,重启游戏
}
// 无论是否长按成功,都重置长按相关变量,为下次长按做准备
isKey5Pressed = false;
pressStartTime = 0;
}
break;
}
}
// OLED显示更新(原逻辑不变,按“提示栏→棋盘→棋子→连线”顺序,避免遮挡)
u8g2.clearBuffer();
drawTipBar();
drawChessBoard();
drawChess();
drawWinLine();
u8g2.sendBuffer();
}完整代码
#include <Arduino.h>
#include <U8g2lib.h>
#define I2C_SDA 9 // ESP32-C5 专用 I2C 数据引脚(SDA)
#define I2C_SCL 10 // ESP32-C5 专用 I2C 时钟引脚(SCL)
#define AD_KEY_PIN A2// ADKeyboard模拟输入引脚
#define EMPTY 0 // 棋盘空状态
#define PLAYER 1 // 玩家(空心圆)
#define AI 2 // 人机(实心圆)
#define LONG_PRESS_TIME 1000
// 1. OLED初始化
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
int chessBoard = {0};// 3x3棋盘,存储落子状态
int currX = 1, currY = 1; // 当前选中的棋盘坐标(0-2,初始居中)
bool isPlayerTurn = true; // 是否玩家回合(true=玩家,false=人机)
bool gameOver = false; // 游戏是否结束
int winner = EMPTY; // 赢家(0=未分胜负,1=玩家,2=人机)
int winLine = {0}; // 赢棋连线坐标(x1,y1,x2,y2)
// 长按重启相关全局变量(跟踪按键按下时长,避免误触)
unsigned long pressStartTime = 0;// 记录S5按键按下的起始时间(毫秒)
bool isKey5Pressed = false; // 标记S5是否处于按下状态(避免重复触发)
// 3. ADKeyboard按键识别
int readADKey() {
int adVal = analogRead(AD_KEY_PIN);
delay(100);// 消抖:避免按键机械抖动导致的误读数
if (adVal >= 2500 && adVal <= 2599) return 1; // S1(上)
else if (adVal >= 2600 && adVal <= 2699) return 2;// S2(左)
else if (adVal >= 2700 && adVal <= 2799) return 3;// S3(下)
else if (adVal >= 2800 && adVal <= 2899) return 4;// S4(右)
else if (adVal >= 3000 && adVal <= 3099) return 5;// S5(确定/长按重启)
else return -1;// 无按键按下,返回-1
}
// 4. 棋盘坐标转OLED像素坐标
void chessToOled(int chessX, int chessY, int &oledX, int &oledY) {
oledX = 20 + chessX * 28;// 横向:左偏移20(居中),每个格子宽28像素
oledY = 20 + chessY * 22;// 纵向:上偏移20(留提示栏),每个格子高22像素
}
// 5. 绘制棋盘线(无修改,确保3x3格子清晰)
void drawChessBoard() {
u8g2.drawLine(48, 20, 48, 62);// 第一条竖线(分割第1、2列)
u8g2.drawLine(76, 20, 76, 62);// 第二条竖线(分割第2、3列)
u8g2.drawLine(20, 42, 98, 42);// 第一条横线(分割第1、2行)
u8g2.drawLine(20, 64, 98, 64);// 第二条横线(分割第2、3行)
}
// 6. 绘制棋子
void drawChess() {
static unsigned long lastFlashTime = 0;
static bool flashFlag = true;// 闪烁标志(300ms切换一次,实现选中闪烁)
if (millis() - lastFlashTime > 300) {
flashFlag = !flashFlag;
lastFlashTime = millis();
}
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int oledX, oledY;
chessToOled(x, y, oledX, oledY);
if (chessBoard == PLAYER) u8g2.drawCircle(oledX, oledY, 8);// 玩家:空心圆
else if (chessBoard == AI) u8g2.drawDisc(oledX, oledY, 8); // 人机:实心圆
else if (x == currX && y == currY && !gameOver && flashFlag) u8g2.drawCircle(oledX, oledY, 8); // 选中空位:闪烁
}
}
}
// 7. 绘制赢棋连线
void drawWinLine() {
if (winner == EMPTY) return;// 无赢家时,不绘制连线
u8g2.drawLine(winLine, winLine, winLine, winLine);// 绘制提前记录的赢棋连线
}
// 8. 绘制顶部提示栏(新增:gameOver时显示“长按S5重启”提示,让用户知晓操作)
void drawTipBar() {
u8g2.setFont(u8g2_font_6x12_tf);// 适配128x64屏幕的小字体,避免文字溢出
if (gameOver) {
// 先显示输赢/平局结果
if (winner == PLAYER) u8g2.drawStr(30, 12, "你赢了!");
else if (winner == AI) u8g2.drawStr(30, 12, "人机赢了!");
else u8g2.drawStr(30, 12, "平局!");
// 新增:显示重启提示,位置在结果下方,用户一眼能看到
u8g2.drawStr(15, 22, "长按S5 重新开始");
} else {
// 非游戏结束时,显示回合提示(原逻辑不变)
if (isPlayerTurn) u8g2.drawStr(20, 12, "你的回合(空心圆)");
else u8g2.drawStr(20, 12, "人机回合(实心圆)");
}
}
// 9. 检查是否赢棋(无修改,确保赢棋判定准确)
bool checkWin(int player) {
// 检查3行
for (int y = 0; y < 3; y++) {
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(0, y, winLine, winLine);// 记录行连线起点
chessToOled(2, y, winLine, winLine);// 记录行连线终点
return true;
}
}
// 检查3列
for (int x = 0; x < 3; x++) {
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(x, 0, winLine, winLine);// 记录列连线起点
chessToOled(x, 2, winLine, winLine);// 记录列连线终点
return true;
}
}
// 检查左上-右下对角线
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(0, 0, winLine, winLine);// 记录对角线起点
chessToOled(2, 2, winLine, winLine);// 记录对角线终点
return true;
}
// 检查右上-左下对角线
if (chessBoard == player && chessBoard == player && chessBoard == player) {
chessToOled(2, 0, winLine, winLine);// 记录对角线起点
chessToOled(0, 2, winLine, winLine);// 记录对角线终点
return true;
}
return false;
}
// 10. 检查是否平局
bool checkDraw() {
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (chessBoard == EMPTY) return false;// 只要有一个空位,就不是平局
}
}
return true;// 棋盘满且无赢家,判定为平局
}
// 11. 人机落子逻辑(无修改,保持“优先赢、次防输、最后随机”策略)
void aiMove() {
if (gameOver) return;// 游戏结束,不执行AI落子
// 第一步:优先自己赢(试落子,能赢就落)
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (chessBoard == EMPTY) {
chessBoard = AI;// 试落AI棋子
if (checkWin(AI)) { // 试落后果:AI赢
winner = AI; // 标记AI为赢家
gameOver = true; // 标记游戏结束
isPlayerTurn = true;// 重置回合,不影响重启
return;
}
chessBoard = EMPTY;// 回溯:取消试落,避免影响后续判断
}
}
}
// 第二步:防玩家赢(试落玩家棋子,堵玩家赢点)
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (chessBoard == EMPTY) {
chessBoard = PLAYER;// 试落玩家棋子
if (checkWin(PLAYER)) { // 试落后果:玩家要赢
chessBoard = AI; // 落AI棋子,堵住这个赢点
isPlayerTurn = true; // 切换回玩家回合
return;
}
chessBoard = EMPTY;// 回溯:取消试落
}
}
}
// 第三步:随机落子(无赢/防赢机会时,避免AI落子固定)
while (true) {
int x = random(0, 3);
int y = random(0, 3);
if (chessBoard == EMPTY) {
chessBoard = AI;
isPlayerTurn = true;
return;
}
}
}
// 新增:游戏参数重置函数(单独封装,避免代码重复,长按重启时调用)
void resetGame() {
memset(chessBoard, 0, sizeof(chessBoard));// 重置棋盘:所有位置变为“空”
currX = 1; currY = 1; // 重置选中位置:回到棋盘正中间
isPlayerTurn = true; // 重置回合:玩家先落子(符合常规习惯)
gameOver = false; // 重置游戏状态:从“结束”变为“进行中”
winner = EMPTY; // 重置赢家:无赢家
memset(winLine, 0, sizeof(winLine)); // 重置赢棋连线:清空连线坐标
// 重置长按相关变量:避免重启后残留按键状态,导致误触发
pressStartTime = 0;
isKey5Pressed = false;
}
// 12. 初始化函数
void setup() {
Serial.begin(115200);// 初始化串口,方便调试AD按键值
u8g2.begin(); // 初始化OLED(I2C通信,自动识别设备地址)
u8g2.clearBuffer(); // 清空OLED缓存,避免残留乱码
resetGame(); // 调用新增的重置函数,初始化首次游戏参数(替代原重复代码)
randomSeed(analogRead(A1));// 用空闲模拟引脚做随机种子,让AI落子更随机
}
// 13. 主循环(修改:新增gameOver状态下的长按S5处理逻辑)
void loop() {
int key = readADKey();// 读取当前按键(先获取按键值,再分状态处理)
if (!gameOver && isPlayerTurn) {
// 状态1:游戏进行中+玩家回合(原逻辑不变,处理上下左右移动和确定落子)
switch (key) {
case 1:// S1:向上(y坐标-1,避免超出棋盘上边界0)
if (currY > 0) currY--;
break;
case 2:// S2:向左(x坐标-1,避免超出棋盘左边界0)
if (currX > 0) currX--;
break;
case 3:// S3:向下(y坐标+1,避免超出棋盘下边界2)
if (currY < 2) currY++;
break;
case 4:// S4:向右(x坐标+1,避免超出棋盘右边界2)
if (currX < 2) currX++;
break;
case 5:// S5:确定落子(仅当前位置为空时有效,避免重复落子)
if (chessBoard == EMPTY) {
chessBoard = PLAYER;// 落玩家棋子(空心圆)
if (checkWin(PLAYER)) { // 检查玩家是否赢
winner = PLAYER;
gameOver = true;
} else if (checkDraw()) { // 检查是否平局
gameOver = true;
} else {
isPlayerTurn = false; // 切换到人机回合
}
}
break;
}
} else if (!gameOver && !isPlayerTurn) {
// 状态2:游戏进行中+人机回合
delay(500);
aiMove();// 执行AI落子
// 落子后检查结果:人机赢或平局
if (checkWin(AI)) {
winner = AI;
gameOver = true;
} else if (checkDraw()) {
gameOver = true;
}
} else {
// 新增:状态3:游戏结束(赢/输/平局通用),处理S5长按重启
switch (key) {
case 5:// 检测到S5按键按下
if (!isKey5Pressed) {// 仅当按键未被标记为“按下”时,记录起始时间(避免重复记录)
isKey5Pressed = true; // 标记S5已按下
pressStartTime = millis(); // 记录按下的起始时间(单位:毫秒)
}
break;
case -1:// 检测到无按键按下(即S5已松开,判断是否为有效长按)
if (isKey5Pressed) {// 之前S5被按下过,现在松开,开始计算时长
unsigned long pressDuration = millis() - pressStartTime;// 计算按键按下的总时长
if (pressDuration >= LONG_PRESS_TIME) {// 时长≥1秒,判定为有效长按
resetGame();// 调用新增的重置函数,重启游戏
}
// 无论是否长按成功,都重置长按相关变量,为下次长按做准备
isKey5Pressed = false;
pressStartTime = 0;
}
break;
}
}
// OLED显示更新
u8g2.clearBuffer();
drawTipBar();
drawChessBoard();
drawChess();
drawWinLine();
u8g2.sendBuffer();
}进行上传后即可问题说明:1.若按键反馈有问题,你可以修改按键输入阈值。2.当前代码为第一版本,可能代码中会有部分bug,最终版已完成,暂时没有时间发表,请期待最终稳定板。
		页: 
[1]