📝 程式碼詳細解說
讓我們一步步了解這個電流急急棒遊戲的程式碼!
📚 第一部分:引入函式庫與腳位定義
使用 TM1637 函式庫來控制數碼管,定義所有元件的接腳
#include <TM1637Display.h>
const int buttonPin = 2;
const int redPin = 3;
const int bluePin = 4;
const int greenPin = 5;
const int displayDIO = 6;
const int displayCLK = 7;
const int goalPin = 8;
const int obstaclePin = 10;
const int motorPin = 9;
const int buzzerPin = 11;
🎮 第二部分:狀態機設計
使用 enum(列舉)定義遊戲的三種狀態,讓程式邏輯更清晰
TM1637Display display(displayCLK, displayDIO);
enum GameState { IDLE, RUNNING, STOPPED };
GameState state = IDLE;
unsigned long startTime = 0;
float elapsed = 0;
bool buttonPressed = false;
bool gameStopped = false;
🧠 什麼是狀態機?
狀態機就像一個「遊戲規則管理員」!它定義了遊戲可能的幾種狀態,以及每種狀態下該做什麼:
- IDLE(待命):什麼都不做,等待玩家按下按鈕
- RUNNING(進行中):計時器在跑,隨時偵測是否碰到障礙或到達終點
- STOPPED(已停止):遊戲結束(成功或失敗),等待重新開始
有了狀態機,程式就不會亂!比如在 IDLE 狀態時,碰到障礙線不會觸發警報。
⏱️ 第三部分:防彈跳與顯示更新
按鈕防彈跳和數碼管的更新頻率控制
unsigned long lastButtonTime = 0;
const unsigned long debounceDelay = 50;
unsigned long lastDisplayTime = 0;
const unsigned long displayInterval = 100;
💡 為什麼需要控制顯示更新頻率?
如果每次 loop() 都更新數碼管,會產生閃爍的問題。設定每 100ms 才更新一次(每秒 10 次),既能即時顯示,又不會閃爍。這就是 millis() 計時法的好處!
⚙️ 第四部分:setup 開機設定
初始化所有接腳和元件
void setup() {
Serial.begin(9600);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
pinMode(goalPin, INPUT_PULLUP);
pinMode(obstaclePin, INPUT_PULLUP);
pinMode(motorPin, OUTPUT);
pinMode(buzzerPin, OUTPUT);
display.setBrightness(0x0f);
display.showNumberDecEx(0, 0b01000000, true);
setRGB(LOW, LOW, LOW);
digitalWrite(motorPin, LOW);
digitalWrite(buzzerPin, LOW);
Serial.println("系統啟動,等待按鈕");
}
🔑 INPUT_PULLUP 的重要性
終點線(D8)和障礙線(D10)都設為 INPUT_PULLUP:
- 平時電壓被「拉高」到 HIGH(沒碰到)
- 金屬環碰到時,接通 GND,變成 LOW(觸發!)
- 不需要外接電阻,Arduino 內建的就夠了
🔄 第五部分:loop 主迴圈
遊戲的核心邏輯都在這裡!
void loop() {
int goalState = digitalRead(goalPin);
int obstacleState = digitalRead(obstaclePin);
if (digitalRead(buttonPin) == LOW) {
if (millis() - lastButtonTime > debounceDelay) {
if (!buttonPressed) {
buttonPressed = true;
handleButtonPress();
}
lastButtonTime = millis();
}
} else {
buttonPressed = false;
}
if (state == RUNNING && !gameStopped) {
if (goalState == LOW) {
gameStopped = true;
setRGB(LOW, HIGH, LOW);
Serial.println("終點觸發 → 遊戲成功");
}
if (obstacleState == LOW) {
gameStopped = true;
setRGB(HIGH, LOW, LOW);
Serial.println("碰到障礙 → 遊戲失敗");
triggerAlarm();
}
if (millis() - lastDisplayTime >= displayInterval) {
elapsed = (millis() - startTime) / 1000.0;
int displayValue = (int)(elapsed + 0.05);
display.showNumberDecEx(displayValue, 0b01000000, true);
lastDisplayTime = millis();
}
}
if (state == IDLE) {
display.showNumberDecEx(0, 0b01000000, true);
}
}
⚠️ 為什麼不用 delay()?
在這個遊戲中,我們不能用 delay() 來計時!因為:
delay() 會讓 Arduino「暫停」,暫停期間無法偵測金屬環碰到障礙
- 使用
millis() 可以在計時的同時持續偵測所有感測器
- 這叫做「非阻塞式程式設計」,是遊戲開發的基本技巧!
🎮 第六部分:按鈕處理函式
按下按鈕時重置所有狀態,開始新一局
void handleButtonPress() {
state = RUNNING;
gameStopped = false;
startTime = millis();
elapsed = 0;
setRGB(LOW, LOW, HIGH);
digitalWrite(motorPin, LOW);
digitalWrite(buzzerPin, LOW);
Serial.println("遊戲開始,計時中...");
}
💡 第七部分:輔助函式
RGB 燈控制與警報觸發
void setRGB(bool r, bool g, bool b) {
digitalWrite(redPin, r);
digitalWrite(greenPin, g);
digitalWrite(bluePin, b);
}
void triggerAlarm() {
digitalWrite(motorPin, HIGH);
digitalWrite(buzzerPin, HIGH);
delay(500);
digitalWrite(motorPin, LOW);
digitalWrite(buzzerPin, LOW);
Serial.println("警報觸發 (馬達 + 蜂鳴器 0.5秒)");
}
🔔 triggerAlarm 的設計考量
這裡用了 delay(500),因為遊戲已經結束(gameStopped = true),不需要再偵測感測器了。0.5 秒的警報既能讓玩家明確感受到「碰到了!」,又不會太長讓人不耐煩。
📋 完整程式碼
點擊展開完整程式碼
#include <TM1637Display.h>
const int buttonPin = 2;
const int redPin = 3;
const int bluePin = 4;
const int greenPin = 5;
const int displayDIO = 6;
const int displayCLK = 7;
const int goalPin = 8;
const int obstaclePin = 10;
const int motorPin = 9;
const int buzzerPin = 11;
TM1637Display display(displayCLK, displayDIO);
enum GameState { IDLE, RUNNING, STOPPED };
GameState state = IDLE;
unsigned long startTime = 0;
float elapsed = 0;
bool buttonPressed = false;
bool gameStopped = false;
unsigned long lastButtonTime = 0;
const unsigned long debounceDelay = 50;
unsigned long lastDisplayTime = 0;
const unsigned long displayInterval = 100;
void setup() {
Serial.begin(9600);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
pinMode(goalPin, INPUT_PULLUP);
pinMode(obstaclePin, INPUT_PULLUP);
pinMode(motorPin, OUTPUT);
pinMode(buzzerPin, OUTPUT);
display.setBrightness(0x0f);
display.showNumberDecEx(0, 0b01000000, true);
setRGB(LOW, LOW, LOW);
digitalWrite(motorPin, LOW);
digitalWrite(buzzerPin, LOW);
Serial.println("系統啟動,等待按鈕");
}
void loop() {
int goalState = digitalRead(goalPin);
int obstacleState = digitalRead(obstaclePin);
if (digitalRead(buttonPin) == LOW) {
if (millis() - lastButtonTime > debounceDelay) {
if (!buttonPressed) {
buttonPressed = true;
handleButtonPress();
}
lastButtonTime = millis();
}
} else {
buttonPressed = false;
}
if (state == RUNNING && !gameStopped) {
if (goalState == LOW) {
gameStopped = true;
setRGB(LOW, HIGH, LOW);
Serial.println("終點觸發 → 遊戲成功");
}
if (obstacleState == LOW) {
gameStopped = true;
setRGB(HIGH, LOW, LOW);
Serial.println("碰到障礙 → 遊戲失敗");
triggerAlarm();
}
if (millis() - lastDisplayTime >= displayInterval) {
elapsed = (millis() - startTime) / 1000.0;
int displayValue = (int)(elapsed + 0.05);
display.showNumberDecEx(displayValue, 0b01000000, true);
lastDisplayTime = millis();
}
}
if (state == IDLE) {
display.showNumberDecEx(0, 0b01000000, true);
}
}
void handleButtonPress() {
state = RUNNING;
gameStopped = false;
startTime = millis();
elapsed = 0;
setRGB(LOW, LOW, HIGH);
digitalWrite(motorPin, LOW);
digitalWrite(buzzerPin, LOW);
Serial.println("遊戲開始,計時中...");
}
void setRGB(bool r, bool g, bool b) {
digitalWrite(redPin, r);
digitalWrite(greenPin, g);
digitalWrite(bluePin, b);
}
void triggerAlarm() {
digitalWrite(motorPin, HIGH);
digitalWrite(buzzerPin, HIGH);
delay(500);
digitalWrite(motorPin, LOW);
digitalWrite(buzzerPin, LOW);
Serial.println("警報觸發 (馬達 + 蜂鳴器 0.5秒)");
}