Breadboard reaction timer game with OLED display, push button, buzzer and ESP32-C3

ผู้เริ่มต้น

สร้างเกมจับเวลาปฏิกิริยาตอบสนอง

จอแสดงผลเกมอินพุต

รอสัญญาณ แล้วกดปุ่มให้เร็วที่สุดเท่าที่จะทำได้ -- จอแสดงผลขนาดเล็กและนาฬิกาจับเวลาที่สร้างขึ้นจากโค้ดของคุณทั้งหมด

รวมชิ้นส่วนจริง 7 รายการ

ราคารวมชุดนี้: 1,800.00 ฿

ยังไม่รวมส่วนลดคำสั่งซื้อที่คุณอาจได้รับตอนชำระเงิน

สิ่งที่คุณต้องใช้

เป้าหมายของโปรเจกต์

โปรเจกต์นี้สร้างเกมตอบสนองเวลาอย่างง่าย: รอสัญญาณ จากนั้นกดปุ่มให้เร็วที่สุดเท่าที่จะทำได้ โดยแสดงผลบนจอแสดงผลขนาดเล็ก ใต้เกมมีนาฬิกาจับเวลาที่สร้างขึ้นจากโค้ดของคุณทั้งหมด โดยไม่มีฮาร์ดแวร์จับเวลาเฉพาะเข้ามาเกี่ยวข้อง

คอนโทรลเลอร์ทำอะไร

คอนโทรลเลอร์จะรันเกมทั้งหมด: มันจะรอแบบสุ่ม ส่งเสียงออดเป็นสัญญาณ 'ไป' บันทึกช่วงเวลาที่เกิดขึ้นอย่างแม่นยำ เฝ้าดูการกดปุ่ม บันทึกช่วงเวลานั้นด้วย และคำนวณความแตกต่างระหว่างการประทับเวลาทั้งสองเป็นเวลาตอบสนอง

จอแสดงผลทำอะไร

หน้าที่เดียวของจอแสดงผล OLED คือการแสดงสถานะเกมปัจจุบัน -- การนับถอยหลังหรือตัวบ่งชี้ 'รอ' จากนั้นก็แสดงเวลาตอบสนองสุดท้ายเมื่อจบรอบ ไม่มีตรรกะเป็นของตัวเอง; คอนโทรลเลอร์เป็นผู้กำหนดทุกสิ่งที่แสดงและเรียกใช้การวาดใหม่ในการเปลี่ยนแปลงสถานะแต่ละครั้ง

ออดและปุ่มทำอะไร

ออดจะให้สัญญาณ 'ไป' ที่ผู้เล่นตอบสนอง และปุ่มกดคือวิธีการที่ผู้เล่นตอบสนองจริง ระหว่างสองสิ่งนี้จะกำหนดการประทับเวลาสองจุดที่เกมทั้งหมดสร้างขึ้นมา -- เมื่อ 'ไป' เกิดขึ้น และเมื่อผู้เล่นตอบสนองต่อมัน

แนวคิดนาฬิกาจับเวลา

ตรรกะของเกมจริงๆ ก็คือนากาจับเวลาที่มีสองเหตุการณ์ ประการแรก หลังจากหน่วงเวลาแบบสุ่ม (นานพอและหลากหลายพอที่ผู้เล่นไม่สามารถคาดการณ์ได้อย่างน่าเชื่อถือ) คอนโทรลเลอร์จะส่งเสียงออดและบันทึกการประทับเวลานั้นทันที ประการที่สอง เมื่อกดปุ่ม คอนโทรลเลอร์จะบันทึกการประทับเวลาที่สอง เวลาตอบสนองที่แสดงบนจอแสดงผลเป็นเพียงความแตกต่างระหว่างสองค่านี้ -- ไม่มีชิปจับเวลาเฉพาะที่ทำงานนี้ มันเป็นเรื่องของการที่คอนโทรลเลอร์อ่านนาฬิกาของตัวเองในสองช่วงเวลาที่เหมาะสม

แนวคิดซอฟต์แวร์และไลบรารี

