Community project

Potentiometer Ping Pong

Arduino
Photo of Potentiometer Ping Pong
Generated with AI

narayanmamta49

Last updated August 11, 2026

Potentiometer Ping Pong is a two-player arcade game built on an Arduino Uno with an I2C LCD display and two potentiometers as paddle controllers. Players rotate their knobs to move paddles up and down, competing to keep the ball in play and score points against their opponent.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to get the game running. The included firmware handles menu navigation, single-player and two-player modes, ball physics, and a chaos mode with multiple balls. Once wired and programmed, the game is ready to play immediately from the LCD interface.

Wiring diagram

Wiring diagram for Potentiometer Ping Pong

Gather all the parts

QtyComponent
1

LCD 16x2 I2C

16x2, I2C (0x27)

16x2 character LCD display with I2C backpack

1

10kΩ Potentiometer

10 kΩ

A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V.

1

10kΩ Potentiometer

10 kΩ

A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V.

Assemble it in 5 steps

1. Power off before wiring

Disconnect the Uno USB cable while making connections. Use a breadboard so the LCD and both potentiometers share the Uno 5V and GND rails.

  • A standard 10 kΩ potentiometer has two outer terminals and one centre wiper terminal.
  • Do not connect or move wires while the Uno is powered.

2. Connect the I²C LCD

Wire LCD VCC to Uno 5V and LCD GND to Uno GND. Wire LCD SDA to Uno A4 and LCD SCL to Uno A5.

  • On an Uno, A4 is the I²C data pin and A5 is the I²C clock pin.
  • Most common I²C backpacks use address 0x27, which this project uses.
  • Keep SDA and SCL separate; reversing them prevents the display from communicating.

3. Wire Player 1/menu potentiometer

Connect one outer P1 terminal to Uno 5V, the other outer terminal to Uno GND, and its centre wiper terminal to A0.

  • If turning clockwise moves the paddle/menu selection the opposite way from your preference, swap only the two outer terminals.
  • P1 is also the menu selector.
  • The centre wiper must go to A0, not directly to 5V or GND.

4. Wire Player 2 potentiometer

Connect one outer P2 terminal to Uno 5V, the other outer terminal to Uno GND, and its centre wiper terminal to A1.

  • P2 is used only in Two Player mode; in Singleplayer, the right paddle is controlled by the built-in AI.
  • Both potentiometers and the LCD must share the Uno GND connection.

5. Power and play

Reconnect USB power. Turn P1 to the top-row or bottom-row option and hold it steady for 15 seconds to choose Normal/Chaos, then Singleplayer/Two Player. During a match, each knob chooses its paddle row. In Normal mode, the first player to 7 wins.

  • The LCD uses | for paddles and + for balls.
  • Chaos mode adds a ball every 10 seconds and does not keep score.
  • If the LCD lights but has no readable text, first adjust the small contrast screw on its I²C backpack.

Review all connections

1. Connections between "lcd_1" and "Arduino"

Functionlcd_1Arduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

2. Connections between "p1_pot" and "Arduino"

Functionp1_potArduino
powerEnd1 (VCC side)5V
analogWiper (middle)GPIO 14
groundEnd2 (GND side)GND

3. Connections between "p2_pot" and "Arduino"

Functionp2_potArduino
powerEnd1 (VCC side)5V
analogWiper (middle)GPIO 15
groundEnd2 (GND side)GND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>


// Hoisted type definitions
enum Screen { MODE_MENU, PLAYER_MENU, PLAYING, WIN_SCREEN };

struct Ball {
  int8_t x;
  int8_t y;
  int8_t dx;
  int8_t dy;
};


// Forward declarations
byte knobRow(byte pin);
void printRow(byte row, const char *text);
void drawMenu();
void resetBall(byte index);
void startGame();
void updateMenu();
void showWinner();
void scoreFor(byte player);
void moveBall(byte i);
void drawGame();
void updateGame();

const byte P1_POT_PIN = A0;
const byte P2_POT_PIN = A1;
const unsigned long MENU_HOLD_MS = 15000UL;
const unsigned long FRAME_MS = 180UL;
const unsigned long CHAOS_ADD_MS = 10000UL;
const byte WIN_SCORE = 7;
const byte MAX_CHAOS_BALLS = 20;

LiquidCrystal_I2C lcd(0x27, 16, 2);


Screen screen = MODE_MENU;
bool chaosMode = false;
bool singlePlayer = false;
byte menuChoice = 0;
byte heldChoice = 255;
unsigned long holdStarted = 0;
unsigned long lastFrame = 0;
unsigned long lastChaosBall = 0;



Ball balls[MAX_CHAOS_BALLS];
byte ballCount = 0;
byte p1Row = 0;
byte p2Row = 0;
byte p1Score = 0;
byte p2Score = 0;
byte winner = 0;

byte knobRow(byte pin) {
  return analogRead(pin) >= 512 ? 1 : 0;
}

void printRow(byte row, const char *text) {
  lcd.setCursor(0, row);
  lcd.print(text);
}

