Evan Rust
Published © GPL3+

Handheld Two-Player Pong with NeoPixels

Play Pong with a friend without the need for a screen.

IntermediateFull instructions provided6 hours1,429
Handheld Two-Player Pong with NeoPixels

Things used in this project

Hardware components

Microchip ATmega8A
×1
WS2812b
×48
Capacitor 1000 µF
Capacitor 1000 µF
×1
Momentary Pushbutton Tall
×4
5v Regulator
×1
16MHz Crystal Oscillator
×1

Software apps and online services

Arduino IDE
Arduino IDE
VS Code
Microsoft VS Code
Microchip Studio
Microchip Studio

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Schematic

PCB

Code

Handheld Pong

C/C++
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>
#include "offset_table.h"

#ifdef __AVR__
 #include <avr/power.h>
#endif

#define PIXEL_NUM 48
#define WIDTH 8
#define HEIGHT 6

#define LED_PIN 4
#define WRITE_ORDER_DATA false

// 40 ms between frames
#define FRAME_DELAY 40
#define MOVE_DELAY 20

Adafruit_NeoPixel pixels(PIXEL_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);

// Paddle 1 left, paddle 1 right, paddle 2 left, paddle 2 right
const uint8_t buttonPins[4] = {3, 1, 2, 0};

uint32_t lastFrameUpdate = 0, lastMoveUpdate = 0;

// Paddle 1, paddle 2, ball
const uint32_t colors[3] = {0x000010D0, 0x000D010, 0x00E5A500};

// Paddle 1 x/y, paddle 2 x/y, ball x/y
uint8_t piecePositions[6] = {WIDTH/2 - 1, 0, WIDTH/2 - 1, HEIGHT-1, WIDTH / 3, HEIGHT/3};
int8_t ballVector[2] = {1, 1};

uint8_t p1Wins = 0, p2Wins = 0;

void setup()
{
    pixels.begin();
    pixels.clear();
    pixels.setBrightness(50);
    if(WRITE_ORDER_DATA)
    {
        for(int i=0; i<PIXEL_NUM; i++)
        {
            EEPROM.write(i, offsets[i]);
            pixels.setPixelColor(EEPROM.read(i), 0x002050FF);
            pixels.show();
            delay(400);
        }
        // Program is done, ready to flash the game
        while(1);
    }
    for(int i=0; i<4; i++)
    {
        pinMode(buttonPins[i], INPUT_PULLUP);
    }
    lastFrameUpdate = millis();
    lastMoveUpdate = millis();
}

void loop()
{
    if(millis() - FRAME_DELAY >= lastFrameUpdate)
    {
        drawFrame();
        lastFrameUpdate = millis();
    }
    // Check button states
    if(millis() - MOVE_DELAY >= lastMoveUpdate)
    {
        if(digitalRead(buttonPins[0]))
        {
            if(piecePositions[0]>0) piecePositions[0]--;
        }
        else if(digitalRead(buttonPins[1]))
        {
            if(piecePositions[0]<WIDTH-2) piecePositions[0]++;
        }
        if(digitalRead(buttonPins[2]))
        {
            if(piecePositions[2]>0) piecePositions[2]--;
        }
        else if(digitalRead(buttonPins[3]))
        {
            if(piecePositions[2]<WIDTH-2) piecePositions[2]++;
        }
        lastMoveUpdate = millis();
    }
    if(checkCollisions()) drawScores();
}

void drawFrame()
{
    pixels.clear();
    // Draw paddles
    pixels.setPixelColor(EEPROM.read(XYtoLED(piecePositions[0], piecePositions[1])), colors[0]);
    pixels.setPixelColor(EEPROM.read(XYtoLED(piecePositions[0]+1, piecePositions[1])), colors[0]);
    pixels.setPixelColor(EEPROM.read(XYtoLED(piecePositions[2], piecePositions[4])), colors[1]);
    pixels.setPixelColor(EEPROM.read(XYtoLED(piecePositions[2]+1, piecePositions[4])), colors[1]);
    // Update ball position
    pixels.setPixelColor(EEPROM.read(XYtoLED(piecePositions[5], piecePositions[6])), colors[2]);
    pixels.show();
}

void resetGame()
{
    piecePositions[0] = WIDTH/2 - 1;
    piecePositions[1] = 0;
    piecePositions[2] = WIDTH/2 - 1;
    piecePositions[3] = HEIGHT-1;
    piecePositions[4] = WIDTH / 3;
    piecePositions[5] = HEIGHT/3;
    drawFrame();
    delay(1000);
}

bool checkCollisions()
{
    if(piecePositions[5] <= 0)
    {
        // If both p1 x and ball x are the same
        if(piecePositions[4] == piecePositions[0] || piecePositions[4] == piecePositions[0] + 1)
        {
            // Reflect ball
            ballVector[1] = -ballVector[1];
            return false;
        }
        else
        {
            // Player 1 has lost
            p2Wins++;
            return true;
        }
    }
    else if(piecePositions[5] >= HEIGHT-1)
    {
        if(piecePositions[4] == piecePositions[2] || piecePositions[4] == piecePositions[2] + 1)
        {
            ballVector[1] = -ballVector[1];
            return false;
        }
        else
        {
            // Player 2 has lost
            p1Wins++;
            return true;
        }
    }
    if(piecePositions[4] <= 0)
    {
        ballVector[0] = -ballVector[0];
        return false;
    }
    else
    {
        ballVector[0] = -ballVector[0];
        return false;
    }
    
}

void drawScores()
{
    for(int i = 0; i < p1Wins; i++)
    {
        pixels.setPixelColor(EEPROM.read(i), colors[0]);
    }
    for(int i = p1Wins; i < p2Wins + p1Wins; i++)
    {
        pixels.setPixelColor(EEPROM.read(i), colors[1]);
    }
    pixels.show();
    delay(3000);
    resetGame();
}

uint8_t XYtoLED(uint8_t x, uint8_t y)
{
    return (y * WIDTH) + x;
}

offset_table.h

C Header File
const uint8_t offsets[48] = {


};

Credits

Evan Rust

Evan Rust

120 projects • 1053 followers
IoT, web, and embedded systems enthusiast. Contact me for product reviews or custom project requests.

Comments