阶段1:最小可行原型 - 完整代码框架

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
digimon-pet/
├── platformio.ini # PlatformIO 项目配置
├── src/
│ ├── main.cpp # 入口文件,setup() + loop()
│ ├── config.h # 引脚定义 + 全局常量
│ ├── display.h # OLED 显示驱动封装
│ ├── display.cpp
│ ├── button.h # 按键驱动(消抖 + 长短按)
│ ├── button.cpp
│ ├── statemachine.h # 游戏状态机
│ └── statemachine.cpp
└── README.md

1. platformio.ini(项目配置)

1
2
3
4
5
6
7
8
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

lib_deps =
olikraus/U8g2@^2.35.30

使用 Arduino 框架 + U8g2 库驱动 SSD1306 OLED。


2. src/config.h(引脚定义 + 全局常量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef CONFIG_H
#define CONFIG_H

// ========== 引脚定义 ==========
// OLED (I2C)
#define OLED_SDA 21
#define OLED_SCL 22

// 按键(上拉输入,按下为 LOW)
#define BTN_A 14 // 确认
#define BTN_B 12 // 取消/返回
#define BTN_C 13 // 选择/菜单

// 蜂鸣器
#define BUZZER_PIN 25

// 震动马达
#define VIB_PIN 27

// LED 呼吸灯
#define LED_PIN 33

// ========== 按键消抖参数 ==========
#define DEBOUNCE_DELAY 50 // 消抖延迟(ms)
#define LONG_PRESS_TIME 800 // 长按判定(ms)

// ========== 游戏状态枚举 ==========
enum GameState {
STATE_EGG, // 数码蛋待孵化
STATE_MAIN_MENU, // 主菜单
STATE_PET_STATUS, // 宠物状态
STATE_COUNT // 状态总数(用于循环)
};

#endif

3. src/display.h + display.cpp(OLED 驱动封装)

display.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef DISPLAY_H
#define DISPLAY_H

#include <U8g2lib.h>
#include "config.h"

// 全局 OLED 对象(SSD1306, 128x64, I2C)
extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2;

// 初始化 OLED
void displayInit();

// 显示数码蛋
void drawEgg();

// 显示主菜单(传入当前选项索引 0~2)
void drawMainMenu(int selected);

// 显示宠物状态占位页
void drawPetStatus();

#endif

display.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "display.h"

// 初始化 U8g2 对象
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R0, // 不旋转
/* reset=*/ U8X8_PIN_NONE,
/* clock=*/ OLED_SCL,
/* data=*/ OLED_SDA
);

void displayInit() {
u8g2.begin();
u8g2.setContrast(128); // 亮度 0~255
}

// 绘制一个简单的像素数码蛋
void drawEgg() {
u8g2.clearBuffer();

// 蛋壳下半部分
u8g2.drawDisc(64, 38, 20, U8G2_DRAW_ALL); // 圆形主体
u8g2.drawBox(44, 20, 40, 20); // 上半矩形
u8g2.drawRBox(44, 20, 40, 36, 8); // 圆角矩形蛋壳

// 裂缝线
u8g2.drawLine(52, 28, 58, 44);
u8g2.drawLine(64, 26, 64, 42);
u8g2.drawLine(76, 28, 70, 44);

// 标题
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(20, 62, "Digimon Egg");

u8g2.sendBuffer();
}

// 主菜单(3个选项)
void drawMainMenu(int selected) {
u8g2.clearBuffer();

u8g2.setFont(u8g2_font_8x13B_tr);
u8g2.drawStr(20, 14, "=== MENU ===");

const char* items[] = {"Status", "Feed", "Train"};
for (int i = 0; i < 3; i++) {
u8g2.setFont(u8g2_font_7x14_tr);
// 被选中项前面加 ">"
char buf[20];
snprintf(buf, sizeof(buf), "%s %s",
(i == selected) ? ">" : " ",
items[i]);
u8g2.drawStr(30, 32 + i * 16, buf);
}

u8g2.sendBuffer();
}

// 宠物状态占位
void drawPetStatus() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_8x13B_tr);
u8g2.drawStr(16, 14, "Pet Status");

u8g2.setFont(u8g2_font_7x14_tr);
u8g2.drawStr(10, 36, "Name: Botamon");
u8g2.drawStr(10, 50, "Stage: Baby I");
u8g2.drawStr(10, 64, "Hunger: 80%");

u8g2.sendBuffer();
}

4. src/button.h + button.cpp(按键驱动)

button.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef BUTTON_H
#define BUTTON_H

#include <Arduino.h>
#include "config.h"

// 按键事件类型
enum ButtonEvent {
BTN_NONE = 0,
BTN_SHORT_PRESS, // 短按
BTN_LONG_PRESS // 长按
};

// 初始化所有按键引脚
void buttonInit();

// 扫描按键,返回哪个按键 + 事件类型
// 返回: bit[7:4] = 事件类型, bit[3:0] = 按键编号 (0=A, 1=B, 2=C)
// 示例: (BTN_SHORT_PRESS << 4) | 0 表示 A 键短按
uint8_t buttonScan();

#endif

button.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include "button.h"

// 按键引脚数组
static const int btnPins[3] = { BTN_A, BTN_B, BTN_C };

// 每个按键的状态跟踪
struct BtnState {
bool lastReading; // 上一次读数
bool stable; // 当前稳定值
unsigned long lastDebounceTime;
unsigned long pressStartTime;
bool longPressFired; // 长按是否已触发
};

static BtnState btns[3];

void buttonInit() {
for (int i = 0; i < 3; i++) {
pinMode(btnPins[i], INPUT_PULLUP);
btns[i].lastReading = HIGH;
btns[i].stable = HIGH;
btns[i].lastDebounceTime = 0;
btns[i].pressStartTime = 0;
btns[i].longPressFired = false;
}
}

