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
Revision:
0:7e989d0083ff
Child:
1:bf46edcd6b4f
--- /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