Nick Tolk
Published © MIT

Pong, using single encoder and Adafruit SSD1306 display

It's Pong-ish. Mostly demo code for an IoT bootcamp, showing use of the display, encoder, and some programming concepts introduced in class.

BeginnerProtip3 hours156
Pong, using single encoder and Adafruit SSD1306 display

Things used in this project

Hardware components

Argon
Particle Argon
×1
Grove - OLED Display 0.66" (SSD1306)- IIC - 3.3V/5V
Seeed Studio Grove - OLED Display 0.66" (SSD1306)- IIC - 3.3V/5V
https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1

Story

Read more

Schematics

Pong SSD1306 breadboard

Fritzing breadboard layout for this Pong game

Pong SSD1306 schematic

Fritzing schematic associated with breadboard layout

Code

Pong_SSD1306.ino

C9Search
/*
 * Project Pong_SSD1306
 * Description: Pong using Adafruit SSD1306 display
 *              Switch starts new game. Initial ball position and direction are randomized.
 *              Ball speeds up with every bounce until max speed is reached. Score accumulates based on speed.
 * Author:      Nick Tolk
 * Date:        03-MAR-2023
 */

SYSTEM_MODE(SEMI_AUTOMATIC)

// https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
#include "Adafruit_SSD1306.h"
// http://www.pjrc.com/teensy/td_libs_Encoder.html
#include "Encoder.h"

#include "pongFuncs.h" // Includes constants and functions needed

static Adafruit_SSD1306 display(OLED_RESET);  // OLED_RESET in "pongFuncs.h"

static Encoder myEnc(PINA, PINB); // PINA and PINB in "pongFuncs.h"
int encPos;                       // Encoder position, returned from movePaddle() to re-write

static PongBall *myBall;
static PongDisplay *myPongDisplay;
static PongPaddle *myPaddle;

enum SwitchColors{SW_RED, SW_GREEN, SW_BLUE};  // the switch is only lit red (game end) or green (game playing)
void setSwColor(SwitchColors color);  // Sets color on switch

int startTime, lastTick = 0;          // for keeping track of game ticks

float score, thisScore;   // "score" is cumulative. "thisScore" is the immediate increment.


void setup() {
  Serial.begin(9600);
  initPins();            // set pin states according to constants in "pongFuncs.h"

  myBall = new PongBall(display.width(), display.height());     // width and height are used mainly in moveBall() to check for bounces or paddle hits
  myPaddle = new PongPaddle();                                  // tracks paddle position against Encoder, and checks for collisions with ball
  myPongDisplay = new PongDisplay(&display, myBall, myPaddle);  // handles drawing the game

  initGame();           // randomizes ball position and direction
  startTime = millis(); // starts timer for refreshes
}


void loop() {
  myPongDisplay->redrawAll((int)score);     // Refresh display

  // Handle paddle position
  encPos = myPaddle->movePaddle(myEnc.read(), display.height() - 1);  // move paddle and get new Encoder setting
  myEnc.write(encPos);                                                // store Encoder setting

  // Move ball and track score
  if (millis() - lastTick > updateDelay){   // only move the ball if enough time has passed
    lastTick = millis();                    // reset timer
    thisScore = myBall->moveBall(myPaddle); // increment ball position and check for end-of-game
    if (thisScore != 0.0){        // any returned value other than 0 indicates ball in play
      score += thisScore / 10.0;  // so increment the score
    } else {                                  // thisScore == 0.0 indicates game end
      endGame();                              // so end the game,
      while (digitalRead(PINSWITCH) == 0){}   // wait for game reset,
      initGame();                             // then restart
    }
  }

  // Check for requested game restart even if playing
  if (digitalRead(PINSWITCH)){    // if the switch is pressed,
    initGame();                   // then start a new game
  }
}

// Randomizes ball position and direction; sets switch color; and resets score
void initGame(){
  myBall->setInit();                // randomize ball
  setSwColor(SW_GREEN);             // make switch green
  score = 0;                        // reset score
  startTime = millis();             // reset play time
}

