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