โปรเจกต์นี้อาศัยตรรกะของโปรแกรมมากกว่าไลบรารีใดไลบรารีหนึ่ง: finite state machine (รอ, พร้อม, ไป, แสดงผลลัพธ์) จะควบคุมสิ่งที่คอนโทรลเลอร์ทำในแต่ละขั้นตอน ในขณะที่ไลบรารี OLED ต้องการเพียงการเรียกใช้การวาดข้อความง่ายๆ เพื่อแสดงสถานะการนับถอยหลังและตัวเลขสุดท้าย ทำให้ตรรกะการจับเวลาทำงานโดยแสดงผลลัพธ์ผ่าน Serial ก่อนที่จะเชื่อมต่อจอแสดงผล เพื่อยืนยันว่านาฬิกาจับเวลาถูกต้องก่อนที่จะกังวลเรื่องหน้าตา

การทดสอบและการแก้ไขปัญหา

  • ตัวจับเวลาอ่านค่าเป็นศูนย์หรือค่าที่ผิดปกติเสมอ -- ตรวจสอบว่าโค้ดของคุณบันทึกการประทับเวลาทั้งสองในช่วงเวลาที่ถูกต้องจริงหรือไม่ แทนที่จะแสดงตัวแปรที่ไม่ได้อัปเดต
  • ปุ่มไม่ตอบสนองการกดอย่างน่าเชื่อถือ -- นี่คือปัญหาการดีบาวซ์ของปุ่มเชิงกลแบบคลาสสิก ตรวจสอบการเดินสายไฟก่อน และพิจารณาเพิ่มการหน่วงเวลาการดีบาวซ์ในซอฟต์แวร์สั้นๆ หากการกดปุ่มยังคงไม่สอดคล้องกัน
  • จอแสดงผลไม่เคยอัปเดต -- ยืนยันว่าต่อสายถูกต้องและโค้ดของคุณเรียกใช้ฟังก์ชันรีเฟรชหน้าจอในการเปลี่ยนแปลงสถานะทุกครั้ง ไม่ใช่แค่เมื่อโปรแกรมเริ่มต้น
  • ผู้เล่นสามารถคาดเดาสัญญาณ 'ไป' ได้ -- ขยายช่วงการหน่วงเวลาแบบสุ่ม; หน้าต่างการหน่วงเวลาที่แคบหรือสั้นทำให้การจับเวลาคาดเดาได้ง่ายกว่าการตอบสนองอย่างแท้จริง

การอัปเกรดเสริมในอนาคต: การแข่งขันปฏิกิริยาสองผู้เล่น

เมื่อเวอร์ชันผู้เล่นคนเดียวทำงานได้อย่างมั่นคง การเพิ่มปุ่มที่สองและเปรียบเทียบเวลาปฏิกิริยาของผู้เล่นสองคนในรอบเดียวกันเป็นขั้นตอนต่อไปที่เป็นธรรมชาติ -- ตรรกะของนาฬิกาจับเวลาเดียวกันนี้เพียงแค่ต้องติดตามการประทับเวลาสองครั้งต่อรอบแทนที่จะเป็นหนึ่ง

การเดินสายไฟ

  • OLED VCC → ESP32-C3 SuperMini 3.3V
  • OLED GND → ESP32-C3 SuperMini GND
  • OLED SDA → GPIO8 (`SDA_PIN`) — ESP32-C3 ค่าเริ่มต้น I2C SDA
  • OLED SCL → GPIO9 (`SCL_PIN`) — ESP32-C3 ค่าเริ่มต้น I2C SCL
  • Active Buzzer signal/+ → GPIO4 (`BUZZER_PIN`)
  • Active Buzzer GND → ESP32-C3 SuperMini GND
  • Push Button leg 1 → GPIO3 (`BUTTON_PIN`)
  • Push Button leg 2 → ESP32-C3 SuperMini GND

