Built-in project · ESP32-C3-DevKitM-1
Pong, two buttons, on an ESP32-C3
The same TFT paddle game on an ESP32-C3, full screen at forty frames a second, played with two buttons. The whole build, an ESP32-C3 and 4 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.
Hold the button
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
sketch.ino · ESP32-C3-DevKitM-1
// Pong: a 2.4 inch ILI9341 and two buttons, on an ESP32-C3.
//
// Wiring, panel to board:
// CS -> GPIO 6 SCK -> GPIO 4
// DC -> GPIO 7 MOSI -> GPIO 5
// RST -> 3V3 LED -> 3V3 VCC -> 3V3, GND -> GND
// Buttons, each from its pin to ground (the pull-up is inside the chip):
// left -> GPIO 2 right -> GPIO 3
// Piezo:
// one leg -> GPIO 8, the other -> GND
//
// Hold a button to slide the paddle. Press either one to serve again after a
// game. Three lives; the score is how many times you sent the ball back.
//
// # Two buttons, not a stick
//
// This sketch was written when the Mokxi ESP32-C3 had no ADC and a
// potentiometer could not be read on this board at all. ADC1 is modelled now
// (docs/esp32c3.md section 3.5) and `stick` and `oledpong` here both read a
// thumb stick, so the buttons are a choice rather than a limit: a held button
// is a rate and a stick is a position, and a full-screen paddle game plays
// perfectly well on a rate. The Uno version of this example is the one with
// the stick on it.
//
// # Nothing is painted that is not used
//
// Every bit of every pixel is a pin change made by hand (there is no SPI
// peripheral either), and at 160 MHz with the direct port writes in
// mokxi_spi.h this board makes about a million of them a second. A full 240x320
// fill is 2.4 million pin changes, and while the chip would do it in eighty
// milliseconds the simulator has to work through every one of them, so it is
// the one thing in this sketch worth not doing.
//
// The controller's memory powers up black, so the sketch paints its playfield
// and its labels and leaves the rest of the panel alone. The playfield is
// 180x220, larger and twice as smooth as the 160x200 the Uno version can
// afford, which is the difference between the two boards in one number.
#include "mokxi_ili9341.h"
#include "mokxi_tone.h"
const uint8_t TFT_SCK = 4;
const uint8_t TFT_MOSI = 5;
const uint8_t TFT_CS = 6;
const uint8_t TFT_DC = 7;
const uint8_t PIEZO_PIN = 8;
const uint8_t LEFT_PIN = 2;
const uint8_t RIGHT_PIN = 3;
SoftSpi spi(TFT_SCK, TFT_MOSI, TFT_CS);
Ili9341 tft(spi, TFT_DC);
// The playfield is most of the panel, with a band above and below it.
const int16_t FX = 30;
const int16_t FY = 60;
const int16_t FW = 180;
const int16_t FH = 220;
const uint16_t BG = 0x0841; // a very dark blue, so "cleared" is visible
const uint16_t EDGE = ILI9341_CYAN;
const int16_t BALL = 10;
const int16_t PADDLE_W = 44;
const int16_t PADDLE_H = 10;
const int16_t PADDLE_Y = FY + FH - 18;
const int16_t PADDLE_STEP = 6;
const int FRAME_MS = 25; // forty frames a second
const uint8_t LIVES = 3;
int16_t bx;
int16_t by;
int16_t bdx;
int16_t bdy;
int16_t px;
int16_t oldPx;
uint8_t lives;
int16_t score;
int16_t shownScore;
bool playing;
bool started;
static int16_t clampInt(int16_t v, int16_t lo, int16_t hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
static void drawFrame() {
tft.text(FX - 12, 22, "MOKXI PONG", ILI9341_WHITE, ILI9341_BLACK, 3);
tft.fillRect(FX, FY, FW, FH, BG);
tft.rect(FX - 2, FY - 2, FW + 4, FH + 4, EDGE, 2);
// One row, not two: the panel is 320 tall and a second line of 14-pixel
// capitals would fall off the bottom of it.
tft.text(FX, FY + FH + 10, "SCORE", ILI9341_WHITE, ILI9341_BLACK, 2);
tft.text(FX + 110, FY + FH + 10, "LIVES", ILI9341_WHITE, ILI9341_BLACK, 2);
}
static void drawScore() {
tft.number(FX + 64, FY + FH + 10, score, ILI9341_YELLOW, ILI9341_BLACK, 2, 3);
shownScore = score;
}
static void drawLives() {
tft.number(FX + 174, FY + FH + 10, lives, ILI9341_ORANGE, ILI9341_BLACK, 2, 1);
}
static void serve() {
bx = FX + FW / 2 - BALL / 2;
by = FY + 30;
bdx = (random(2) == 0) ? -4 : 4;
bdy = 4;
tft.fillRect(bx, by, BALL, BALL, ILI9341_YELLOW);
}
static void startGame() {
score = 0;
shownScore = -1;
lives = LIVES;
playing = true;
if (started) {
// Only a restart needs the playfield wiped; the first one was just painted.
tft.fillRect(FX, FY, FW, FH, BG);
}
started = true;
drawScore();
drawLives();
px = FX + (FW - PADDLE_W) / 2;
oldPx = px;
tft.fillRect(px, PADDLE_Y, PADDLE_W, PADDLE_H, ILI9341_GREEN);
serve();
}
void setup() {
Serial.begin(115200);
Serial.println("Mokxi ESP32-C3: pong on an ILI9341, two buttons for the paddle");
pinMode(LEFT_PIN, INPUT_PULLUP);
pinMode(RIGHT_PIN, INPUT_PULLUP);
mokxiNoTone(PIEZO_PIN);
randomSeed(micros());
tft.begin(0);
drawFrame();
startGame();
}
void loop() {
bool left = digitalRead(LEFT_PIN) == LOW;
bool right = digitalRead(RIGHT_PIN) == LOW;
if (!playing) {
if (left || right) {
startGame();
}
delay(FRAME_MS);
return;
}
if (left != right) {
px = clampInt((int16_t)(px + (left ? -PADDLE_STEP : PADDLE_STEP)), FX,
(int16_t)(FX + FW - PADDLE_W));
if (px != oldPx) {
// Erase only the strip that stopped being paddle, then redraw it. A step
// is never wider than the paddle, so there is always an overlap to keep.
if (px > oldPx) {
tft.fillRect(oldPx, PADDLE_Y, (int16_t)(px - oldPx), PADDLE_H, BG);
} else {
tft.fillRect((int16_t)(px + PADDLE_W), PADDLE_Y, (int16_t)(oldPx - px), PADDLE_H, BG);
}
tft.fillRect(px, PADDLE_Y, PADDLE_W, PADDLE_H, ILI9341_GREEN);
oldPx = px;
}
}
int16_t nx = (int16_t)(bx + bdx);
int16_t ny = (int16_t)(by + bdy);
bool bounced = false;
if (nx < FX) {
nx = FX;
bdx = (int16_t)-bdx;
bounced = true;
} else if (nx > FX + FW - BALL) {
nx = (int16_t)(FX + FW - BALL);
bdx = (int16_t)-bdx;
bounced = true;
}
if (ny < FY) {
ny = FY;
bdy = (int16_t)-bdy;
bounced = true;
}
if (bdy > 0 && ny + BALL >= PADDLE_Y && by + BALL < PADDLE_Y + PADDLE_H) {
if (nx + BALL > px && nx < px + PADDLE_W) {
ny = (int16_t)(PADDLE_Y - BALL);
bdy = (int16_t)-bdy;
// Where it hit decides which way it leaves, which is the whole game.
int16_t offset = (int16_t)((nx + BALL / 2) - (px + PADDLE_W / 2));
bdx = (int16_t)(offset / 6);
if (bdx > 5) bdx = 5;
if (bdx < -5) bdx = -5;
if (bdx == 0) bdx = 1;
score++;
mokxiTone(PIEZO_PIN, NOTE_C6, 25);
bounced = false;
}
}
tft.fillRect(bx, by, BALL, BALL, BG);
if (ny > FY + FH - BALL) {
mokxiTone(PIEZO_PIN, NOTE_C4, 150);
lives--;
drawLives();
if (lives == 0u) {
playing = false;
tft.text(FX + 6, FY + FH / 2 - 12, "GAME OVER", ILI9341_RED, BG, 3);
Serial.print("game over, score ");
Serial.println(score);
delay(FRAME_MS);
return;
}
serve();
delay(300);
return;
}
bx = nx;
by = ny;
tft.fillRect(bx, by, BALL, BALL, ILI9341_YELLOW);
if (score != shownScore) {
drawScore();
}
if (bounced) {
mokxiTone(PIEZO_PIN, NOTE_G5, 18);
}
delay(FRAME_MS);
}
Parts list
6 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
9 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; TFT display, 240x320 pin VCC; TFT display, 240x320 pin RST; TFT display, 240x320 pin LED
- ESP32-C3-DevKitM-1 pin 2; Pushbutton (1) pin 1a
- ESP32-C3-DevKitM-1 pin 3; Pushbutton (2) pin 1a
- Ground: ESP32-C3-DevKitM-1 pin GND; TFT display, 240x320 pin GND; Pushbutton (1) pin 2a; Pushbutton (2) pin 2a; Piezo speaker pin 2
- ESP32-C3-DevKitM-1 pin 4; TFT display, 240x320 pin SCK
- ESP32-C3-DevKitM-1 pin 5; TFT display, 240x320 pin MOSI
- ESP32-C3-DevKitM-1 pin 6; TFT display, 240x320 pin CS
- ESP32-C3-DevKitM-1 pin 7; TFT display, 240x320 pin DC
- ESP32-C3-DevKitM-1 pin 8; Piezo speaker pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.