Saturday, April 27, 2013

Breakout game in Qt4

In this part of the Qt4 C++ programming tutorial we will create a simple Breakout game clone.
The Breakout is an arcade game developed by Atari Inc. The game was created in 1976. In this game, the player moves a paddle on the screen and bounces a ball/balls. The objective is to destroy bricks in the top of the window.

The development

In our game, we have one paddle, one ball and 30 bricks. I have created an image for a ball, paddle and a brick in Inkscape. We use a timer to create a game cycle. We do not work with angles, we simply change directions. Top, bottom, left and right. I was inspired by the pybreakout game. It was developed in PyGame library by Nathan Dawson.
The game is intentionally simple. There are no bonuses, levels, score. So that it is easier to understand.
The Qt4 C++ programming library is developed for creating computer applications. Nevertheless, it can be used to create games as well. Developing a computer game is a great way to learn the Qt4 programming library.
paddle.h
#ifndef PADDLE_H
#define PADDLE_H

#include <QImage>
#include <QRect>

class Paddle
{

  public:
    Paddle();
    ~Paddle();

  public:
    void resetState();
    void moveLeft(int);
    void moveRight(int);
    QRect getRect();
    QImage & getImage();

  private:
    QImage image;
    QRect rect;

};

#endif
This is a header file for the paddle object.
paddle.cpp
#include "paddle.h"
#include <iostream>

Paddle::Paddle()
{
  image.load("paddle.png");

  rect = image.rect();
  resetState();
}

Paddle::~Paddle()
{
 std::cout << ("Paddle deleted\n");
}

void Paddle::moveLeft(int left)
{
  if (rect.left() >= 2)
    rect.moveTo(left, rect.top());
}

void Paddle::moveRight(int right)
{
  if (rect.right() <= 298)
    rect.moveTo(right, rect.top());
}

void Paddle::resetState()
{
  rect.moveTo(200, 360);
}

QRect Paddle::getRect()
{
  return rect;
}

QImage & Paddle::getImage()
{
  return image;
}
The paddle can be moved to the right or to the left.
Paddle::Paddle()
{
  image.load("paddle.png");

  rect = image.rect();
  resetState();
}
In the constructor, we load our paddle image. Get the image rectangle and move the image to it's starting position.
void Paddle::moveLeft(int left)
{
  if (rect.left() >= 2)
    rect.moveTo(left, rect.top());
}
The moveLeft() method moves the rectangle to the left.
brick.h
#ifndef BRICK_H
#define BRICK_H

#include <QImage>
#include <QRect>

class Brick
{

  public:
    Brick(int, int);
    ~Brick();

  public:
    void resetState();
    bool isDestroyed();
    void setDestroyed(bool);
    QRect getRect();
    void setRect(QRect);
    QImage & getImage();

  private:
    QImage image;
    QRect rect;
    int position;
    bool destroyed;

};

#endif
This is the header file for the brick object.
brick.cpp
#include "brick.h"
#include <iostream>

Brick::Brick(int x, int y) 
{
  image.load("brick.png");
  destroyed = FALSE;
  rect = image.rect();
  rect.translate(x, y);
}

Brick::~Brick() {

   std::cout << ("Brick deleted\n");
}

QRect Brick::getRect()
{
  return rect;
}

void Brick::setRect(QRect rct)
{
  rect = rct;
}

QImage & Brick::getImage()
{
  return image;
}

bool Brick::isDestroyed()
{
  return destroyed;
}

void Brick::setDestroyed(bool destr)
{
  destroyed = destr;
}
bool Brick::isDestroyed()
{
  return destroyed;
}
The brick has a destroyed flag. If the destroyed flag is set, the brick is not drawn on the window.
ball.h
#ifndef BALL_H
#define BALL_H

#include <QImage>
#include <QRect>

class Ball
{

  public:
    Ball();
    ~Ball();

