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.

Pong, two buttons · ESP32-C3-DevKitM-1live0.000 s 0.00x
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.