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
Diff: Game.cpp
- 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