Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 |
I coded up a basic Pong game using some materials provided in an IoT C++ Deepdive course through CNM Ingenuity. I2C communications are used for game display on a SSD1306 128x64 OLED. An encoder with RGB LEDs and a switch are used for gameplay and feedback. In code, classes are used for ball position and speed tracking, paddle handling, etc.
It's posted mainly to help out getting acquainted with the display, encoder use, and C++ coding for the Particle Argon.
/*
* 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);
}
#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
// 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_
#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 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_
Comments