ปุ่มใช้ `INPUT_PULLUP` จึงอ่านค่า LOW เมื่อกด — ไม่ต้องใช้ตัวต้านทานภายนอก สเก็ตช์จะดีบาวซ์ปุ่มในซอฟต์แวร์และกำหนดให้ผู้เล่นปล่อยปุ่มทั้งหมดก่อนที่จะเริ่มรอบใหม่ได้ ซึ่งจะช่วยป้องกันการกดค้างไว้เพื่อโกง

แผนภาพการต่อสาย

VCC → 3.3V GND SDA → GPIO8 SCL → GPIO9 signal/+ → GPIO4 GND leg 1 → GPIO3 leg 2 → GND ESP32-C3 SuperMini OLED Active Buzzer Push Button

โค้ดเริ่มต้น

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SDA_PIN 8      // ESP32-C3 default I2C SDA - adjust if your board differs
#define SCL_PIN 9      // ESP32-C3 default I2C SCL - adjust if your board differs
#define BUTTON_PIN 3   // Tactile button, other leg to GND - any digital input GPIO
#define BUZZER_PIN 4   // Active buzzer signal - any digital output GPIO

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDRESS 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

enum GameState { IDLE, GET_READY, ARMED, GO, SHOW_RESULT };
GameState state = IDLE;

unsigned long stateEnteredAt = 0;
unsigned long randomDelay = 0;
unsigned long goTime = 0;

bool rawPressed() {
  return digitalRead(BUTTON_PIN) == LOW; // INPUT_PULLUP: LOW = pressed
}

// Returns true once, only on the release -> press transition (debounced).
bool pressedEdge() {
  static bool lastStable = false;
  static unsigned long lastChange = 0;
  bool reading = rawPressed();
  if (reading != lastStable && millis() - lastChange > 30) {
    lastStable = reading;
    lastChange = millis();
    return lastStable; // true only on the press edge
  }
  return false;
}

void showMessage(const char* line1, const char* line2) {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 10);
  display.println(line1);
  display.setTextSize(1);
  display.setCursor(0, 44);
  display.println(line2);
  display.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);

  Wire.begin(SDA_PIN, SCL_PIN);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println("SSD1306 init failed - check wiring/address");
    while (true) delay(1000);
  }

  randomSeed(analogRead(0)); // floating ADC pin as an entropy source
  showMessage("Reaction Timer", "Press to start");
}

void loop() {
  bool edge = pressedEdge();

  switch (state) {
    case IDLE:
      if (edge) {
        showMessage("Get ready...", "");
        state = GET_READY;
        stateEnteredAt = millis();
      }
      break;

    case GET_READY:
      // brief pause so the player lets go of the button before arming
      if (millis() - stateEnteredAt > 500) {
        randomDelay = random(1500, 5000); // 1.5-5s random wait before GO
        stateEnteredAt = millis();
        state = ARMED;
      }
      break;

    case ARMED:
      if (rawPressed()) {
        showMessage("Too early!", "Press to retry");
        digitalWrite(BUZZER_PIN, HIGH);
        delay(200);
        digitalWrite(BUZZER_PIN, LOW);
        state = SHOW_RESULT;
      } else if (millis() - stateEnteredAt >= randomDelay) {
        showMessage("GO!", "");
        digitalWrite(BUZZER_PIN, HIGH);
        goTime = millis();
        state = GO;
      }
      break;

    case GO:
      if (millis() - goTime > 150) {
        digitalWrite(BUZZER_PIN, LOW); // stop the short GO beep after 150ms
      }
      if (rawPressed()) {
        unsigned long reactionMs = millis() - goTime;
        char line1[16];
        snprintf(line1, sizeof(line1), "%lu ms", reactionMs);
        showMessage(line1, "Press to retry");
        Serial.printf("Reaction time: %lu ms\n", reactionMs);
        state = SHOW_RESULT;
      }
      break;

    case SHOW_RESULT:
      if (edge) {
        showMessage("Get ready...", "");
        state = GET_READY;
        stateEnteredAt = millis();
      }
      break;
  }
}

ไม่แน่ใจขั้นตอนไหน? ถามเรา