  public:
    void resetState();
    void moveBottom(int);
    void moveTop(int);
    void moveLeft(int);
    void moveRight(int);
    void autoMove();
    void setXDir(int);
    void setYDir(int);
    int getXDir();
    int getYDir();
    QRect getRect();
    QImage & getImage();

  private:
    int angle;
    int speed;
    int xdir;
    int ydir;
    bool stuck;
    QImage image;
    QRect rect;

};

#endif
The header file for the ball object.
ball.cpp
#include "ball.h"
#include <iostream>

Ball::Ball()
{

  xdir = 1;
  ydir = -1;

  image.load("ball.png");

  rect = image.rect();
  resetState();

}

Ball::~Ball() {
  std::cout << ("Ball deleted\n");
}


void Ball::autoMove()
{
  rect.translate(xdir, ydir);

  if (rect.left() == 0) {
    xdir = 1;
  }

  if (rect.right() == 300) {
    xdir = -1;
  }

  if (rect.top() == 0) {
    ydir = 1;
  }
}

void Ball::resetState() 
{
  rect.moveTo(230, 355);
}

void Ball::moveBottom(int bottom)
{
  rect.moveBottom(bottom);
}

void Ball::moveTop(int top)
{
  rect.moveTop(top);
}

void Ball::moveLeft(int left)
{
  rect.moveLeft(left);
}

void Ball::moveRight(int right)
{
  rect.moveRight(right);
}

void Ball::setXDir(int x)
{
  xdir = x;
}

void Ball::setYDir(int y)
{
  ydir = y;
}

int Ball::getXDir()
{
  return xdir;
}

int Ball::getYDir()
{
  return ydir;
}

QRect Ball::getRect()
{
  return rect;
}

QImage & Ball::getImage()
{
  return image;
}
The autoMove() method is called each game cycle to move the ball on the screen. If it hists the boudaries, the ball direction changes.
breakout.h
#ifndef BREAKOUT_H
#define BREAKOUT_H

#include "ball.h"
#include "brick.h"
#include "paddle.h"
#include <QWidget>
#include <QKeyEvent>

class Breakout : public QWidget
{
  Q_OBJECT

  public:
    Breakout(QWidget *parent = 0);
    ~Breakout();

  protected:
    void paintEvent(QPaintEvent *event);
    void timerEvent(QTimerEvent *event);
    void keyPressEvent(QKeyEvent *event);

    void startGame();
    void pauseGame();
    void stopGame();
    void victory();
    void checkCollision();

  private:
    int x;
    int timerId;
    Ball *ball;
    Paddle *paddle;
    Brick * bricks[30];
    bool gameOver;
    bool gameWon;
    bool gameStarted;
    bool paused;

};

#endif
This is the header file for the breakout object.
 int x;
 int timerId;
The x value stores the current x position of the paddle. The timerId is used for identifying of the timer object. This is necessary, when we pause the game.
breakout.cpp
#include "breakout.h"
#include <QPainter>
#include <QApplication>

Breakout::Breakout(QWidget *parent)
    : QWidget(parent)
{

  x = 0;
  gameOver = FALSE;
  gameWon = FALSE;
  paused = FALSE;
  gameStarted = FALSE;
  ball = new Ball();
  paddle = new Paddle();


  int k = 0;
  for (int i=0; i<5; i++) {
    for (int j=0; j<6; j++) {
      bricks[k] = new Brick(j*40+30, i*10+50);
      k++; 
    }
  }
}

Breakout::~Breakout() {
 delete ball;
 delete paddle;
 for (int i=0; i<30; i++) {
   delete bricks[i];
 }
}

