Community project

Kids Challenge Game

ESP32
Photo of Kids Challenge Game

Pakorn Koomtritong

Published September 14, 2026

Kids Challenge Game is an interactive trash toss game built around an ESP32 microcontroller and a 3.2-inch touchscreen display. Players aim and flick virtual paper balls into a bin, with touch calibration ensuring accurate aiming on the screen.

This guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions. After connecting USB power and calibrating the touch panel with four setup dots, the game is ready to play. The included firmware handles all game logic, physics simulation, and score tracking.

Wiring diagram

Assemble it in 4 steps

1. Leave the screen board assembled

Use the ESP32 L0496 as it came. Its colour screen and touch layer are already connected inside the board, so do not add jumper wires to the display pins.

  • Put the board on a dry, non-metal table where you can comfortably tap the screen.
  • Do not connect anything to the built-in display or touch pins; extra wires can stop the screen or touch layer from working.

2. Connect USB power

Plug a USB cable from a computer or USB power source into the ESP32 board. The cable powers the board and is all the hardware this game needs.

  • Use a data-capable USB cable so Schematik can send the game to the board.
  • Keep the board away from drinks and metal objects while it is powered.

3. Touch the four setup dots

After the game starts, touch each yellow dot near the four corners and hold your finger still until it moves to the next dot. This teaches the board exactly where your finger lands on this particular screen.

  • Use a fingertip or the soft end of a plastic stylus. Touch the middle of each dot for the best aim.
  • Do not press the screen with a pen, pencil, or sharp object because it can scratch the touch layer.

4. Play Trash Toss

Touch and drag from the paper ball near the kid toward the bin, then lift your finger to throw. Aim high enough for the paper to arc down into the bin. You get five paper balls each round.

  • If it lands short, drag farther toward the bin; if it sails past, aim a little lower.
  • The game recalibrates after every restart, so complete the four-dot setup before playing.

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <XPT2046_Touchscreen.h>


// Hoisted type definitions
struct ScreenPoint { int x; int y; };


// Forward declarations
bool panelPressed();
TS_Point stableRawPoint();
ScreenPoint screenPoint();
void target(int x, int y);
TS_Point captureTarget(int x, int y);
void calibrateTouch();
void drawBin();
void drawKid();
void drawBackground();
void drawPaper(int x, int y, uint16_t color);
void showMessage(const char *line1, const char *line2, uint16_t color);
void resetBall();
void startThrow();
void finishThrow(bool scored);
void updateFlight();

constexpr int TFT_SCK = 14;
constexpr int TFT_MOSI = 13;
constexpr int TFT_MISO = 12;
constexpr int TFT_CS = 15;
constexpr int TFT_DC = 2;
constexpr int TFT_BACKLIGHT = 21;
constexpr int TFT_RST = -1;

constexpr int TOUCH_SCK = 25;
constexpr int TOUCH_MOSI = 32;
constexpr int TOUCH_MISO = 39;
constexpr int TOUCH_CS = 33;
constexpr int TOUCH_IRQ = 36;

constexpr int SCREEN_W = 320;
constexpr int SCREEN_H = 240;
constexpr uint16_t SKY = 0x8DDF;
constexpr uint16_t GRASS = 0x5D87;
constexpr uint16_t DARK_GRASS = 0x3484;
constexpr uint16_t BIN_BLUE = 0x051F;
constexpr uint16_t PAPER = 0xFFFF;

SPIClass displaySPI(HSPI);
Adafruit_ILI9341 tft(&displaySPI, TFT_DC, TFT_CS, TFT_RST);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);


int rawLeft, rawRight, rawTop, rawBottom;
int score = 0;
int throwsLeft = 5;
int binX = 246;
int binY = 153;
bool aiming = false;
bool flying = false;
bool roundOver = false;
ScreenPoint aimPoint = {210, 115};
float ballX = 67, ballY = 173, vx = 0, vy = 0;
unsigned long lastFrame = 0;

bool panelPressed() {
  return digitalRead(TOUCH_IRQ) == LOW && touch.touched();
}

