# Qt5 中的打砖块游戏

> 原文： [http://zetcode.com/gui/qt5/breakoutgame/](http://zetcode.com/gui/qt5/breakoutgame/)

在 Qt5 教程的这一部分中，我们创建了一个简单的打砖块游戏克隆。

打砖块是 Atari Inc. 开发的一款街机游戏。该游戏创建于 1976 年。在该游戏中，玩家移动桨叶并弹跳球。 目的是销毁窗口顶部的砖块。

## 开发

在我们的游戏中，我们只有一个桨，一个球和三十个砖头。 计时器用于创建游戏周期。 我们不使用角度，而是仅更改方向：顶部，底部，左侧和右侧。 该代码的灵感来自 Nathan Dawson 在 PyGame 库中开发的 PyBreakout 游戏。

游戏是故意简单的。 没有奖金，等级或分数。 这样更容易理解。

Qt5 库用于创建计算机应用。 但是，它也可以用于创建游戏。 开发计算机游戏是学习有关 Qt5 的好方法。

`paddle.h`

```cpp
#pragma once

#include <QImage>
#include <QRect>

class Paddle {

  public:
    Paddle();
    ~Paddle();

  public:
    void resetState();
    void move();
    void setDx(int);
    QRect getRect();
    QImage & getImage();

  private:
    QImage image;
    QRect rect;
    int dx;
    static const int INITIAL_X = 200;
    static const int INITIAL_Y = 360;
};

```

这是桨对象的头文件。 `INITIAL_X`和`INITIAL_Y`是代表桨状对象的初始坐标的常数。

`paddle.cpp`

```cpp
#include <iostream>
#include "paddle.h"

Paddle::Paddle() {

  dx = 0;    
  image.load("paddle.png");

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

Paddle::~Paddle() {

 std::cout << ("Paddle deleted") << std::endl;
}

void Paddle::setDx(int x) {
  dx = x;
}

void Paddle::move() {

    int x = rect.x() + dx;
    int y = rect.top();

    rect.moveTo(x, y);
}

void Paddle::resetState() {

  rect.moveTo(INITIAL_X, INITIAL_Y);
}

QRect Paddle::getRect() {

  return rect;
}

QImage & Paddle::getImage() {

  return image;
}

```

桨板可以向右或向左移动。

```cpp
Paddle::Paddle() {

  dx = 0;    
  image.load("paddle.png");

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

```

在构造函数中，我们启动`dx`变量并加载桨图像。 我们得到图像矩形并将图像移动到其初始位置。

```cpp
void Paddle::move() {

    int x = rect.x() + dx;
    int y = rect.top();

    rect.moveTo(x, y);
}

```

`move()`方法移动桨的矩形。 移动方向由`dx`变量控制。

```cpp
void Paddle::resetState() {

  rect.moveTo(INITIAL_X, INITIAL_Y);
}

```

`resetState()`将拨片移动到其初始位置。

`brick.h`

```cpp
#pragma once

#include <QImage>
#include <QRect>

class Brick {

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

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

  private:
    QImage image;
    QRect rect;
    bool destroyed;
};

```

这是砖对象的头文件。 如果销毁了积木，则`destroyed`变量将设置为`true`。

`brick.cpp`

```cpp
#include <iostream>
#include "brick.h"

Brick::Brick(int x, int y) {

  image.load("brickie.png");
  destroyed = false;
  rect = image.rect();
  rect.translate(x, y);
}

Brick::~Brick() {

  std::cout << ("Brick deleted") << std::endl;
}

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

```

`Brick`类代表砖对象。

```cpp
Brick::Brick(int x, int y) {

  image.load("brickie.png");
  destroyed = false;
  rect = image.rect();
  rect.translate(x, y);
}

```

砖的构造器加载其图像，启动`destroyed`标志，然后将图像移至其初始位置。

```cpp
bool Brick::isDestroyed() {

  return destroyed;
}

```

砖块具有`destroyed`标志。 如果设置了`destroyed`标志，则不会在窗口上绘制砖块。

`ball.h`

```cpp
#pragma once

#include <QImage>
#include <QRect>

class Ball {

  public:
    Ball();
    ~Ball();

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

  private:
    int xdir;
    int ydir;
    QImage image;
    QRect rect;
    static const int INITIAL_X = 230;
    static const int INITIAL_Y = 355;    
    static const int RIGHT_EDGE = 300;
};

```

这是球形对象的头文件。 `xdir`和`ydir`变量存储球的运动方向。

`ball.cpp`

```cpp
#include <iostream>
#include "ball.h"

Ball::Ball() {

  xdir = 1;
  ydir = -1;

  image.load("ball.png");

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

Ball::~Ball() {

  std::cout << ("Ball deleted") << std::endl;
}

void Ball::autoMove() {

  rect.translate(xdir, ydir);

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

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

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

void Ball::resetState() {

  rect.moveTo(INITIAL_X, INITIAL_Y);
}

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

```

`Ball`类表示球对象。

```cpp
xdir = 1;
ydir = -1;

```

开始时，球向东北方向移动。

```cpp
void Ball::autoMove() {

  rect.translate(xdir, ydir);

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

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

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

```

在每个游戏周期都会调用`autoMove()`方法来在屏幕上移动球。 如果它破坏了边界，球的方向就会改变。 如果球越过底边，则球不会反弹回来-游戏结束。

`breakout.h`

```cpp
#pragma once

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

class Breakout : public QWidget {

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

  protected:
    void paintEvent(QPaintEvent *);
    void timerEvent(QTimerEvent *);
    void keyPressEvent(QKeyEvent *);
    void keyReleaseEvent(QKeyEvent *);
    void drawObjects(QPainter *);
    void finishGame(QPainter *, QString);
    void moveObjects();

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

  private:
    int x;
    int timerId;
    static const int N_OF_BRICKS = 30;
    static const int DELAY = 10;
    static const int BOTTOM_EDGE = 400;
    Ball *ball;
    Paddle *paddle;
    Brick *bricks[N_OF_BRICKS];
    bool gameOver;
    bool gameWon;
    bool gameStarted;
    bool paused;
};

```

这是突破对象的头文件。

```cpp
void keyPressEvent(QKeyEvent *);
void keyReleaseEvent(QKeyEvent *);

```

使用光标键控制桨。 在游戏中，我们监听按键和按键释放事件。

```cpp
int x;
int timerId;

```

`x`变量存储桨的当前 x 位置。 `timerId`用于识别计时器对象。 当我们暂停游戏时，这是必需的。

```cpp
static const int N_OF_BRICKS = 30;

```

`N_OF_BRICKS`常数存储游戏中的积木数量。

```cpp
static const int DELAY = 10;

```

`DELAY`常数控制游戏的速度。

```cpp
static const int BOTTOM_EDGE = 400;

```

当球通过底边时，比赛结束。

```cpp
Ball *ball;
Paddle *paddle;
Brick *bricks[N_OF_BRICKS];

```

游戏包括一个球，一个球拍和一系列砖块。

```cpp
bool gameOver;
bool gameWon;
bool gameStarted;
bool paused;

```

这四个变量代表游戏的各种状态。

`breakout.cpp`

```cpp
#include <QPainter>
#include <QApplication>
#include "breakout.h"

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<N_OF_BRICKS; i++) {
   delete bricks[i];
 }
}

void Breakout::paintEvent(QPaintEvent *e) {

  Q_UNUSED(e);  

  QPainter painter(this);

  if (gameOver) {

    finishGame(&painter, "Game lost");    

  } else if(gameWon) {

    finishGame(&painter, "Victory");
  }
  else {

    drawObjects(&painter);
  }
}

void Breakout::finishGame(QPainter *painter, QString message) {

  QFont font("Courier", 15, QFont::DemiBold);
  QFontMetrics fm(font);
  int textWidth = fm.width(message);

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

  painter->translate(QPoint(w/2, h/2));
  painter->drawText(-textWidth/2, 0, message);    
}

void Breakout::drawObjects(QPainter *painter) {

  painter->drawImage(ball->getRect(), ball->getImage());
  painter->drawImage(paddle->getRect(), paddle->getImage());

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

void Breakout::timerEvent(QTimerEvent *e) {

  Q_UNUSED(e);  

  moveObjects();
  checkCollision();
  repaint();
}

void Breakout::moveObjects() {

  ball->autoMove();
  paddle->move();
}

void Breakout::keyReleaseEvent(QKeyEvent *e) {

    int dx = 0;

    switch (e->key()) {
        case Qt::Key_Left:
            dx = 0;
            paddle->setDx(dx);        
            break;       

        case Qt::Key_Right:
            dx = 0;
            paddle->setDx(dx);        
            break;    
    }
}

void Breakout::keyPressEvent(QKeyEvent *e) {

    int dx = 0;

    switch (e->key()) {
    case Qt::Key_Left:

        dx = -1;
        paddle->setDx(dx);

        break;

    case Qt::Key_Right:

        dx = 1;
        paddle->setDx(dx);        
        break;

    case Qt::Key_P:

        pauseGame();
        break;

    case Qt::Key_Space:

        startGame();
        break;        

    case Qt::Key_Escape:

        qApp->exit();
        break;

    default:
        QWidget::keyPressEvent(e);
    }
}

void Breakout::startGame() {

  if (!gameStarted) {
    ball->resetState();
    paddle->resetState();

    for (int i=0; i<N_OF_BRICKS; i++) {
      bricks[i]->setDestroyed(false);
    }

    gameOver = false; 
    gameWon = false; 
    gameStarted = true;
    timerId = startTimer(DELAY);  
  }      
}

void Breakout::pauseGame() {

  if (paused) {

    timerId = startTimer(DELAY);
    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() > BOTTOM_EDGE) {
    stopGame();
  }

  for (int i=0, j=0; i<N_OF_BRICKS; i++) {

    if (bricks[i]->isDestroyed()) {
      j++;
    }

    if (j == N_OF_BRICKS) {
      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<N_OF_BRICKS; 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);
      }
    }
  }
}

```

在`breakout.cpp`文件中，我们有游戏逻辑。

```cpp
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`对象的构造函数中，我们实例化了三十个砖块。

```cpp
void Breakout::paintEvent(QPaintEvent *e) {

  Q_UNUSED(e);  

  QPainter painter(this);

  if (gameOver) {

    finishGame(&painter, "Game lost");    

  } else if(gameWon) {

    finishGame(&painter, "Victory");
  }
  else {

    drawObjects(&painter);
  }
}

```

根据`gameOver`和`gameWon`变量，我们要么用消息结束游戏，要么在窗口上绘制游戏对象。

```cpp
void Breakout::finishGame(QPainter *painter, QString message) {

  QFont font("Courier", 15, QFont::DemiBold);
  QFontMetrics fm(font);
  int textWidth = fm.width(message);

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

  painter->translate(QPoint(w/2, h/2));
  painter->drawText(-textWidth/2, 0, message);    
}

```

`finishGame()`方法在窗口中心绘制一条最终消息。 它是“游戏失败”或“胜利”。 `QFontMetrics'` `width()`用于计算字符串的宽度。

```cpp
void Breakout::drawObjects(QPainter *painter) {

  painter->drawImage(ball->getRect(), ball->getImage());
  painter->drawImage(paddle->getRect(), paddle->getImage());

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

```

`drawObjects()`方法在窗口上绘制游戏的所有对象：球，球拍和砖头。 这些对象由图像表示，`drawImage()`方法将它们绘制在窗口上。

```cpp
void Breakout::timerEvent(QTimerEvent *e) {

  Q_UNUSED(e);  

  moveObjects();
  checkCollision();
  repaint();
}

```

在`timerEvent()`中，我们移动对象，检查球是否与桨或砖相撞，并生成绘图事件。

```cpp
void Breakout::moveObjects() {

  ball->autoMove();
  paddle->move();
}

```

`moveObjects()`方法移动球和桨对象。 他们自己的`move`方法被调用。

```cpp
void Breakout::keyReleaseEvent(QKeyEvent *e) {

    int dx = 0;

    switch (e->key()) {
        case Qt::Key_Left:
            dx = 0;
            paddle->setDx(dx);        
            break;       

        case Qt::Key_Right:
            dx = 0;
            paddle->setDx(dx);        
            break;    
    }
}

```

当播放器释放`左`光标键或`右`光标键时，我们将板的`dx`变量设置为零。 结果，桨停止运动。

```cpp
void Breakout::keyPressEvent(QKeyEvent *e) {

    int dx = 0;

    switch (e->key()) {
    case Qt::Key_Left:

        dx = -1;
        paddle->setDx(dx);

        break;

    case Qt::Key_Right:

        dx = 1;
        paddle->setDx(dx);        
        break;

    case Qt::Key_P:

        pauseGame();
        break;

    case Qt::Key_Space:

        startGame();
        break;        

    case Qt::Key_Escape:

        qApp->exit();
        break;

    default:
        QWidget::keyPressEvent(e);
    }
}

```

在`keyPressEvent()`方法中，我们监听与游戏相关的按键事件。 左和右光标键移动桨状对象。 他们设置`dx`变量，该变量随后添加到桨的 x 坐标中。 `P`键暂停游戏，空格键启动游戏。 `Esc`键退出应用。

```cpp
void Breakout::startGame() {

  if (!gameStarted) {
    ball->resetState();
    paddle->resetState();

    for (int i=0; i<N_OF_BRICKS; i++) {
      bricks[i]->setDestroyed(false);
    }

    gameOver = false; 
    gameWon = false; 
    gameStarted = true;
    timerId = startTimer(DELAY);  
  }      
}

```

`startGame()`方法重置球和桨对象； 他们被转移到他们的初始位置。 在`for`循环中，我们将每个积木的`destroyed`标志重置为`false`，从而将它们全部显示在窗口中。 `gameOver`，`gameWon`和`gameStarted`变量获得其初始布尔值。 最后，使用`startTimer()`方法启动计时器。

```cpp
void Breakout::pauseGame() {

  if (paused) {

    timerId = startTimer(DELAY);
    paused = false;
  } else {

    paused = true;
    killTimer(timerId); 
  }        
}

```

`pauseGame()`用于暂停和开始暂停的游戏。 状态由`paused`变量控制。 我们还存储计时器的 ID。 为了暂停游戏，我们使用`killTimer()`方法终止计时器。 要重新启动它，我们调用`startTimer()`方法。

```cpp
void Breakout::stopGame() {

  killTimer(timerId);    
  gameOver = true;      
  gameStarted = false;
}

```

在`stopGame()`方法中，我们终止计时器并设置适当的标志。

```cpp
void Breakout::checkCollision() {

  if (ball->getRect().bottom() > BOTTOM_EDGE) {
    stopGame();
  }
...
}

```

在`checkCollision()`方法中，我们对游戏进行碰撞检测。 如果球撞到底边，则比赛结束。

```cpp
for (int i=0, j=0; i<N_OF_BRICKS; i++) {

  if (bricks[i]->isDestroyed()) {
    j++;
  }

  if (j == N_OF_BRICKS) {
    victory();
  }
}

```

我们检查了多少砖被破坏了。 如果我们摧毁了所有积木，我们将赢得这场比赛。

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

```

如果球碰到了桨的第一部分，我们会将球的方向更改为西北。

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

```

如果球撞击砖的底部，我们将改变球的 y 方向； 它下降了。

`main.cpp`

```cpp
#include <QApplication>
#include "breakout.h"

int main(int argc, char *argv[]) {

  QApplication app(argc, argv);  

  Breakout window;

  window.resize(300, 400);
  window.setWindowTitle("Breakout");
  window.show();

  return app.exec();
}

```

这是主文件。

![The Breakout game](img/8fba6e5d33e40021a6382d17adcf16bb.jpg)

图：打砖块游戏

这是 Qt5 中的打砖块游戏。