uint8_t buttonScan() {
uint8_t result = BTN_NONE;

for (int i = 0; i < 3; i++) {
bool reading = digitalRead(btnPins[i]);
BtnState& s = btns[i];

// 消抖
if (reading != s.lastReading) {
s.lastDebounceTime = millis();
}
if ((millis() - s.lastDebounceTime) > DEBOUNCE_DELAY) {
s.stable = reading;
}
s.lastReading = reading;

// 按键按下
if (s.stable == LOW && s.pressStartTime == 0) {
s.pressStartTime = millis();
s.longPressFired = false;
}

// 按键松开
if (s.stable == HIGH && s.pressStartTime != 0) {
unsigned long duration = millis() - s.pressStartTime;
if (duration >= LONG_PRESS_TIME) {
result = (BTN_LONG_PRESS << 4) | i;
} else if (duration > DEBOUNCE_DELAY) {
result = (BTN_SHORT_PRESS << 4) | i;
}
s.pressStartTime = 0;
}

// 持续长按中(实时触发一次)
if (s.stable == LOW && s.pressStartTime != 0 && !s.longPressFired) {
if ((millis() - s.pressStartTime) >= LONG_PRESS_TIME) {
s.longPressFired = true;
result = (BTN_LONG_PRESS << 4) | i;
}
}
}

return result;
}

5. src/statemachine.h + statemachine.cpp(状态机)

statemachine.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef STATEMACHINE_H
#define STATEMACHINE_H

#include "config.h"

// 获取当前游戏状态
GameState getGameState();

// 处理按键事件,可能切换状态
void handleInput(uint8_t btnEvent);

// 初始化状态机
void stateMachineInit();

#endif

statemachine.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include "statemachine.h"
#include "display.h"

static GameState currentState = STATE_EGG;
static int menuSelected = 0; // 主菜单当前选中项

// 每个状态进入时调用一次
static void enterState(GameState newState) {
currentState = newState;
switch (newState) {
case STATE_EGG:
drawEgg();
break;
case STATE_MAIN_MENU:
menuSelected = 0;
drawMainMenu(menuSelected);
break;
case STATE_PET_STATUS:
drawPetStatus();
break;
}
}

void stateMachineInit() {
enterState(STATE_EGG);
}

GameState getGameState() {
return currentState;
}

void handleInput(uint8_t btnEvent) {
uint8_t evtType = btnEvent >> 4;
uint8_t btnId = btnEvent & 0x0F;

// ---- 状态: 数码蛋 ----
if (currentState == STATE_EGG) {
// 任意按键短按 → 进入主菜单
if (evtType == BTN_SHORT_PRESS) {
enterState(STATE_MAIN_MENU);
}
return;
}

// ---- 状态: 主菜单 ----
if (currentState == STATE_MAIN_MENU) {
if (evtType == BTN_SHORT_PRESS) {
if (btnId == 0) { // A键 = 确认
// 根据当前选中项进入对应子状态
switch (menuSelected) {
case 0: enterState(STATE_PET_STATUS); break;
case 1: /* TODO: 喂食 */ break;
case 2: /* TODO: 训练 */ break;
}
} else if (btnId == 1) { // B键 = 取消/返回
enterState(STATE_EGG);
} else if (btnId == 2) { // C键 = 切换选项
menuSelected = (menuSelected + 1) % 3;
drawMainMenu(menuSelected);
}
}
return;
}

// ---- 状态: 宠物状态 ----
if (currentState == STATE_PET_STATUS) {
if (evtType == BTN_SHORT_PRESS && btnId == 1) {
// B键返回主菜单
enterState(STATE_MAIN_MENU);
}
return;
}
}

6. src/main.cpp(主入口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <Arduino.h>
#include "config.h"
#include "display.h"
#include "button.h"
#include "statemachine.h"

void setup() {
Serial.begin(115200);
Serial.println("Digimon VPET - Stage 1");

displayInit();
buttonInit();
stateMachineInit();

// 可选:开机蜂鸣提示
pinMode(BUZZER_PIN, OUTPUT);
tone(BUZZER_PIN, 880, 100);
delay(150);
tone(BUZZER_PIN, 660, 100);
delay(150);
noTone(BUZZER_PIN);

Serial.println("Ready.");
}

void loop() {
// 每帧扫描一次按键
uint8_t btn = buttonScan();

// 如果有按键事件,交给状态机处理
if (btn != BTN_NONE) {
Serial.print("Button event: ");
Serial.println(btn, HEX);
handleInput(btn);
}

delay(10); // 10ms 扫描间隔
}

代码框架总结

文件 职责 关键点
platformio.ini 项目配置 ESP32 + U8g2 库
config.h 常量定义 引脚、消抖参数、状态枚举
display.h/.cpp OLED 驱动 数码蛋绘制、菜单渲染、状态页
button.h/.cpp 按键驱动 消抖算法、短按/长按检测
statemachine.h/.cpp 状态机 3 状态切换逻辑
main.cpp 入口 初始化 + 主循环扫描

如何使用

  1. 安装 VSCode + PlatformIO 插件
  2. 创建新项目 digimon-pet,选择板子 Espressif ESP32 Dev Module
  3. 将上述代码复制到对应文件
  4. 连接 ESP32,点击 PlatformIO 的 Upload and Monitor 按钮
  5. 串口监视器输出 Ready. 后,OLED 显示数码蛋,按 A 键进入菜单

按键操作说明

按键 蛋状态 菜单状态 状态页
A (确认) 进入菜单 确认选项
B (取消) 进入菜单 返回蛋 返回菜单
C (选择) 进入菜单 切换选项