/*
* ESP32 Ping Pong Game
* Display: 0.96" OLED (SSD1306, 128x64, I2C)
*
* Wiring:
* OLED SDA -> GPIO 21
* OLED SCL -> GPIO 22
* OLED VCC -> 3.3V
* OLED GND -> GND
*
* LEFT Button -> GPIO 12 (other leg to GND)
* RIGHT Button -> GPIO 14 (other leg to GND)
* START Button -> GPIO 27 (other leg to GND, also used to restart after game over)
*
* Libraries required (install via Arduino Library Manager):
* - Adafruit SSD1306
* - Adafruit GFX Library
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ── Display config ──────────────────────────────────────────────────────────
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // not used with I2C
#define SCREEN_ADDRESS 0x3C // most common SSD1306 address
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ── Button pins ─────────────────────────────────────────────────────────────
#define BTN_LEFT 12
#define BTN_RIGHT 14
#define BTN_START 27
// ── Game dimensions ─────────────────────────────────────────────────────────
#define PADDLE_W 24
#define PADDLE_H 4
#define PADDLE_Y (SCREEN_HEIGHT - 6) // y position of paddle top edge
#define PADDLE_SPEED 3
#define BALL_SIZE 3
#define SCORE_AREA 10 // pixels reserved at top for score bar
// ── Game state ───────────────────────────────────────────────────────────────
float ballX, ballY;
float ballDX, ballDY;
int paddleX;
int score;
bool gameOver;
bool gameStarted;
// Debounce helpers
unsigned long lastLeft = 0;
unsigned long lastRight = 0;
unsigned long lastStart = 0;
#define DEBOUNCE_MS 50
// ── Helpers ──────────────────────────────────────────────────────────────────
bool btnPressed(int pin, unsigned long &lastTime) {
if (digitalRead(pin) == LOW) {
unsigned long now = millis();
if (now - lastTime > DEBOUNCE_MS) {
lastTime = now;
return true;
}
}
return false;
}
void initGame() {
paddleX = (SCREEN_WIDTH - PADDLE_W) / 2;
// Ball starts just above paddle centre
ballX = SCREEN_WIDTH / 2.0f;
ballY = PADDLE_Y - BALL_SIZE - 2;
// Random horizontal direction, always going upward first
float angle = radians(random(210, 330)); // 210–330° → upward arc
float speed = 2.2f;
ballDX = cos(angle) * speed;
ballDY = sin(angle) * speed;
score = 0;
gameOver = false;
}
// ── Setup ────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
pinMode(BTN_START, INPUT_PULLUP);
randomSeed(analogRead(0));
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
gameStarted = false;
gameOver = false;
score = 0;
showTitleScreen();
}
// ── Title screen ─────────────────────────────────────────────────────────────
void showTitleScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(14, 10);
display.print(F("PONG"));
display.setTextSize(1);
display.setCursor(10, 36);
display.print(F("Press START"));
display.setCursor(16, 48);
display.print(F("to begin!"));
display.display();
}
// ── Game-over screen ─────────────────────────────────────────────────────────
void showGameOver() {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(4, 6);
display.print(F("GAME OVR"));
display.setTextSize(1);
display.setCursor(28, 30);
display.print(F("Score: "));
display.print(score);
display.setCursor(10, 46);
display.print(F("Press START"));
display.setCursor(22, 56);
display.print(F("to retry"));
display.display();
}
// ── Main loop ────────────────────────────────────────────────────────────────
void loop() {
// ── Wait for start ────────────────────────────────────────────────────────
if (!gameStarted) {
if (btnPressed(BTN_START, lastStart)) {
gameStarted = true;
initGame();
}
return;
}
// ── Game-over state ───────────────────────────────────────────────────────
if (gameOver) {
if (btnPressed(BTN_START, lastStart)) {
initGame();
}
return;
}
// ── Read paddle input ─────────────────────────────────────────────────────
if (digitalRead(BTN_LEFT) == LOW) {
paddleX -= PADDLE_SPEED;
if (paddleX < 0) paddleX = 0;
}
if (digitalRead(BTN_RIGHT) == LOW) {
paddleX += PADDLE_SPEED;
if (paddleX > SCREEN_WIDTH - PADDLE_W) paddleX = SCREEN_WIDTH - PADDLE_W;
}
// ── Move ball ─────────────────────────────────────────────────────────────
ballX += ballDX;
ballY += ballDY;
// Wall bounces (left / right)
if (ballX <= 0) {
ballX = 0;
ballDX = -ballDX;
} else if (ballX + BALL_SIZE >= SCREEN_WIDTH) {
ballX = SCREEN_WIDTH - BALL_SIZE;
ballDX = -ballDX;
}
// Top wall bounce
if (ballY <= SCORE_AREA) {
ballY = SCORE_AREA;
ballDY = -ballDY;
}
// Paddle collision
if (ballY + BALL_SIZE >= PADDLE_Y &&
ballY + BALL_SIZE <= PADDLE_Y + PADDLE_H + 2 &&
ballX + BALL_SIZE >= paddleX &&
ballX <= paddleX + PADDLE_W) {
ballY = PADDLE_Y - BALL_SIZE; // push ball above paddle
ballDY = -ballDY; // reverse vertical
// Add a little spin based on where ball hits paddle
float hitPos = (ballX + BALL_SIZE / 2.0f) - (paddleX + PADDLE_W / 2.0f);
ballDX += hitPos * 0.06f;
// Clamp speed so it doesn't get out of hand
float speed = sqrt(ballDX * ballDX + ballDY * ballDY);
if (speed > 4.5f) {
ballDX = ballDX / speed * 4.5f;
ballDY = ballDY / speed * 4.5f;
}
score++;
}
// Missed ball → game over
if (ballY > SCREEN_HEIGHT) {
gameOver = true;
showGameOver();
return;
}
// ── Draw frame ────────────────────────────────────────────────────────────
display.clearDisplay();
// Score bar
display.setTextSize(1);
display.setCursor(0, 0);
display.print(F("Score:"));
display.print(score);
// Divider line under score
display.drawLine(0, SCORE_AREA - 1, SCREEN_WIDTH - 1, SCORE_AREA - 1, SSD1306_WHITE);
// Ball
display.fillRect((int)ballX, (int)ballY, BALL_SIZE, BALL_SIZE, SSD1306_WHITE);
// Paddle
display.fillRect(paddleX, PADDLE_Y, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.display();
delay(16); // ~60 fps target
}
Loading
esp32-devkit-c-v4
esp32-devkit-c-v4