void Breakout::paintEvent(QPaintEvent *event)
{
  QPainter painter(this);

  if (gameOver) {
    QFont font("Courier", 15, QFont::DemiBold);
    QFontMetrics fm(font);
    int textWidth = fm.width("Game Over");

    painter.setFont(font);
    int h = height();
    int w = width();

    painter.translate(QPoint(w/2, h/2));
    painter.drawText(-textWidth/2, 0, "Game Over");
  }
  else if(gameWon) {
    QFont font("Courier", 15, QFont::DemiBold);
    QFontMetrics fm(font);
    int textWidth = fm.width("Victory");

    painter.setFont(font);
    int h = height();
    int w = width();

    painter.translate(QPoint(w/2, h/2));
    painter.drawText(-textWidth/2, 0, "Victory");
  }
  else {
    painter.drawImage(ball->getRect(), 
        ball->getImage());
    painter.drawImage(paddle->getRect(), 
        paddle->getImage());

    for (int i=0; i<30; i++) {
        if (!bricks[i]->isDestroyed()) 
          painter.drawImage(bricks[i]->getRect(), 
              bricks[i]->getImage());
    }
  }
}

void Breakout::timerEvent(QTimerEvent *event)
{
  ball->autoMove();
  checkCollision();
  repaint();
}



void Breakout::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Left:
       {  
         int x = paddle->getRect().x();
        for (int i=1; i<=5; i++)
          paddle->moveLeft(x--);
        break;
       }
    case Qt::Key_Right:
        { 
          int x = paddle->getRect().x();
          for (int i=1; i<=5; i++)
            paddle->moveRight(x++);
        }
        break;
    case Qt::Key_P:
        { 
          pauseGame();
        }
        break;
    case Qt::Key_Space:
        {
          startGame();
        }
        break;
    case Qt::Key_Escape:
        {
          qApp->exit();
        }
        break;
    default:
        QWidget::keyPressEvent(event);
    }
}

void Breakout::startGame()
{ 
  if (!gameStarted) {
    ball->resetState();
    paddle->resetState();

    for (int i=0; i<30; i++) {
      bricks[i]->setDestroyed(FALSE);
    }
    gameOver = FALSE; 
    gameWon = FALSE; 
    gameStarted = TRUE;
    timerId = startTimer(10);  
  }
}

void Breakout::pauseGame()
{
  if (paused) {
    timerId = startTimer(10);
    paused = FALSE;
  } else {
    paused = TRUE;
    killTimer(timerId); 
   }
}

void Breakout::stopGame()
{
  killTimer(timerId);    
  gameOver = TRUE;      
  gameStarted = FALSE;
}

void Breakout::victory()
{
  killTimer(timerId);
  gameWon = TRUE;  
  gameStarted = FALSE;
}

void Breakout::checkCollision()
{

  if (ball->getRect().bottom() > 400)
    stopGame(); 

  for (int i=0, j=0; i<30; i++) {
    if (bricks[i]->isDestroyed()) {
      j++;
    }
    if (j==30) 
      victory();
  }

  if ((ball->getRect()).intersects(paddle->getRect())) {

    int paddleLPos = paddle->getRect().left();  
    int ballLPos = ball->getRect().left();   

    int first = paddleLPos + 8;
    int second = paddleLPos + 16;
    int third = paddleLPos + 24;
    int fourth = paddleLPos + 32;

    if (ballLPos < first) {
      ball->setXDir(-1);
      ball->setYDir(-1);
    }

    if (ballLPos >= first && ballLPos < second) {
      ball->setXDir(-1);
      ball->setYDir(-1*ball->getYDir());
    }

    if (ballLPos >= second && ballLPos < third) {
       ball->setXDir(0);
       ball->setYDir(-1);
    }

    if (ballLPos >= third && ballLPos < fourth) {
       ball->setXDir(1);
       ball->setYDir(-1*ball->getYDir());
    }

    if (ballLPos > fourth) {
      ball->setXDir(1);
      ball->setYDir(-1);
    }
  }


  for (int i=0; i<30; i++) {
    if ((ball->getRect()).intersects(bricks[i]->getRect())) {

      int ballLeft = ball->getRect().left();  
      int ballHeight = ball->getRect().height(); 
      int ballWidth = ball->getRect().width();
      int ballTop = ball->getRect().top();  

      QPoint pointRight(ballLeft + ballWidth + 1, ballTop);
      QPoint pointLeft(ballLeft - 1, ballTop);  
      QPoint pointTop(ballLeft, ballTop -1);
      QPoint pointBottom(ballLeft, ballTop + ballHeight + 1);  

      if (!bricks[i]->isDestroyed()) {
        if(bricks[i]->getRect().contains(pointRight)) {
           ball->setXDir(-1);
        } 

        else if(bricks[i]->getRect().contains(pointLeft)) {
           ball->setXDir(1);
        } 

        if(bricks[i]->getRect().contains(pointTop)) {
           ball->setYDir(1);
        } 

        else if(bricks[i]->getRect().contains(pointBottom)) {
           ball->setYDir(-1);
        } 

        bricks[i]->setDestroyed(TRUE);
      }
    }
  }

}
Here is the game logic.
 int k = 0;
 for (int i=0; i<5; i++) {
   for (int j=0; j<6; j++) {
     bricks[k] = new Brick(j*40+30, i*10+50);
     k++; 
   }
 }
