ESP32 Bench
Boards: ESP32, S2, S3, C3, C6
Flash and monitor: Web Serial
← All projects

OLED Pong

Classic Pong on a tiny OLED with two knobs as paddles. Play a friend, or set one side to the computer.

Intermediate30 min115200 baud
Board
Fig. 1. Pins this project uses on the ESP32-DevKitC V4, 38 pin. USB connector at the bottom.

Wiring

FromTo boardNote
OLED VCC3V3
OLED GNDGND
OLED SDAGPIO 21
OLED SCLGPIO 22
Left knob, middle pinGPIO 34Outer pins of each knob to 3V3 and GND, never 5V
Right knob, middle pinGPIO 35Skip it and set TWO_PLAYERS to false to play the computer

Tested on this board

Not run on this board yet. Treat the wiring and code as a first draft.

Firmware, Arduino sketch for ESP32 Dev Module

// Pong on a 128 x 64 SSD1306 with two potentiometers as paddles.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int KNOB_LEFT = 34;    // ADC1 pins
const int KNOB_RIGHT = 35;
const bool TWO_PLAYERS = true;  // false: the right paddle plays itself
const int WIN_SCORE = 7;

const int W = 128, H = 64, PADDLE_H = 14, PADDLE_W = 3;
Adafruit_SSD1306 display(W, H, &Wire, -1);

float ballX, ballY, velX, velY;
float leftY = 25, rightY = 25;
int scoreLeft = 0, scoreRight = 0;

void serve(int direction) {
  ballX = W / 2;
  ballY = H / 2;
  velX = 1.6f * direction;
  velY = random(-10, 11) / 10.0f;
}

float knobToY(int pin, float current) {
  float target = analogRead(pin) * (H - PADDLE_H) / 4095.0f;
  return current + (target - current) * 0.5f;  // a little smoothing
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("SSD1306 not found at 0x3C");
    for (;;) delay(1000);
  }
  analogReadResolution(12);
  randomSeed(esp_random());
  serve(1);
  Serial.printf("Pong ready. Left paddle: GPIO %d, right paddle: %s\n", KNOB_LEFT, TWO_PLAYERS ? "knob" : "computer");
}

void loop() {
  leftY = knobToY(KNOB_LEFT, leftY);
  if (TWO_PLAYERS) {
    rightY = knobToY(KNOB_RIGHT, rightY);
  } else {
    float aim = ballY - PADDLE_H / 2;
    rightY += constrain(aim - rightY, -1.3f, 1.3f);  // beatable on purpose
    rightY = constrain(rightY, 0.0f, (float)(H - PADDLE_H));
  }

  ballX += velX;
  ballY += velY;
  if (ballY <= 0 || ballY >= H - 2) velY = -velY;

  // Paddles: the bounce angle depends on where the ball hits.
  if (velX < 0 && ballX <= 4 && ballY + 2 >= leftY && ballY <= leftY + PADDLE_H) {
    velX = -velX * 1.05f;
    velY = (ballY - (leftY + PADDLE_H / 2)) / 4;
  }
  if (velX > 0 && ballX >= W - 6 && ballY + 2 >= rightY && ballY <= rightY + PADDLE_H) {
    velX = -velX * 1.05f;
    velY = (ballY - (rightY + PADDLE_H / 2)) / 4;
  }
  velX = constrain(velX, -4.0f, 4.0f);

  if (ballX < 0 || ballX > W) {
    bool leftScored = ballX > W;
    if (leftScored) scoreLeft++;
    else scoreRight++;
    Serial.printf("point to %s, score %d-%d\n", leftScored ? "left" : "right", scoreLeft, scoreRight);
    if (scoreLeft == WIN_SCORE || scoreRight == WIN_SCORE) {
      Serial.printf("%s wins %d-%d\n", scoreLeft > scoreRight ? "left" : "right", scoreLeft, scoreRight);
      display.clearDisplay();
      display.setTextSize(2);
      display.setTextColor(SSD1306_WHITE);
      display.setCursor(10, 24);
      display.print(scoreLeft > scoreRight ? "LEFT WINS" : "RIGHT WINS");
      display.display();
      delay(3000);
      scoreLeft = scoreRight = 0;
    }
    serve(leftScored ? -1 : 1);
    delay(500);
  }

  display.clearDisplay();
  for (int y = 0; y < H; y += 6) display.drawFastVLine(W / 2, y, 3, SSD1306_WHITE);
  display.fillRect(1, (int)leftY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
  display.fillRect(W - 1 - PADDLE_W, (int)rightY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
  display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(W / 2 - 18, 0);
  display.print(scoreLeft);
  display.setCursor(W / 2 + 13, 0);
  display.print(scoreRight);
  display.display();
  delay(16);  // about 60 frames a second
}

Parts

QuantityPartWhere to buy
1×ESP32 dev boardBuy at Amazon
1×0.96 inch SSD1306 OLED, I2C, address 0x3C
2×10 kΩ potentiometersBuy at Adafruit
10×Jumper wires

Amazon buy links may include an Associates tag, and this site may earn a commission from qualifying Amazon purchases at no extra cost to you. Adafruit links are direct.

Libraries

Adafruit SSD1306, Adafruit GFX Library

Load it

  1. In the Arduino IDE, install the esp32 board package and pick the board named above the sketch.
  2. Install the libraries listed above from the Library Manager.
  3. Paste the sketch, choose the board's port and upload. Hold BOOT if the upload stalls at Connecting.
  4. Close the IDE's serial monitor, then open the bench here to watch the output.

Field notes

  • The knobs are on ADC1 pins. On the classic ESP32 those are GPIO 32 to 39; GPIO 34 and 35 are input only, which is all a knob needs.
  • Wire the knobs' outer pins to 3V3 and GND. Using 5V would put 5 V on the GPIO when the knob is turned all the way.
  • No second knob? Set TWO_PLAYERS to false and the right paddle plays itself. It is slow on purpose, so you can win.
  • If a paddle jitters, add a 100 nF capacitor from the knob's middle pin to GND.

Not tested yet

  • Not tested on hardware yet. Compiles for every board listed with Arduino-ESP32 core 3.3.12.

Built it? Send a build report with your board and what the serial port printed. Working reports become the "Tested on this board" entries above.