TS_Point stableRawPoint() {
  long x = 0, y = 0;
  const int samples = 8;
  for (int i = 0; i < samples; i++) {
    TS_Point p = touch.getPoint();
    x += p.x;
    y += p.y;
    delay(8);
  }
  return TS_Point(x / samples, y / samples, 1);
}

ScreenPoint screenPoint() {
  TS_Point p = touch.getPoint();
  ScreenPoint s;
  s.x = constrain((long)(p.x - rawLeft) * (SCREEN_W - 1) / (rawRight - rawLeft), 0, SCREEN_W - 1);
  s.y = constrain((long)(p.y - rawTop) * (SCREEN_H - 1) / (rawBottom - rawTop), 0, SCREEN_H - 1);
  return s;
}

void target(int x, int y) {
  tft.fillScreen(SKY);
  tft.setTextColor(ILI9341_BLACK);
  tft.setTextSize(2);
  tft.setCursor(24, 20);
  tft.print("Touch the yellow dot");
  tft.setCursor(56, 46);
  tft.print("and hold still!");
  tft.fillCircle(x, y, 15, ILI9341_YELLOW);
  tft.drawCircle(x, y, 20, ILI9341_BLACK);
  tft.fillCircle(x, y, 4, ILI9341_RED);
}

TS_Point captureTarget(int x, int y) {
  target(x, y);
  while (!panelPressed()) delay(10);
  delay(180);
  TS_Point p = stableRawPoint();
  while (panelPressed()) delay(10);
  delay(150);
  return p;
}

void calibrateTouch() {
  TS_Point topLeft = captureTarget(25, 25);
  TS_Point topRight = captureTarget(SCREEN_W - 26, 25);
  TS_Point bottomRight = captureTarget(SCREEN_W - 26, SCREEN_H - 26);
  TS_Point bottomLeft = captureTarget(25, SCREEN_H - 26);
  rawLeft = (topLeft.x + bottomLeft.x) / 2;
  rawRight = (topRight.x + bottomRight.x) / 2;
  rawTop = (topLeft.y + topRight.y) / 2;
  rawBottom = (bottomLeft.y + bottomRight.y) / 2;
}

void drawBin() {
  tft.fillRect(binX + 3, binY + 18, 52, 58, BIN_BLUE);
  tft.fillRect(binX, binY + 11, 58, 12, ILI9341_NAVY);
  tft.fillRoundRect(binX + 6, binY, 46, 17, 5, ILI9341_DARKGREY);
  tft.drawRoundRect(binX + 3, binY + 18, 52, 58, 3, ILI9341_BLACK);
  tft.setTextColor(ILI9341_WHITE);
  tft.setTextSize(1);
  tft.setCursor(binX + 13, binY + 42);
  tft.print("BIN");
}

void drawKid() {
  tft.fillCircle(49, 141, 17, 0xFD20);
  tft.fillCircle(43, 138, 2, ILI9341_BLACK);
  tft.fillCircle(55, 138, 2, ILI9341_BLACK);
  tft.drawFastHLine(45, 149, 9, ILI9341_BLACK);
  tft.fillRect(37, 158, 25, 39, ILI9341_RED);
  tft.drawLine(58, 165, 75, 174, 0xFD20);
  tft.drawLine(43, 197, 38, 216, ILI9341_NAVY);
  tft.drawLine(57, 197, 64, 216, ILI9341_NAVY);
}

void drawBackground() {
  tft.fillScreen(SKY);
  tft.fillRect(0, 198, SCREEN_W, 42, GRASS);
  tft.fillRect(0, 218, SCREEN_W, 22, DARK_GRASS);
  tft.fillCircle(284, 35, 21, ILI9341_YELLOW);
  drawKid();
  drawBin();
  tft.setTextColor(ILI9341_BLACK);
  tft.setTextSize(2);
  tft.setCursor(8, 7);
  tft.print("TRASH TOSS!");
  tft.setTextSize(1);
  tft.setCursor(10, 29);
  tft.print("Drag toward the bin, then let go!");
  tft.setTextColor(ILI9341_BLACK);
  tft.setTextSize(2);
  tft.setCursor(10, 48);
  tft.print("Score: ");
  tft.print(score);
  tft.setCursor(205, 48);
  tft.print("Balls: ");
  tft.print(throwsLeft);
}

