Built-in project · Arduino Uno R3
D-pad pong on an Arduino Uno
A TFT, a console pad and a piezo on an Uno: paddle game, arrow keys or clicks, A to serve. 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.
Click to open it in the editor
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
// D-pad pong: the paddle game on an ILI9341, played with a console pad.
//
// Panel: CS -> pin 10, DC -> pin 9, SCK -> pin 13, MOSI -> pin 11
// RST -> 5 V, LED -> 5 V, VCC -> 5 V, GND -> GND
// Pad: UP -> 7, DOWN -> 8, LEFT -> 5, RIGHT -> 4, A -> 3, B -> 2, GND -> GND
// Piezo: one leg -> pin 6, the other -> GND
//
// Hold LEFT and RIGHT to move the paddle. A serves, and starts a new game when
// you have run out of lives. On screen the pad's buttons are clickable and the
// arrow keys, Z and X work when the pointer is on the canvas.
//
// # A pad is a direction, not a position
//
// This is the whole difference from the thumb-stick version. A pot is an
// absolute position, so the paddle can be put wherever the stick is. Six
// switches say only left, right or neither, so the paddle has a *speed*: held,
// it moves five pixels a frame, and let go it stops. Five pixels at thirty
// frames a second crosses the playfield in about a second, which is fast enough
// to catch a ball and slow enough to aim.
//
// # 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 bit of every pixel is a pin change made by hand, about fifty
// thousand pixels a second. The whole panel is 76 800 of them, a second and a
// half to clear once. So the game lives in a window in the middle and the rest
// stays the black the controller powers up as. A frame only ever repaints the
// ball and the strip of paddle that moved, which is under two thousand pixels
// and is what makes it playable.
#include "mokxi_ili9341.h"
#include "mokxi_gamepad.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);
// up down left right A B
Gamepad pad(7, 8, 5, 4, 3, 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;
/** Pixels the paddle moves in a frame while a direction is held. */
const int16_t PADDLE_SPEED = 5;
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);
}
// Kept out of line on purpose. mokxiTone is an inline loop, and three copies of
// it inside a loop() this size push an AVR conditional branch past the 64-word
// reach of R_AVR_7_PCREL and the link fails. One call does the same job.
static void beep(uint16_t hz, uint16_t ms) __attribute__((noinline));
static void beep(uint16_t hz, uint16_t ms) {
mokxiTone(PIEZO_PIN, hz, ms);
}
// 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, "D-PAD", ILI9341_WHITE, ILI9341_BLACK, 3);
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) {
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: D-pad pong");
pad.begin();
mokxiNoTone(PIEZO_PIN);
tft.begin(0);
drawFrame();
startGame();
}
void loop() {
pad.update();
if (!playing) {
if (pad.pressed(PAD_A)) {
tft.fillRect(FX + 8, FY + FH / 2 - 16, FW - 16, 32, BG);
startGame();
}
delay(FRAME_MS);
return;
}
// A direction, not a position: held left or right moves the paddle at a
// fixed speed and nothing moves it back.
int8_t dir = pad.dx();
if (dir != 0) {
px = clampInt((int16_t)(px + (int16_t)dir * PADDLE_SPEED), 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.
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 = 1;
}
if (bdx > 4) bdx = 4;
if (bdx < -4) bdx = -4;
score++;
beep(NOTE_C6, 30);
bounced = false;
}
}
tft.fillRect(bx, by, BALL, BALL, BG);
if (ny > FY + FH - BALL) {
// Missed.
beep(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 A");
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) {
beep(NOTE_G5, 20);
}
delay(FRAME_MS);
}
Parts list
5 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
13 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 8; Game console buttons pin DOWN
- Arduino Uno R3 pin 7; Game console buttons pin UP
- Arduino Uno R3 pin 6; Piezo speaker pin 1
- Arduino Uno R3 pin 5; Game console buttons pin LEFT
- Arduino Uno R3 pin 4; Game console buttons pin RIGHT
- Arduino Uno R3 pin 3; Game console buttons pin A
- Arduino Uno R3 pin 2; Game console buttons pin B
- Arduino Uno R3 pin 5V; TFT display, 240x320 pin VCC; TFT display, 240x320 pin RST; TFT display, 240x320 pin LED
- Ground: Arduino Uno R3 pin GND; TFT display, 240x320 pin GND; Game console buttons pin GND; Piezo speaker pin 2
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.