Built-in project · Arduino Uno R3
Pong console on an Arduino Uno
An Uno, a 240x320 TFT, a thumb stick and a piezo: a paddle game in the window an Uno can repaint. The whole build, an Arduino Uno and 3 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 · Arduino Uno R3
// Pong: a 2.4 inch ILI9341, a thumb stick and a piezo, on an Arduino Uno.
//
// Wiring, panel to board:
// CS -> pin 10 SCK -> pin 13
// DC -> pin 9 MOSI -> pin 11
// RST -> 5 V LED -> 5 V VCC -> 5 V, GND -> GND
// Thumb stick:
// VRX -> A0 VRY -> A1 SW -> pin 2, VCC -> 5 V, GND -> GND
// Piezo:
// one leg -> pin 6, the other -> GND
//
// Push the stick left and right to move the paddle. Press it to serve. Three
// lives; the score is how many times you sent the ball back.
//
// # Why the playfield is a window and not the screen
//
// There is no SPI peripheral on an ATmega328P in Mokxi (docs/uno.md section 3),
// so every one of the sixteen bits of a pixel is a pin change made by hand.
// Even writing the port directly (which mokxi_spi.h does, rather than going
// through digitalWrite), that is about nineteen cycles a bit, so this board
// manages some fifty thousand pixels a second. The whole 240x320 panel is
// 76 800 of them: a second and a half to clear it once, and nothing left over.
//
// So the game lives in a 160x200 window in the middle of the panel and the rest
// is left at the black the controller powers up as; painting it would cost
// more than the game does. Clearing the window costs about six hundred
// milliseconds at the start and nothing afterwards, because a frame only ever
// repaints the ball and the strip of paddle that moved. That is under two
// thousand pixels a frame, which is what makes it playable.
//
// The ESP32-C3 version of this example gets a 180x220 window at forty frames a
// second off the same driver, which is the difference between the two boards
// written down as an area and a frame rate.
#include "mokxi_ili9341.h"
#include "mokxi_joystick.h"
#include "mokxi_tone.h"
const uint8_t TFT_CS = 10;
const uint8_t TFT_DC = 9;
const uint8_t TFT_SCK = 13;
const uint8_t TFT_MOSI = 11;
const uint8_t PIEZO_PIN = 6;
SoftSpi spi(TFT_SCK, TFT_MOSI, TFT_CS);
Ili9341 tft(spi, TFT_DC);
Joystick stick(A0, A1, 2);
// The playfield: a window in the middle of the panel, in panel coordinates.
const int16_t FX = 40;
const int16_t FY = 70;
const int16_t FW = 160;
const int16_t FH = 200;
const uint16_t BG = 0x0841; // a very dark blue, so "cleared" is visible
const uint16_t EDGE = ILI9341_CYAN;
const uint16_t BALL_COLOUR = ILI9341_YELLOW;
const uint16_t PADDLE_COLOUR = ILI9341_GREEN;
const int16_t BALL = 8;
const int16_t PADDLE_W = 36;
const int16_t PADDLE_H = 8;
const int16_t PADDLE_Y = FY + FH - 16;
const int FRAME_MS = 33; // 30 frames a second
const uint8_t LIVES = 3;
int16_t bx, by, bdx, bdy;
int16_t px, oldPx;
uint8_t lives;
int16_t score, 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);
}
// The furniture: a border, the labels, and a cleared playfield. Drawn once.
static void drawFrame() {
tft.fillRect(FX, FY, FW, FH, BG);
tft.rect(FX - 2, FY - 2, FW + 4, FH + 4, EDGE, 2);
tft.text(FX, FY - 26, "PONG", ILI9341_WHITE, ILI9341_BLACK, 3);
// One row, not two: the panel is 320 tall and the module's own silkscreen
// takes the last few rows 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 + 24;
// Alternate sides rather than pull in a random number generator: there is
// 32 KB of flash and this example is already most of an eighth of it.
bdx = (int16_t)-bdx;
if (bdx == 0) {
bdx = 3;
}
bdy = 3;
tft.fillRect(bx, by, BALL, BALL, BALL_COLOUR);
}
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, PADDLE_COLOUR);
serve();
}
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Uno: pong on an ILI9341");
stick.begin();
mokxiNoTone(PIEZO_PIN);
tft.begin(0);
drawFrame();
startGame();
}
void loop() {
if (!playing) {
if (stick.clicked()) {
tft.fillRect(FX + 8, FY + FH / 2 - 16, FW - 16, 32, BG);
startGame();
}
delay(FRAME_MS);
return;
}
// The paddle follows the stick outright rather than drifting with it: the
// pot is an absolute position and so is the paddle. A shift rather than
// map(), because 1023 is near enough 1024 and a 32-bit divide is 150 bytes
// of flash this sketch would rather spend on the game.
px = (int16_t)(FX + (int16_t)(((int32_t)stick.x() * (FW - PADDLE_W)) >> 10));
px = clampInt(px, FX, (int16_t)(FX + FW - PADDLE_W));
if (px != oldPx) {
// Only the strip that stopped being paddle is erased; the rest is redrawn
// over itself, which is far less than repainting the whole thing. A jump
// wider than the paddle (which a stick slammed to one end is) leaves no
// overlap at all, so the old rectangle goes as a whole.
int16_t gap = (int16_t)(px > oldPx ? px - oldPx : oldPx - px);
if (gap >= PADDLE_W) {
tft.fillRect(oldPx, PADDLE_Y, PADDLE_W, PADDLE_H, BG);
} else if (px > oldPx) {
tft.fillRect(oldPx, PADDLE_Y, gap, PADDLE_H, BG);
} else {
tft.fillRect((int16_t)(px + PADDLE_W), PADDLE_Y, gap, PADDLE_H, BG);
}
tft.fillRect(px, PADDLE_Y, PADDLE_W, PADDLE_H, PADDLE_COLOUR);
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;
}
// The paddle: caught if the ball's foot reaches its top while they overlap.
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 == 0) {
bdx = (bdx >= 0) ? 1 : -1;
}
if (bdx > 4) bdx = 4;
if (bdx < -4) bdx = -4;
score++;
mokxiTone(PIEZO_PIN, NOTE_C6, 30);
bounced = false;
}
}
tft.fillRect(bx, by, BALL, BALL, BG);
if (ny > FY + FH - BALL) {
// Missed.
mokxiTone(PIEZO_PIN, NOTE_C4, 160);
lives--;
drawLives();
if (lives == 0u) {
playing = false;
tft.text(FX + 10, FY + FH / 2 - 10, "GAME OVER", ILI9341_RED, BG, 2);
Serial.println("game over, press the stick to play again");
delay(FRAME_MS);
return;
}
serve();
delay(300);
return;
}
bx = nx;
by = ny;
tft.fillRect(bx, by, BALL, BALL, BALL_COLOUR);
if (score != shownScore) {
drawScore();
}
if (bounced) {
mokxiTone(PIEZO_PIN, NOTE_G5, 20);
}
delay(FRAME_MS);
}
Parts list
5 parts, plus the jumper wires. Every one is in the editor's parts bin.
- 1 × Arduino Uno R3
- 1 × Full-size breadboard
- 1 × TFT display, 240x320
- 1 × Thumb stick
- 1 × Piezo speaker
How it is wired
10 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.
- Arduino Uno R3 pin 13; TFT display, 240x320 pin SCK
- Arduino Uno R3 pin 11; TFT display, 240x320 pin MOSI
- Arduino Uno R3 pin 10; TFT display, 240x320 pin CS
- Arduino Uno R3 pin 9; TFT display, 240x320 pin DC
- Arduino Uno R3 pin 6; Piezo speaker pin 1
- Arduino Uno R3 pin 2; Thumb stick pin SW
- Arduino Uno R3 pin 5V; TFT display, 240x320 pin VCC; TFT display, 240x320 pin RST; TFT display, 240x320 pin LED; Thumb stick pin VCC
- Ground: Arduino Uno R3 pin GND; TFT display, 240x320 pin GND; Thumb stick pin GND; Piezo speaker pin 2
- Arduino Uno R3 pin A0; Thumb stick pin VRX
- Arduino Uno R3 pin A1; Thumb stick pin VRY
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.