// Turns switch red and draws "end of game" screen
void endGame(){
  setSwColor(SW_RED);                                           // make switch red
  myPongDisplay->redrawAll(score, (millis() - startTime)/1000); // refresh display with playing time
}

// Sets switch color to either SW_GREEN or SW_RED.
void setSwColor(SwitchColors color){
  switch(color){
    case SW_RED:
      digitalWrite(PINBUTTR, LEDON);    // turn on the red light,
      digitalWrite(PINBUTTG, LEDOFF);   // turn off the green one,
      digitalWrite(PINBUTTB, LEDOFF);   // and turn off the blue one
      break;
    case SW_GREEN:
      digitalWrite(PINBUTTR, LEDOFF);
      digitalWrite(PINBUTTG, LEDON);
      digitalWrite(PINBUTTB, LEDOFF);
      break;
    case SW_BLUE:
    default:
      digitalWrite(PINBUTTR, LEDOFF);
      digitalWrite(PINBUTTG, LEDOFF);
      digitalWrite(PINBUTTB, LEDON);
      break;
  }
}

// sets pin states for current breadboard wiring
void initPins(){
    pinMode(PINBUTTR, OUTPUT);
    pinMode(PINBUTTG, OUTPUT);
    pinMode(PINBUTTB, OUTPUT);
    pinMode(PINSWITCH, INPUT_PULLDOWN);
    setSwColor(SW_GREEN);
}

pongBall.h

C/C++
#ifndef _PONGBALL_
#define _PONGBALL_

#ifndef sqrt        // sqrt needed for speed calculation, included in "math.h"
#include <math.h>
#endif  // sqrt

#include "pongPaddle.h"

// PongBall class
// Tracks & modifies position and velocity of ball
class PongBall {
    private:
        float _xPos, _yPos;       // (x, y) position of ball
        float _xVel, _yVel;       // (x, y) increments in position in pixels per tick
        float _speed;             // calculated from _xVal and _yVal. Used in scoring.

        int _dWidth, _dHeight;    // size of OLED display
    public:
        PongBall(int dWidth, int dHeight);
        void setInit();     // randomize ball position and velocity
        float moveBall(PongPaddle *myPaddle);    // increment ball position using current velocity

        float getSpeed();   // accessor for _speed
        int getX();         // accessor for _xPos. Returns int for display, rather than float used for ball tracking.
        int getY();         // accessor for _yPos. Returns int for display, rather than float used for ball tracking.
        int getR();         // accessor for ballR. Currently a constant.
};

PongBall::PongBall(int dWidth, int dHeight){
    _dWidth = dWidth;
    _dHeight = dHeight;
    setInit();
}

// Sets initial position and direction of ball using constants and values taken from display
void PongBall::setInit(){
  _xPos = random(ballR + 1, _dWidth - 1 - ballR);   // Ball position set randomly within display
  _yPos = random(ballR + 1, _dHeight - 1 - ballR);

  _xVel = random(3000, 5000) / 1000.0;
  _yVel = sqrt(startSpeed * startSpeed - _xVel * _xVel);  // Ball velocity in pixels per tick
  if (_xPos > 64 ){                                       // Set x direction toward farthest wall
    _xVel *= -1;
  }
  if (rand() %2 ){          // y direction set randomly up or down
    _yVel *= -1;
  }
  _speed = startSpeed;
}

// Increment ball's position by one tick. Returns 0 for game loss. Otherwise returns current speed.
float PongBall::moveBall(PongPaddle *myPaddle){
    _xPos += _xVel;
    _yPos += _yVel;
    if (_xPos - ballR <= 0 || _xPos + ballR >= _dWidth - 1){    // check edge to see if we need to check the paddle
        if (myPaddle->paddleHit((int)_yPos, (int)_yVel)){       // check the paddle
            if (_xPos < _dWidth / 2){                           // We hit the paddle!
                _xPos = ballR;
            } else {
                _xPos = _dWidth - ballR;
            }
            _xVel = -_xVel;                                         // reverse x direction
            if (sqrt(_xVel * _xVel + _yVel * _yVel) < maxSpeed){    // and ACCELERATE!
                _xVel *= speedInc;
                _yVel *= speedInc;
                _speed = sqrt(_xVel * _xVel + _yVel * _yVel);
            }
        } else {
            return (0.0);               // missed the paddle; lose
        }
    }
    if (_yPos + ballR >= _dHeight){     // check the top/bottom and bounce if needed
        _yPos = _dHeight - ballR;
        _yVel = -_yVel;
    } else if (_yPos - ballR <= 0){
        _yPos = ballR;
        _yVel = -_yVel;
    }

    return(_speed / (float)startSpeed); // Ball is alive, so return the current normalized speed for scoring
}

