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

Game.cpp

Committer:
maxint
Date:
2015-03-02
Revision:
6:1f5862465b5d
Parent:
5:8441b390a15f

File content as of revision 6:1f5862465b5d:

#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 = "Left/Right/tilt to play.";

#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()

//
// Initialisation
//

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), 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), 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::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");
}


//
// Generic methods
//

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::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::printf(int x, int y, const char *szFormat, ...)
{   // formats: %s, %d, %0.2f
    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);
}

int Game::checkTiltLeftRight()
{    // check current X-tilting for left-right input (left=-1, right=1)
    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);
}


//
// Paddle
//

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
    {    // move the paddle by tilting the board left or right
        int i=this->checkTiltLeftRight();
        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::checkPaddle()
{
    Rectangle rScreen=Rectangle(0,0, WIDTH, HEIGHT);            // screen boundary

    this->paddle.checkBoundary(rScreen);
}


//
// Balls
//

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() % 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::checkNumBalls()
{
    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);   
    }
}



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 rPaddleMiddle=Rectangle(paddle.pos.getX() + Game::PADDLE_WIDTH/3, paddle.pos.getY(), paddle.pos.getX() + Game::PADDLE_WIDTH/3 + Game::PADDLE_WIDTH/3, HEIGHT+10);      // paddle middle 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

    Line lPaddleLeft=Line(paddle.pos.getX(), paddle.pos.getY(), paddle.pos.getX() + Game::PADDLE_WIDTH/3, HEIGHT+10);      // paddle left part
    Line lPaddleRight=Line(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

    //printf(0, 20, "Paddle: %d-%d  %d-%d  ", rPaddleLeft.getX1(), rPaddleLeft.getX2(), rPaddleRight.getX1(), rPaddleRight.getX2());

    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(lPaddleLeft) || pBall->collides(lPaddleRight) || pBall->collides(rPaddleMiddle))
            {
                if(pBall->collides(lPaddleLeft))   pBall->vSpeed.add(Vector(-1.3,0));      // left side of paddle has bias to the left
                if(pBall->collides(lPaddleRight))  pBall->vSpeed.add(Vector(1.3,0));       // right side of paddle has bias to the right
                pBall->Bounce(Vector(1,-1));
              
                {
                    // 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->vSpeed.multiply(Vector(1,1.02));        // bounce up from paddle at higher speed
                }
                //printf(10, 10, "Bounce: %0.2f, %0.2f  ", pBall->vSpeed.x, pBall->vSpeed.y);
        
                // 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();
            }
            this->fDrawPaddle=true;
        }
    }
}


//
// Other gamestuff
//

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->checkNumBalls(); 
        
        wait_ms(this->nGameTickDelay);  // can be adjusted using up/down
    }
    else {
        this->accel.updateGraph();
        wait_ms(100);
    } 
}


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::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::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;
    }
}