void drawMenu() {
  char line[17];
  lcd.clear();
  if (screen == MODE_MENU) {
    snprintf(line, sizeof(line), "%c Normal", menuChoice == 0 ? '>' : ' ');
    printRow(0, line);
    snprintf(line, sizeof(line), "%c Chaos", menuChoice == 1 ? '>' : ' ');
    printRow(1, line);
  } else {
    snprintf(line, sizeof(line), "%c Singleplayer", menuChoice == 0 ? '>' : ' ');
    printRow(0, line);
    snprintf(line, sizeof(line), "%c Two Player", menuChoice == 1 ? '>' : ' ');
    printRow(1, line);
  }
}

void resetBall(byte index) {
  balls[index].x = 7;
  balls[index].y = index & 1;
  balls[index].dx = (index & 1) ? 1 : -1;
  balls[index].dy = (index % 3 == 0) ? 1 : -1;
}

void startGame() {
  p1Score = 0;
  p2Score = 0;
  p1Row = 0;
  p2Row = 0;
  ballCount = 1;
  resetBall(0);
  lastChaosBall = millis();
  lastFrame = millis();
  screen = PLAYING;
  lcd.clear();
}

void updateMenu() {
  byte choice = knobRow(P1_POT_PIN);
  if (choice != menuChoice) {
    menuChoice = choice;
    heldChoice = 255;
    drawMenu();
  }
  if (heldChoice != menuChoice) {
    heldChoice = menuChoice;
    holdStarted = millis();
  }
  unsigned long elapsed = millis() - holdStarted;
  lcd.setCursor(8, 1);
  if (elapsed < MENU_HOLD_MS) {
    lcd.print("Hold ");
    lcd.print((MENU_HOLD_MS - elapsed + 999UL) / 1000UL);
    lcd.print("s ");
  }
  if (elapsed >= MENU_HOLD_MS) {
    if (screen == MODE_MENU) {
      chaosMode = (menuChoice == 1);
      screen = PLAYER_MENU;
      heldChoice = 255;
      drawMenu();
    } else {
      singlePlayer = (menuChoice == 0);
      startGame();
    }
  }
}

void showWinner() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Nice one P");
  lcd.print(winner);
  lcd.setCursor(0, 1);
  lcd.print("Turn knob restart");
}

void scoreFor(byte player) {
  if (chaosMode) return;
  if (player == 1) p1Score++; else p2Score++;
  if (p1Score >= WIN_SCORE || p2Score >= WIN_SCORE) {
    winner = (p1Score >= WIN_SCORE) ? 1 : 2;
    screen = WIN_SCREEN;
    showWinner();
  }
}

void moveBall(byte i) {
  Ball &b = balls[i];
  int8_t nextX = b.x + b.dx;
  int8_t nextY = b.y + b.dy;
  if (nextY < 0 || nextY > 1) {
    b.dy = -b.dy;
    nextY = b.y + b.dy;
  }
  if (nextX == 0 && b.dx < 0) {
    if (nextY == p1Row) {
      b.dx = 1;
      nextX = 1;
    } else {
      scoreFor(2);
      resetBall(i);
      return;
    }
  }
  if (nextX == 15 && b.dx > 0) {
    if (nextY == p2Row) {
      b.dx = -1;
      nextX = 14;
    } else {
      scoreFor(1);
      resetBall(i);
      return;
    }
  }
  b.x = nextX;
  b.y = nextY;
}

void drawGame() {
  char rows[2][17] = {"                ", "                "};
  rows[p1Row][0] = '|';
  rows[p2Row][15] = '|';
  if (!chaosMode) {
    rows[0][7] = '0' + p1Score;
    rows[0][8] = ':';
    rows[0][9] = '0' + p2Score;
  } else {
    rows[0][6] = 'C';
    rows[0][7] = ':';
    rows[0][8] = '0' + (ballCount / 10);
    rows[0][9] = '0' + (ballCount % 10);
  }
  for (byte i = 0; i < ballCount; i++) {
    if (balls[i].x > 0 && balls[i].x < 15) rows[balls[i].y][balls[i].x] = '+';
  }
  lcd.setCursor(0, 0);
  lcd.print(rows[0]);
  lcd.setCursor(0, 1);
  lcd.print(rows[1]);
}

void updateGame() {
  p1Row = knobRow(P1_POT_PIN);
  if (singlePlayer) {
    byte target = p2Row;
    for (byte i = 0; i < ballCount; i++) {
      if (balls[i].dx > 0) {
        target = balls[i].y;
        break;
      }
    }
    p2Row = target;
  } else {
    p2Row = knobRow(P2_POT_PIN);
  }
  if (chaosMode && millis() - lastChaosBall >= CHAOS_ADD_MS) {
    lastChaosBall += CHAOS_ADD_MS;
    if (ballCount < MAX_CHAOS_BALLS) {
      resetBall(ballCount);
      ballCount++;
    }
  }
  if (millis() - lastFrame >= FRAME_MS) {
    lastFrame = millis();
    for (byte i = 0; i < ballCount; i++) {
      moveBall(i);
      if (screen != PLAYING) return;
    }
    drawGame();
  }
}

void setup() {
  lcd.init();
  lcd.backlight();
  drawMenu();
}

void loop() {
  if (screen == MODE_MENU || screen == PLAYER_MENU) {
    updateMenu();
  } else if (screen == PLAYING) {
    updateGame();
  } else if (screen == WIN_SCREEN && analogRead(P1_POT_PIN) < 450) {
    screen = MODE_MENU;
    heldChoice = 255;
    drawMenu();
  }
}

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