// Returns speed normalized by starting speed. Used for scoring.
float PongBall::getSpeed(){
    return(_speed / (float)startSpeed);
}

// Accessor function for x position
int PongBall::getX(){
    return((int)_xPos);
}

// Accessor function for y position
int PongBall::getY(){
    return((int)_yPos);
}

// Accessor function for ball radius
int PongBall::getR(){
    return(ballR);
}

#endif  // _PONGBALL

pongDisplay.h

C/C++
// Drawing functions to support L07_05b_Pong2
#ifndef _PONGDISPLAY_
#define _PONGDISPLAY_

#include "pongBall.h"
#include "pongPaddle.h"

class PongDisplay {
    private:
        Adafruit_SSD1306 *_display; // pointer to SSD1306 display object
        PongBall *_myBall;          // ponter to ball object
        PongPaddle *_myPaddle;      
        float _xPos, _yPos;         // (x, y) position of ball
        float _xVel, _yVel;         // (x, y) increments in position in pixels per tick
        float _speed;               // calculated from _xVal and _yVal. Used in scoring.

        int _dWidth, _dHeight;      // size of OLED display
        void initDisplay();
        void drawPaddle();
        void drawWalls();
        void drawBall();
        void drawScore(uint score);  // "score" is tracked as float, but drawn as int
        void drawLoss(uint timePlayed); // end screen
    public:
        void redrawAll(uint score, uint timePlayed);    // Calls all drawing functions to refresh display. 
                                                        // "timePlayed" defaults to 0, and anything else indicates endgame.
        PongDisplay(Adafruit_SSD1306 *display, PongBall *myBall, PongPaddle *myPaddle);
};

// Assigns pointers and initializes display
PongDisplay::PongDisplay(Adafruit_SSD1306 *display, PongBall *myBall, PongPaddle *myPaddle){
    _display = display;
    _myBall = myBall;
    _myPaddle = myPaddle;
    initDisplay(); // Uses display constants in "pongConsts.h"
}

// Calls begin(), sets text stuff, and clears display
void PongDisplay::initDisplay(){
    _display->begin(SSD1306_SWITCHCAPVCC, displayAddress);
    _display->setTextColor(WHITE);
    _display->setTextSize(1);
    _display->clearDisplay();
    _display->display();
}

// Calls redraw functions and updates display. timePlayed > 0 indicates end of game.
void PongDisplay::redrawAll(uint score, uint timePlayed = 0){
    _display->clearDisplay();
    drawWalls();
    drawPaddle();
    drawBall();
    drawScore(score);
    if (timePlayed > 0){      // Any value for this except 0 is loss
        drawLoss(timePlayed);
    }
    _display->display();
}

// Draws to matching vertical lines at edged. The paddle starts at paddlePos, then continues for paddleSize pixels.
void PongDisplay::drawPaddle(){
    _display->drawLine(0, _myPaddle->getStart(), 0, _myPaddle->getEnd(), WHITE);
    _display->drawLine(_display->width()-1, _myPaddle->getStart(), _display->width()-1, _myPaddle->getEnd(), WHITE);
}

// Draws horizontal lines across the top and bottom of display
void PongDisplay::drawWalls(){
    _display->drawLine(0, 0, _display->width()-1, 0, WHITE);
    _display->drawLine(0, _display->height()-1, _display->width()-1, _display->height()-1, WHITE);
}