In the constructor of the Breakout object, we create 30 bricks.
 painter.drawImage(ball->getRect(), 
     ball->getImage());
 painter.drawImage(paddle->getRect(), 
     paddle->getImage());

 for (int i=0; i<30; i++) {
     if (!bricks[i]->isDestroyed()) 
       painter.drawImage(bricks[i]->getRect(), 
           bricks[i]->getImage());
 }
If the game is not won or lost, we draw the ball, paddle and the bricks in the paintEvent().
void Breakout::timerEvent(QTimerEvent *event)
{
  ball->autoMove();
  checkCollision();
  repaint();
}
In the timerEvent(), we move the ball, check if the ball collided with the paddle or a brick and generate a paint event.
 case Qt::Key_Left:
    {  
      int x = paddle->getRect().x();
     for (int i=1; i<=5; i++)
       paddle->moveLeft(x--);
     break;
    }
If we press the left cursor key, we call the moveLeft() method of the paddle object. We call the method five times, so that the movement is smooth.
void Breakout::stopGame()
{
  killTimer(timerId);    
  gameOver = TRUE;      
  gameStarted = FALSE;
}
In the stopGame() method, we kill the timer and set the appropriate flags.
 if (ball->getRect().bottom() > 400)
   stopGame(); 
If the ball hits the bottom, we stop the game.
 for (int i=0, j=0; i<30; i++) {
   if (bricks[i]->isDestroyed()) {
     j++;
   }
   if (j==30) 
     victory();
 }
We check how many bricks are destroyed. If we destroyed all 30 bricks, we win the game.
 if (ballLPos < first) {
   ball->setXDir(-1);
   ball->setYDir(-1);
 }
If the ball hits the first part of the paddle, we change the direction of the ball to north east.
 if(bricks[i]->getRect().contains(pointTop)) {
   ball->setYDir(1);
 }
If the ball hits the bottom of the brick, we change the y direction of the ball. It goes down.
main.cpp
#include "breakout.h"
#include <QDesktopWidget>
#include <QApplication>

void center(QWidget &widget)
{
  int x, y;
  int screenWidth;
  int screenHeight;

  int WIDTH = 300;
  int HEIGHT = 400;
  

  QDesktopWidget *desktop = QApplication::desktop();

  screenWidth = desktop->width();
  screenHeight = desktop->height();
 
  x = (screenWidth - WIDTH) / 2;
  y = (screenHeight - HEIGHT) / 2;

  widget.setGeometry(x, y, WIDTH, HEIGHT);
  widget.setFixedSize(WIDTH, HEIGHT);
}


int main(int argc, char *argv[])
{
  QApplication app(argc, argv);  
    
  Breakout window;
  
  window.setWindowTitle("Breakout");
  window.show();
  center(window);

  return app.exec();
}
Main file.
The Breakout game
Figure: The Breakout game
So this was the Breakout game in C++ and Qt4 library.

No comments:

Post a Comment