Balls & Paddle game for RETRO Pong inspired game featuring multi-directional tilt-sensitive paddle, multiple balls, shrinking ceiling and a bit of gravity.

Dependencies:   LCD_ST7735 MusicEngine RETRO_BallsAndThings mbed

Balls and Paddle

After doing some work on the Pong mod I decided to put my efforts into making my version object oriented and try to make a generic object-library that could be use for other ball-and-things games. To add some challenges to the gameplay, the following features were added:

  • extra-free additional balls to please the juglers
  • gravity for pulling the ball down to create some dynamic movement
  • directional power-paddle that counters the ball with a bit more speed
  • lowering ceiling to make endless gameplay impossible

Files at this revision

API Documentation at this revision

Comitter:
maxint
Date:
Fri Feb 06 10:18:41 2015 +0000
Child:
1:bf46edcd6b4f
Commit message:
First game ready for competition

Changed in this revision

Game.cpp Show annotated file Show diff for this revision Revisions of this file
Game.h Show annotated file Show diff for this revision Revisions of this file
LCD_ST7735.lib Show annotated file Show diff for this revision Revisions of this file
Main.cpp Show annotated file Show diff for this revision Revisions of this file
MusicEngine.lib Show annotated file Show diff for this revision Revisions of this file
RETRO_BallsAndThings.lib Show annotated file Show diff for this revision Revisions of this file
mbed.bld Show annotated file Show diff for this revision Revisions of this file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Game.cpp	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,430 @@
+#include "Game.h"
+
+const char* Game::LOSE_1 = "Game over.";
+const char* Game::LOSE_2 = "Press ship to restart.";
+const char* Game::SPLASH_1 = "-*- Balls and paddle -*-";
+const char* Game::SPLASH_2 = "Press ship to start.";
+const char* Game::SPLASH_3 = "Made by Maxint";
+
+
+#define WHITE Color565::White
+#define BLACK Color565::Black
+#define BLUE Color565::Blue
+#define RED Color565::Red
+#define YELLOW Color565::Yellow
+
+#define CHAR_WIDTH 8
+#define CHAR_HEIGHT 8
+#define HEIGHT this->disp.getHeight()
+#define WIDTH this->disp.getWidth()
+
+
+
+Game::Game() : left(P0_14, PullUp), right(P0_11, PullUp), down(P0_12, PullUp), up(P0_13, PullUp), square(P0_16, PullUp), circle(P0_1, PullUp), led1(P0_9), led2(P0_8), 
+    ain(P0_15), i2c(P0_5, P0_4), disp(P0_19, P0_20, P0_7, P0_21, P0_22, P1_15, P0_2, LCD_ST7735::RGB), accel(this->I2C_ADDR, &disp), vGravity(0, 0.1), ball(&disp), paddle(&disp)
+{
+    this->disp.setOrientation(LCD_ST7735::Rotate270, false);
+    this->disp.setForegroundColor(WHITE);
+    this->disp.setBackgroundColor(BLACK);
+    this->disp.clearScreen();
+
+    srand(this->ain.read_u16());
+    
+    this->lastUp = false;
+    this->lastDown = false;
+    this->mode = true;          // mode: true=game, false=graph
+
+    this->nGameTickDelay=25;    // game tickdelay can be adjusted using up/down
+
+    for(int i=0; i<NUM_BALLS; i++)
+        this->aBalls[i]=Ball(&(this->disp));
+
+    this->snd.reset();
+}
+
+void Game::printDouble(double value, int x, int y)
+{
+    char buffer[10];
+    int len = sprintf(buffer, "%.1f ", value);
+    
+    this->disp.drawString(font_oem, x, y, buffer);
+}
+
+void Game::initialize()
+{
+    this->disp.clearScreen();
+
+    this->snd.reset();
+    this->nBalls = 4;
+    this->nScore = 0;
+    this->nTopWall = 8;
+    this->fDrawTopWall=true;
+    
+    this->initializePaddle();
+    this->setNoBalls();     // reset all balls
+    this->newBall();     // start first ball
+    this->snd.play("T240 L16 O5 D E F");
+}
+
+
+void Game::initializePaddle()
+{
+    this->paddle.initialize(WIDTH / 2 - Game::PADDLE_WIDTH/2, HEIGHT - Game::PADDLE_HEIGHT, Game::PADDLE_WIDTH, Game::PADDLE_HEIGHT);
+    this->fDrawPaddle=true;
+}
+
+
+void Game::updatePaddle()
+{
+    if (!this->left.read())  // note: read is LOW (0) when button pressed
+        this->paddle.move(Vector(-1 * Game::PADDLE_SPEED, 0));
+    else if (!this->right.read())
+        this->paddle.move(Vector(Game::PADDLE_SPEED, 0));
+    else
+    {
+        int i=this->checkTilt();
+        if(i>0)
+            this->paddle.move(Vector(Game::PADDLE_SPEED, 0));
+        else if(i<0)
+            this->paddle.move(Vector(-1 * Game::PADDLE_SPEED, 0));
+        else if(this->paddle.hasChanged())
+            paddle.move(Vector(0, 0));  // move to same place to restrict redraws
+    }
+}
+
+void Game::redrawPaddle()
+{   // redraw the paddle when moved, or when forced by this->fDrawPaddle (set at bounce)
+    this->paddle.redraw(this->fDrawPaddle);
+    this->fDrawPaddle=false;
+}
+
+void Game::setNoBalls()
+{   // make sure no balls are active
+    for(int i=0; i<NUM_BALLS; i++)
+        this->aBalls[i].fActive=false;
+}
+
+void Game::newBall()
+{   // add a new ball to the game
+    for(int i=0; i<NUM_BALLS; i++)
+    {
+        if(this->aBalls[i].fActive)
+            continue;
+        else
+        {
+            this->aBalls[i].initialize(WIDTH / 2 - Game::BALL_RADIUS, this->nTopWall + (HEIGHT-this->nTopWall) / 4 - Game::BALL_RADIUS, Game::BALL_RADIUS, Color565::fromRGB(i==0?0xFF:0x33, i==1?0xFF:0x33, i==2?0xFF:0x33));
+            //float ftRandX=rand() % 2 ? 1 : -1;
+            //float ftRandY=rand() % 2 ? 1 : -1;
+            //this->aBalls[i].setSpeed(ftRandX, ftRandY);
+            float ftRandX=((rand() % 20) - 10)/5.0;     // left/right at random speed
+            float ftRandY=((rand() % 10) - 10)/5.0;     // up at random speed
+            this->aBalls[i].vSpeed.set(ftRandX, ftRandY);
+            this->aBalls[i].fActive=true;
+            break;
+        }
+    }
+}
+
+void Game::updateBalls()
+{
+    for(int i=0; i<NUM_BALLS; i++)
+    {
+        if(!this->aBalls[i].fActive)
+            continue;
+
+        this->aBalls[i].update();                    // update the ball position 
+
+        // add downward gravity
+        if(this->aBalls[i].vSpeed.getSize()<10.0)
+            this->aBalls[i].vSpeed.add(this->vGravity);    // add some gravity
+
+    }
+}
+
+void Game::redrawBalls()
+{
+    for(int i=0; i<NUM_BALLS; i++)
+    {
+        if(!this->aBalls[i].fActive)
+            continue;
+        this->aBalls[i].redraw();                    // update the ball position 
+    }
+}
+
+int Game::countBalls()
+{
+    int nResult=0;
+    for(int i=0; i<NUM_BALLS; i++)
+    {
+        if(this->aBalls[i].fActive)
+            nResult++;
+    }
+    return(nResult);
+}
+
+
+
+
+
+void Game::tick()
+{  
+    this->checkButtons();
+    
+    if (this->mode) {
+
+        this->updateBalls();                    // update the ball positions
+        this->updatePaddle();
+    
+        this->checkPaddle();
+        this->checkBallsCollision();
+
+        this->redrawBalls();
+        this->redrawPaddle();
+        this->redrawTopWall();
+        
+        //this->checkScore(); 
+        this->checkBalls(); 
+        
+        wait_ms(this->nGameTickDelay);  // can be adjusted using up/down
+    }
+    else {
+        this->accel.updateGraph();
+        wait_ms(100);
+    } 
+}
+
+int Game::checkTilt()
+{    // move the paddle by tilting the board left or righr
+    double x, y, z;
+    this->accel.getXYZ(x, y, z);
+    if(x<-0.1) return(-1);
+    else if(x>0.1) return(1);
+    else return(0);
+}
+
+void Game::checkButtons()
+{
+    if(!this->square.read())       // note: button.read() is false (LOW/0) when pressed
+    {
+        wait_ms(250);   // el-cheapo deboounce
+        this->mode = !this->mode;
+        
+        this->disp.clearScreen();
+        
+        if (!this->mode)
+        {
+            this->accel.resetGraph();
+        }
+        
+        this->led1.write(this->mode);
+        this->led2.write(!this->mode);
+    }
+    else if(!this->circle.read() && this->mode)       // note: button.read() is false (LOW/0) when pressed
+    {
+        bool fMute=this->snd.getMute();
+        fMute=!fMute;
+        this->snd.setMute(fMute);
+        this->led2.write(fMute);
+        wait_ms(250);   // el-cheapo deboounce
+    }
+    else
+    {  
+        bool isUp = !this->up.read();
+        bool isDown = !this->down.read();
+        
+        if (isUp && isDown) goto end;
+        if (!isUp && !isDown) goto end;
+        
+        if (isUp && this->lastUp) goto end;
+        if (isDown && this->lastDown) goto end;
+        
+        if (isUp)
+        {
+            if(this->nGameTickDelay<1000) this->nGameTickDelay=(float)this->nGameTickDelay*1.20;
+            this->printf(100, 0, "Speed: %d  ", this->nGameTickDelay);   
+        }
+        else if (isDown)
+        {
+            if(this->nGameTickDelay>5) this->nGameTickDelay=(float)this->nGameTickDelay/1.20;
+            this->printf(100, 0, "Speed: %d  ", this->nGameTickDelay);   
+        }
+    
+end:
+        this->lastUp = isUp;
+        this->lastDown = isDown;
+    }
+}
+
+void Game::drawString(const char* str, int y)
+{
+    uint8_t width;
+    uint8_t height;
+    
+    this->disp.measureString(font_oem, str, width, height);
+    this->disp.drawString(font_oem, WIDTH / 2 - width / 2, y, str);
+    
+}
+
+void Game::showSplashScreen()
+{
+    this->drawString(Game::SPLASH_1, HEIGHT / 2 - CHAR_HEIGHT / 2);  
+    this->drawString(Game::SPLASH_2, HEIGHT / 2 + CHAR_HEIGHT / 2); 
+    this->drawString(Game::SPLASH_3, HEIGHT / 2 + CHAR_HEIGHT / 2 + 2*CHAR_HEIGHT); 
+           
+    while (this->circle.read())
+        wait_ms(1);
+    wait_ms(250);   // el-cheapo deboounce
+
+    this->initialize();     // start a new game
+}
+
+
+
+void Game::checkBallsCollision()
+{
+    Rectangle rTop=Rectangle(0, -10, WIDTH, this->nTopWall);       // top wall
+    Rectangle rBottom=Rectangle(0, HEIGHT, WIDTH, HEIGHT+10);      // bottom gap
+    Rectangle rLeft=Rectangle(-10, 0, 0, HEIGHT);                  // left wall
+    Rectangle rRight=Rectangle(WIDTH, 0, WIDTH+10, HEIGHT);        // right wall
+    Rectangle rPaddle=Rectangle(paddle.pos.getX(), paddle.pos.getY(), paddle.pos.getX() + Game::PADDLE_WIDTH, HEIGHT+10);        // paddle
+    Rectangle rPaddleLeft=Rectangle(paddle.pos.getX(), paddle.pos.getY(), paddle.pos.getX() + Game::PADDLE_WIDTH/3, HEIGHT+10);      // paddle left part
+    Rectangle rPaddleRight=Rectangle(paddle.pos.getX()+ Game::PADDLE_WIDTH/3 + Game::PADDLE_WIDTH/3, paddle.pos.getY(), paddle.pos.getX() + Game::PADDLE_WIDTH, HEIGHT+10);      // paddle right part
+
+    Ball* pBall;
+    for(int i=0; i<NUM_BALLS; i++)
+    {
+        if(!this->aBalls[i].fActive)
+            continue;
+
+        pBall=&(this->aBalls[i]);
+
+        if(pBall->collides(rTop) && pBall->vSpeed.isUp())      // top wall
+        {
+            pBall->Bounce(Vector(1,-1));        // bounce vertical
+            this->snd.beepShort();
+            this->fDrawTopWall=true;
+        }
+        if(pBall->collides(rRight) && pBall->vSpeed.isRight())      // right wall
+        {
+            pBall->Bounce(Vector(-1,1));        // bounce horizontal
+            this->snd.beepShort();
+        }
+        if(pBall->collides(rLeft) && pBall->vSpeed.isLeft())      // left wall
+        {
+            pBall->Bounce(Vector(-1,1));        // bounce horizontal
+            this->snd.beepShort();
+        }
+        if(pBall->collides(rPaddle) && pBall->vSpeed.isDown())      // paddle
+        {
+            if(pBall->collides(rPaddleLeft))   pBall->vSpeed.add(Vector(-1.3,0));      // left side of paddle has bias to the left
+            if(pBall->collides(rPaddleRight))  pBall->vSpeed.add(Vector(1.3,0));       // right side of paddle has bias to the right
+    
+            // bounce the ball
+            // increase the speed of the ball when hitting the paddle to increase difficulty
+            float ftSpeedMax=3.0;
+            if(this->nScore>50)
+                ftSpeedMax=5.0;
+            if(this->nScore>100)
+                ftSpeedMax=10.0;
+            if(this->nScore>150)
+                ftSpeedMax=999.0;
+            if(pBall->vSpeed.getSize()<ftSpeedMax)
+                pBall->Bounce(Vector(1,-1.02));        // bounce from paddle at higher speed
+            else
+                pBall->Bounce(Vector(1,-1));        // bounce vertical at same speed
+    
+            // force drawing the paddle after redrawing the bounced ball
+            this->fDrawPaddle=true;
+
+            // make sound and update the score
+            this->snd.beepLong();
+            this->nScore++;
+            this->printf(100, 0, "Score: %d ", this->nScore);   
+
+            // add a new ball every 10 points
+            if(this->nScore>0 && this->nScore%10==0)
+            {
+                this->newBall();
+                this->nBalls++;
+                this->snd.play("T240 L16 O5 D E F");
+            }
+
+            // lower the ceiling every 25 points
+            if(this->nScore>0 && this->nScore%25==0)
+            {
+                this->nTopWall+=3;
+                this->fDrawTopWall=true;
+                this->snd.play("T240 L16 O5 CDEFG");
+            }
+
+        }
+        if(pBall->collides(rBottom) && pBall->vSpeed.isDown())      // bottom gap
+        {
+            pBall->clearPrev();   // clear the ball from its previous position
+            pBall->clear();   // clear the ball from its current position
+            pBall->vSpeed.set(0,0);
+            pBall->fActive=false;
+            this->nBalls--;
+            if(countBalls()==0)
+            {
+                this->newBall();     // start a new ball
+                this->snd.beepLow();
+            }
+        }
+    }
+}
+
+void Game::redrawTopWall()
+{
+    if(this->fDrawTopWall)
+    {
+        int nTop=max(this->nTopWall-2, 8);
+        this->disp.fillRect(0, 8, WIDTH, nTop, Color565::Black);
+        this->disp.fillRect(0, nTop, WIDTH, this->nTopWall, Color565::Purple);
+        this->fDrawTopWall=false;
+    }
+}
+
+void Game::checkPaddle()
+{
+    Rectangle rScreen=Rectangle(0,0, WIDTH, HEIGHT);            // screen boundary
+
+    this->paddle.checkBoundary(rScreen);
+}
+
+
+void Game::printf(int x, int y, const char *szFormat, ...)
+{
+    char szBuffer[256];
+    va_list args;
+
+    va_start(args, szFormat);
+    vsprintf(szBuffer, szFormat, args);
+    va_end(args);
+    this->disp.drawString(font_oem, x, y, szBuffer);
+}
+
+
+void Game::checkBalls()
+{
+    if (this->nBalls == 0)
+    {   // game over
+        char buf[256];
+        this->disp.clearScreen();
+
+        this->drawString(Game::LOSE_1, HEIGHT / 2 - CHAR_HEIGHT); 
+        this->drawString(Game::LOSE_2, HEIGHT / 2);  
+        sprintf(buf,"Your score: %d  ", this->nScore);
+        this->drawString(buf, HEIGHT / 2 + CHAR_HEIGHT / 2 + CHAR_HEIGHT ); 
+        
+        this->snd.play("T120 O3 L4 R4 F C F2 C");
+        while (this->circle.read())
+            wait_ms(1);
+        wait_ms(250);   // el-cheapo deboounce
+        this->initialize();
+    }
+    else
+    {
+        this->printf(0, 0, "Balls: %d  ", this->nBalls);   
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Game.h	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,104 @@
+#pragma once
+#include <stdarg.h>
+#include <algorithm>        // required for min() and max(), see http://www.cplusplus.com/reference/algorithm/
+#include "mbed.h"
+
+#include "Color565.h"
+#include "font_OEM.h"
+#include "LCD_ST7735.h"
+#include "SoundFX.h"
+#include "Accelerometer.h"
+#include "Shapes.h"
+#include "Ball.h"
+#include "Paddle.h"
+
+
+#define NUM_BALLS 3
+
+class Game
+{
+    static const char* LOSE_1;
+    static const char* LOSE_2;
+    static const char* SPLASH_1;
+    static const char* SPLASH_2;
+    static const char* SPLASH_3;
+    //char buf[256];
+    
+    //static const int BALL_RADIUS = 3;
+    static const int BALL_RADIUS = 6;
+    static const int PADDLE_WIDTH = 38;
+    static const int PADDLE_HEIGHT = 4;
+    static const int PADDLE_SPEED = 4;
+    static const char I2C_ADDR = 0x1C << 1;
+
+    DigitalIn left;
+    DigitalIn right;
+    DigitalIn down;
+    DigitalIn up;
+    DigitalIn square;
+    DigitalIn circle; 
+    DigitalOut led1;
+    DigitalOut led2;
+    AnalogIn ain;
+    I2C i2c;
+    LCD_ST7735 disp;
+
+    SoundFX snd;
+    Accelerometer accel;
+//    Timer tWait;    // timer used for tickcounts
+
+    Vector vGravity;
+    Ball ball;
+    Paddle paddle;
+    Ball aBalls[NUM_BALLS];
+
+    bool mode;
+    int nBalls;
+    int nScore;
+    bool fDrawPaddle;
+    bool fDrawTopWall;
+    
+    int nTopWall;
+
+    bool lastUp;
+    bool lastDown;
+    int nGameTickDelay;     // delay during game-tick
+
+    void printDouble(double value, int x, int y);
+    
+    void initialize();
+    
+    void drawString(const char* str, int y);
+    
+
+    void initializePaddle();
+    void updatePaddle();
+    void redrawPaddle();
+/*
+    void clearPaddle();
+    void drawPaddle();
+*/
+    //void initializeBall();
+    //void initializeBalls();
+    void setNoBalls();
+    void newBall();
+    void updateBalls();
+    void redrawBalls();
+    int countBalls();
+    void checkBallsCollision();
+    void redrawTopWall();
+    
+    int checkTilt();
+    void checkButtons();
+
+    void checkPaddle();
+    //void checkCollision();
+    void printf(int x, int y, const char *szFormat, ...);
+    void checkBalls();
+    
+    public:
+        Game();
+        
+        void showSplashScreen();
+        void tick();
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LCD_ST7735.lib	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,1 @@
+http://mbed.org/users/taylorza/code/LCD_ST7735/#c94d0a2c2ba0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Main.cpp	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,12 @@
+#include "mbed.h"
+
+#include "Game.h"
+
+int main() {
+    Game game;
+        
+    game.showSplashScreen();
+    
+    while (true)
+        game.tick();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MusicEngine.lib	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,1 @@
+http://developer.mbed.org/users/taylorza/code/MusicEngine/#4f7c4255997a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RETRO_BallsAndThings.lib	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,1 @@
+RETRO_BallsAndThings#71185a0aadfc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mbed.bld	Fri Feb 06 10:18:41 2015 +0000
@@ -0,0 +1,1 @@
+http://mbed.org/users/mbed_official/code/mbed/builds/e188a91d3eaa
\ No newline at end of file