// Draws a circle at the coordinates returned by myBall
void PongDisplay::drawBall(){
    _display->fillCircle(_myBall->getX(), _myBall->getY(), _myBall->getR(), WHITE);
}

// Displays current score and ball speed. Contains some magic numbers tweaked for SSD1306 cosmetics.
void PongDisplay::drawScore(uint score){
    _display->setCursor(_display->width() - 32, 5);
    _display->setTextSize(1);
    _display->printf("%5u\n", score);
    _display->setCursor(_display->width() - 26, 15);
    _display->printf("x%0.1f\n", _myBall->getSpeed());
}

void PongDisplay::drawLoss(uint timePlayed){
    _display->setCursor(0, 5);
    _display->setTextSize(2);
    _display->printf(" %u s\n", timePlayed);
    _display->setTextSize(1);
    _display->printf(" Push switch\n to try again");
}

#endif  // _PONGDISPLAY_

pongFuncs.h

C/C++
#ifndef _PONGCONSTS_
#define _PONGCONSTS_

#define LEDON LOW
#define LEDOFF HIGH

// gameplay constants
const int paddleSize = 20;
const int ballR = 2;            // radius of ball
const float speedInc = 1.05;    // multiplier for x and y velocity after every bounce, until maxSpeed is reached or exceeded
const int startSpeed = 6;       // In pixels per tick. Calculated as sqrt(_xVel^2 + _yVel^2).
const int maxSpeed = 20;        // speed cap
const int updateDelay = 20;     // minimum ms between plot points

// pin assignments and addresses
const uint8_t displayAddress = 0x3C;


// Encoder pins
const int PINSWITCH = D3;
const int PINBUTTR = D8;
const int PINBUTTG = D7;
const int PINBUTTB = D6;
const int PINA = D5;
const int PINB = D4;
const int OLED_RESET = D9;  // Not actually wired right now and unused

const int encMax = 95;      // Encoder limit

void initGame();    // Sets score to 0, resets ball, and sets switch color
void endGame();     // Displays endgame info and sets pin color

#include "pongDisplay.h"
#include "pongBall.h"
#include "pongPaddle.h"

#endif      // _PONGCONSTS_

pongPaddle.h

C/C++
// PongPaddle class
#ifndef _PONGPADDLE_
#define _PONGPADDLE_


class PongPaddle {
    private:
        int _paddlePos;     // lowest pixel included as part of paddle
        int _paddleSize;    // size of paddle
    public:
        PongPaddle();
        bool paddleHit(int yPos, int yVel);   // returns true if paddle collides
        int getStart();     // accessor for _paddlePos
        int getEnd();       // returns _paddlePos + _paddleSize
        int movePaddle(int encPos, int yMax);
};

// Constructor. Set size to default, and position to 0.
PongPaddle::PongPaddle(){
    _paddlePos = 0;
    _paddleSize = paddleSize;
}

// Changes paddle position accoring to Encoder, and returns new encoder setting
int PongPaddle::movePaddle(int encPos, int yMax){       // yMax based on display height
    _paddlePos = map(encPos, 0, encMax, yMax - _paddleSize, 0);
    _paddlePos = (_paddlePos < 0) ? 0 : (_paddlePos > yMax - _paddleSize) ? yMax - _paddleSize : _paddlePos;  // bounds check
    return(map(_paddlePos, 0, yMax - _paddleSize, encMax, 0));
}

// Returns true if next ball position is within paddle limits
bool PongPaddle::paddleHit(int yPos, int yVel){
    return(yPos > _paddlePos - abs(yVel) && yPos < _paddlePos + paddleSize + abs(yVel));
}

// returns _paddlePos (Beggining of paddle in pixels)
int PongPaddle::getStart(){
    return(_paddlePos);
}

// returns _paddlePos + _paddleSize (End of paddle in pixels)
int PongPaddle::getEnd(){
    return(_paddlePos + _paddleSize);
}

#endif  // _PONGPADDLE_

Credits

Nick Tolk

Nick Tolk

4 projects • 9 followers

Comments