void drawPaper(int x, int y, uint16_t color) {
  tft.fillCircle(x, y, 8, color);
  if (color == PAPER) tft.drawCircle(x, y, 8, ILI9341_BLACK);
}

void showMessage(const char *line1, const char *line2, uint16_t color) {
  tft.fillRoundRect(50, 75, 220, 48, 8, color);
  tft.drawRoundRect(50, 75, 220, 48, 8, ILI9341_BLACK);
  tft.setTextColor(ILI9341_BLACK);
  tft.setTextSize(2);
  tft.setCursor(70, 84);
  tft.print(line1);
  tft.setTextSize(1);
  tft.setCursor(70, 109);
  tft.print(line2);
}

void resetBall() {
  ballX = 75;
  ballY = 174;
  vx = 0;
  vy = 0;
  aiming = false;
  flying = false;
  drawBackground();
  drawPaper((int)ballX, (int)ballY, PAPER);
}

void startThrow() {
  float dx = aimPoint.x - ballX;
  float dy = aimPoint.y - ballY;
  if (dx < 25) dx = 25;
  vx = dx * 0.050f;
  vy = dy * 0.050f - 3.1f;
  flying = true;
  aiming = false;
  throwsLeft--;
}

void finishThrow(bool scored) {
  flying = false;
  if (scored) {
    score++;
    drawBackground();
    showMessage("SWISH!", "The bin says: yummy paper!", ILI9341_GREEN);
  } else {
    drawBackground();
    showMessage("PLOP!", "Oops! Try a different angle.", ILI9341_ORANGE);
  }
  delay(1050);
  if (throwsLeft <= 0) {
    drawBackground();
    if (score >= 3) showMessage("YOU ROCK!", "A very tidy champion! Tap to play again.", ILI9341_GREEN);
    else showMessage("ROUND OVER", "Tap anywhere for another go!", ILI9341_YELLOW);
    roundOver = true;
  } else {
    resetBall();
  }
}

void updateFlight() {
  int oldX = (int)ballX;
  int oldY = (int)ballY;
  ballX += vx;
  ballY += vy;
  vy += 0.18f;
  // Repaint only the old paper-ball spot, then redraw nearby static features if needed.
  tft.fillCircle(oldX, oldY, 10, oldY >= 198 ? GRASS : SKY);
  if (oldX > binX - 14 && oldX < binX + 70 && oldY > binY - 14 && oldY < binY + 88) drawBin();
  bool inBin = ballX > binX + 7 && ballX < binX + 53 && ballY > binY + 14 && ballY < binY + 27 && vy > 0;
  if (inBin) {
    finishThrow(true);
    return;
  }
  if (ballY > 220 || ballX > SCREEN_W + 10 || ballX < -10) {
    finishThrow(false);
    return;
  }
  drawPaper((int)ballX, (int)ballY, PAPER);
}

void setup() {
  pinMode(TFT_BACKLIGHT, OUTPUT);
  digitalWrite(TFT_BACKLIGHT, HIGH);
  pinMode(TOUCH_IRQ, INPUT);
  displaySPI.begin(TFT_SCK, TFT_MISO, TFT_MOSI, TFT_CS);
  tft.begin();
  tft.setRotation(1);
  SPI.begin(TOUCH_SCK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS);
  touch.begin();
  calibrateTouch();
  resetBall();
}

void loop() {
  if (roundOver) {
    if (panelPressed()) {
      while (panelPressed()) delay(10);
      score = 0;
      throwsLeft = 5;
      roundOver = false;
      resetBall();
    }
    return;
  }

  bool down = panelPressed();
  if (!flying) {
    if (down) {
      aimPoint = screenPoint();
      if (aimPoint.x > 80) aiming = true;
    } else if (aiming) {
      startThrow();
    }
  }

  if (millis() - lastFrame >= 50) {
    lastFrame = millis();
    if (flying) updateFlight();
    else if (aiming) {
      drawBackground();
      drawPaper((int)ballX, (int)ballY, PAPER);
      tft.drawLine((int)ballX, (int)ballY, aimPoint.x, aimPoint.y, ILI9341_MAGENTA);
      tft.fillCircle(aimPoint.x, aimPoint.y, 4, ILI9341_MAGENTA);
    }
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik