OLED pong on an ESP32-C3
The thumb-stick pong, on the other board: a twelve-bit ADC moves the paddle and the score is drawn in digits. The whole build, an ESP32-C3 and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
AdvancedRuns in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// OLED pong: a game of pong on a 128x64 SSD1306, played with a thumb stick,
// on an ESP32-C3.
//
// Wiring, board to modules:
// OLED SCL -> GPIO 5 SDA -> GPIO 4 VCC -> 3V3 GND -> GND
// stick VRY -> GPIO 0 SW -> GPIO 3 VCC -> 3V3 GND -> GND
//
// Push the stick up and down and the left paddle follows it outright, because a
// potentiometer is an absolute position and so is a paddle: there is no
// acceleration to fight and no key to hold. The board plays the right paddle.
// First to nine. Press the stick to serve again.
//
// # The stick works here now
//
// This example used to be an Uno's alone: a thumb stick is two potentiometers,
// and the ESP32-C3 model had no ADC to read one with. It has one now (ADC1,
// one-shot, on GPIO 0 to GPIO 4 (docs/esp32c3.md section 3.5), so analogRead
// on GPIO 0 is a real twelve-bit conversion of the voltage the stick's Y pot
// puts on the pin, and the paddle follows the stick rather than a button.
//
// The Uno version of this game is still there and still an Uno's, so the two
// can be opened side by side. Wire for wire they are the same circuit; the
// differences are all in this file.
//
// # Twelve bits, not ten
//
// An axis here is 0..4095 rather than 0..1023, so the sketch maps the range on
// to the 39 rows a paddle may start on with `* 5 >> 9` where the Uno uses
// `* 5 >> 7`. Both are the mapping to within a row without a divide.
//
// # The score is digits
//
// The Uno version draws the score as nine pips a side, because the 5x7 font and
// the routine that walks it are eleven hundred bytes of an ATmega328P's eight
// kilobyte budget, and a kilobyte of that budget is already the frame buffer.
// This board has sixteen kilobytes of budget and four hundred of RAM, so the
// font costs nothing worth counting and the score is two numerals on a line
// with the word they are keeping.
//
// # Painting only what moved
//
// A 128x64 monochrome frame is 1024 bytes and this board bit-bangs the bus at
// about 196 kbit/s, so sending all of it every frame would be forty
// milliseconds of pin changes for the kernel to carry. mokxi_ssd1306.h sends
// only the eight-column blocks of each page that changed since the last
// display(), and this sketch helps it: each frame rubs out the ball and the two
// paddles where they were, draws them where they are now, and touches nothing
// else. The court and the net are drawn once, the score only when it changes.
//
// # The ball
//
// Sixteenths of a pixel, so the ball can move at a shallow angle without the
// integer arithmetic snapping it to 45 degrees. Where it hits the paddle sets
// the angle it leaves at (the ends throw it steeply, the middle straight),
// which is the one rule that makes pong a game of placement rather than of
// reflexes.
#include "mokxi_i2c.h"
#include "mokxi_ssd1306.h"
const uint8_t SCL_PIN = 5;
const uint8_t SDA_PIN = 4;
const uint8_t VRY_PIN = 0;
const uint8_t SW_PIN = 3;
SoftI2c bus(SCL_PIN, SDA_PIN, 400);
Ssd1306 oled(bus, SSD1306_ADDRESS);
// The stick is read straight rather than through mokxi_joystick.h: this sketch
// wants one absolute axis and a button, not a centred, dead-zoned pair.
// The court: the whole panel below the score line.
const int16_t TOP = 10;
const int16_t BOTTOM = 63;
const int16_t LEFT = 0;
const int16_t RIGHT = 127;
const int16_t PADDLE_H = 14;
const int16_t PADDLE_W = 2;
const int16_t BALL = 3;
const int16_t PLAYER_X = 2;
const int16_t COMPUTER_X = RIGHT - 1 - PADDLE_W;
/** Sixteenths of a pixel: the ball's own units. */
const int16_t SUB = 16;
const uint8_t SUB_BITS = 4;
const uint8_t TARGET = 9;
/** The column the net runs down. */
const int16_t NET_X = 63;
int16_t playerY = TOP + 8;
int16_t computerY = TOP + 8;
int16_t drawnPlayerY = -1;
int16_t drawnComputerY = -1;
int16_t ballX = 64 * SUB;
int16_t ballY = 32 * SUB;
int16_t ballDX = 3 * SUB / 2;
int16_t ballDY = SUB / 2;
int16_t drawnBallX = -1;
int16_t drawnBallY = -1;
uint8_t playerScore = 0;
uint8_t computerScore = 0;
bool waiting = true;
/** How far a paddle may sit, so it never leaves the court. */
static int16_t clampPaddle(int16_t y)
{
if (y < TOP + 1) {
return (int16_t)(TOP + 1);
}
if (y > BOTTOM - PADDLE_H) {
return (int16_t)(BOTTOM - PADDLE_H);
}
return y;
}
static void drawCourt()
{
oled.clear();
// The net, and the line under the score.
oled.fillRect(NET_X, (int16_t)(TOP + 2), 1, (int16_t)(BOTTOM - TOP - 1), true);
oled.fillRect(LEFT, TOP, (int16_t)(RIGHT - LEFT + 1), 1, true);
}
/** The score, in the 5x7 font: YOU on the left, C3 on the right. */
static void drawScore()
{
oled.fillRect(0, 0, 128, 9, false);
oled.text(2, 1, "YOU");
oled.number(22, 1, (long)playerScore);
oled.number(99, 1, (long)computerScore);
oled.text(107, 1, "C3");
// A cue that the board is waiting for a serve: the net's top pixel blinks
// off, which costs nothing and is the only thing on the panel that moves.
oled.fillRect(NET_X, (int16_t)(TOP + 2), 1, 3, !waiting);
}
/** Rub out the moving things where they were, and draw them where they are. */
static void paint()
{
if (drawnPlayerY >= 0 && drawnPlayerY != playerY) {
oled.fillRect(PLAYER_X, drawnPlayerY, PADDLE_W, PADDLE_H, false);
}
if (drawnComputerY >= 0 && drawnComputerY != computerY) {
oled.fillRect(COMPUTER_X, drawnComputerY, PADDLE_W, PADDLE_H, false);
}
int16_t bx = (int16_t)(ballX >> SUB_BITS);
int16_t by = (int16_t)(ballY >> SUB_BITS);
if (drawnBallX >= 0 && (drawnBallX != bx || drawnBallY != by)) {
oled.fillRect(drawnBallX, drawnBallY, BALL, BALL, false);
// The net runs down the middle of the court, so rubbing the ball out there
// takes a piece of it with it. Put that piece back.
if (drawnBallX <= NET_X && drawnBallX + BALL > NET_X) {
oled.fillRect(NET_X, drawnBallY, 1, BALL, true);
}
}
oled.fillRect(PLAYER_X, playerY, PADDLE_W, PADDLE_H, true);
oled.fillRect(COMPUTER_X, computerY, PADDLE_W, PADDLE_H, true);
oled.fillRect(bx, by, BALL, BALL, true);
drawnPlayerY = playerY;
drawnComputerY = computerY;
drawnBallX = bx;
drawnBallY = by;
oled.display();
}
/** Put the ball back in the middle, heading at `dir`. */
static void serve(int16_t dir)
{
ballX = 64 * SUB;
ballY = (int16_t)(((TOP + BOTTOM) / 2) * SUB);
ballDX = (int16_t)(dir * 3 * SUB / 2);
ballDY = (int16_t)(((playerScore + computerScore) & 1) == 0 ? SUB / 2 : -SUB / 2);
drawnBallX = -1;
drawnBallY = -1;
}
/**
* The angle the ball leaves a paddle at: where along the paddle it struck, as a
* fraction either side of the middle, scaled to a sensible slope.
*/
static int16_t bounceAngle(int16_t by, int16_t paddleY)
{
int16_t centre = (int16_t)(paddleY + PADDLE_H / 2);
int16_t offset = (int16_t)(by + BALL / 2 - centre);
// The paddle is fourteen tall, so the offset runs about +/- 7; doubling it
// spans the slope the ball is allowed.
int16_t dy = (int16_t)(offset * 2);
if (dy > SUB + SUB / 2) {
dy = (int16_t)(SUB + SUB / 2);
}
if (dy < -(SUB + SUB / 2)) {
dy = (int16_t)(-(SUB + SUB / 2));
}
return dy;
}
void setup()
{
Serial.begin(115200);
Serial.println("Mokxi ESP32-C3: pong on a 128x64 OLED, played with a thumb stick");
pinMode(SW_PIN, INPUT_PULLUP);
if (!oled.begin()) {
Serial.println("no OLED acknowledged 0x3c");
return;
}
drawCourt();
drawScore();
paint();
}
void loop()
{
// The stick: Y counts down the screen, so pushing it up moves the paddle up.
// 0..4095 onto the 39 rows the paddle may start on. `* 5 >> 9` is that to
// within a row and stays in sixteen bits.
int16_t axis = (int16_t)analogRead(VRY_PIN);
playerY = clampPaddle((int16_t)(TOP + 1 + ((axis * 5) >> 9)));
if (waiting) {
paint();
if (digitalRead(SW_PIN) == LOW) {
waiting = false;
if (playerScore >= TARGET || computerScore >= TARGET) {
playerScore = 0;
computerScore = 0;
drawCourt();
}
serve(playerScore > computerScore ? -1 : 1);
drawScore();
// Let go of the switch before the next round listens for it.
while (digitalRead(SW_PIN) == LOW) {
delay(8);
}
}
delay(24);
return;
}
// The board's paddle: it chases the ball, but not at full speed, so a ball
// placed at the far end of its travel gets past it.
int16_t want = (int16_t)((ballY >> SUB_BITS) - PADDLE_H / 2);
if (want > computerY + 1) {
computerY = (int16_t)(computerY + 2);
} else if (want < computerY - 1) {
computerY = (int16_t)(computerY - 2);
}
computerY = clampPaddle(computerY);
ballX = (int16_t)(ballX + ballDX);
ballY = (int16_t)(ballY + ballDY);
// The top and bottom walls.
if (ballY < (TOP + 1) * SUB) {
ballY = (int16_t)((TOP + 1) * SUB);
ballDY = (int16_t)(-ballDY);
}
if (ballY > (BOTTOM - BALL) * SUB) {
ballY = (int16_t)((BOTTOM - BALL) * SUB);
ballDY = (int16_t)(-ballDY);
}
int16_t bx = (int16_t)(ballX >> SUB_BITS);
int16_t by = (int16_t)(ballY >> SUB_BITS);
// The player's paddle.
if (ballDX < 0 && bx <= PLAYER_X + PADDLE_W && bx + BALL >= PLAYER_X) {
if (by + BALL > playerY && by < playerY + PADDLE_H) {
ballX = (int16_t)((PLAYER_X + PADDLE_W) * SUB);
ballDX = (int16_t)(-ballDX);
ballDY = bounceAngle(by, playerY);
}
}
// The board's.
if (ballDX > 0 && bx + BALL >= COMPUTER_X && bx <= COMPUTER_X + PADDLE_W) {
if (by + BALL > computerY && by < computerY + PADDLE_H) {
ballX = (int16_t)((COMPUTER_X - BALL) * SUB);
ballDX = (int16_t)(-ballDX);
ballDY = bounceAngle(by, computerY);
}
}
// Off the end: a point, and back to waiting.
if (bx < LEFT) {
computerScore++;
waiting = true;
} else if (bx + BALL > RIGHT) {
playerScore++;
waiting = true;
}
if (waiting) {
Serial.println(bx < LEFT ? "point to the board" : "point to you");
serve(0);
ballDX = 0;
ballDY = 0;
drawScore();
}
paint();
delay(24);
}
Parts list
4 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
7 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- ESP32-C3-DevKitM-1 pin 3V3; OLED display, 128x64 pin VCC; Thumb stick pin VCC
- ESP32-C3-DevKitM-1 pin 3; Thumb stick pin SW
- Ground: ESP32-C3-DevKitM-1 pin GND; OLED display, 128x64 pin GND; Thumb stick pin GND
- ESP32-C3-DevKitM-1 pin 0; Thumb stick pin VRY
- ESP32-C3-DevKitM-1 pin 1; Thumb stick pin VRX
- ESP32-C3-DevKitM-1 pin 4; OLED display, 128x64 pin SDA
- ESP32-C3-DevKitM-1 pin 5; OLED display, 128x64 pin SCL
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.