原文: http://zetcode.com/gui/qt4/breakoutgame/

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

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

开发

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

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

Qt4 库是为创建计算机应用而开发的。 但是,它也可以用于创建游戏。 开发计算机游戏是了解 Qt4 的好方法。

paddle.h

  1. #pragma once
  2. #include <QImage>
  3. #include <QRect>
  4. class Paddle {
  5. public:
  6. Paddle();
  7. ~Paddle();
  8. public:
  9. void resetState();
  10. void move();
  11. void setDx(int);
  12. QRect getRect();
  13. QImage & getImage();
  14. private:
  15. QImage image;
  16. QRect rect;
  17. int dx;
  18. static const int INITIAL_X = 200;
  19. static const int INITIAL_Y = 360;
  20. };

这是桨对象的头文件。 INITIAL_XINITIAL_Y是代表桨状对象的初始坐标的常数。

paddle.cpp

  1. #include "paddle.h"
  2. #include <iostream>
  3. Paddle::Paddle() {
  4. dx = 0;
  5. image.load("paddle.png");
  6. rect = image.rect();
  7. resetState();
  8. }
  9. Paddle::~Paddle() {
  10. std::cout << ("Paddle deleted") << std::endl;
  11. }
  12. void Paddle::setDx(int x) {
  13. dx = x;
  14. }
  15. void Paddle::move() {
  16. int x = rect.x() + dx;
  17. int y = rect.top();
  18. rect.moveTo(x, y);
  19. }
  20. void Paddle::resetState() {
  21. rect.moveTo(INITIAL_X, INITIAL_Y);
  22. }
  23. QRect Paddle::getRect() {
  24. return rect;
  25. }
  26. QImage & Paddle::getImage() {
  27. return image;
  28. }

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

  1. Paddle::Paddle() {
  2. dx = 0;
  3. image.load("paddle.png");
  4. rect = image.rect();
  5. resetState();
  6. }

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

  1. void Paddle::move() {
  2. int x = rect.x() + dx;
  3. int y = rect.top();
  4. rect.moveTo(x, y);
  5. }

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

  1. void Paddle::resetState() {
  2. rect.moveTo(INITIAL_X, INITIAL_Y);
  3. }

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

brick.h

  1. #pragma once
  2. #include <QImage>
  3. #include <QRect>
  4. class Brick {
  5. public:
  6. Brick(int, int);
  7. ~Brick();
  8. public:
  9. bool isDestroyed();
  10. void setDestroyed(bool);
  11. QRect getRect();
  12. void setRect(QRect);
  13. QImage & getImage();
  14. private:
  15. QImage image;
  16. QRect rect;
  17. bool destroyed;
  18. };

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

brick.cpp

  1. #include "brick.h"
  2. #include <iostream>
  3. Brick::Brick(int x, int y) {
  4. image.load("brickie.png");
  5. destroyed = false;
  6. rect = image.rect();
  7. rect.translate(x, y);
  8. }
  9. Brick::~Brick() {
  10. std::cout << ("Brick deleted") << std::endl;
  11. }
  12. QRect Brick::getRect() {
  13. return rect;
  14. }
  15. void Brick::setRect(QRect rct) {
  16. rect = rct;
  17. }
  18. QImage & Brick::getImage() {
  19. return image;
  20. }
  21. bool Brick::isDestroyed() {
  22. return destroyed;
  23. }
  24. void Brick::setDestroyed(bool destr) {
  25. destroyed = destr;
  26. }

Brick类代表砖对象。

  1. Brick::Brick(int x, int y) {
  2. image.load("brickie.png");
  3. destroyed = false;
  4. rect = image.rect();
  5. rect.translate(x, y);
  6. }

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

  1. bool Brick::isDestroyed() {
  2. return destroyed;
  3. }

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

ball.h

  1. #pragma once
  2. #include <QImage>
  3. #include <QRect>
  4. class Ball {
  5. public:
  6. Ball();
  7. ~Ball();
  8. public:
  9. void resetState();
  10. void autoMove();
  11. void setXDir(int);
  12. void setYDir(int);
  13. int getXDir();
  14. int getYDir();
  15. QRect getRect();
  16. QImage & getImage();
  17. private:
  18. int xdir;
  19. int ydir;
  20. QImage image;
  21. QRect rect;
  22. static const int INITIAL_X = 230;
  23. static const int INITIAL_Y = 355;
  24. static const int RIGHT_EDGE = 300;
  25. };

这是球形对象的头文件。 xdirydir变量存储球的运动方向。

ball.cpp

  1. #include "ball.h"
  2. #include <iostream>
  3. Ball::Ball() {
  4. xdir = 1;
  5. ydir = -1;
  6. image.load("ball.png");
  7. rect = image.rect();
  8. resetState();
  9. }
  10. Ball::~Ball() {
  11. std::cout << ("Ball deleted") << std::endl;
  12. }
  13. void Ball::autoMove() {
  14. rect.translate(xdir, ydir);
  15. if (rect.left() == 0) {
  16. xdir = 1;
  17. }
  18. if (rect.right() == RIGHT_EDGE) {
  19. xdir = -1;
  20. }
  21. if (rect.top() == 0) {
  22. ydir = 1;
  23. }
  24. }
  25. void Ball::resetState() {
  26. rect.moveTo(INITIAL_X, INITIAL_Y);
  27. }
  28. void Ball::setXDir(int x) {
  29. xdir = x;
  30. }
  31. void Ball::setYDir(int y) {
  32. ydir = y;
  33. }
  34. int Ball::getXDir() {
  35. return xdir;
  36. }
  37. int Ball::getYDir() {
  38. return ydir;
  39. }
  40. QRect Ball::getRect() {
  41. return rect;
  42. }
  43. QImage & Ball::getImage() {
  44. return image;
  45. }

Ball类表示球对象。

  1. xdir = 1;
  2. ydir = -1;

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

  1. void Ball::autoMove() {
  2. rect.translate(xdir, ydir);
  3. if (rect.left() == 0) {
  4. xdir = 1;
  5. }
  6. if (rect.right() == RIGHT_EDGE) {
  7. xdir = -1;
  8. }
  9. if (rect.top() == 0) {
  10. ydir = 1;
  11. }
  12. }

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

breakout.h

  1. #pragma once
  2. #include <QWidget>
  3. #include <QKeyEvent>
  4. #include "ball.h"
  5. #include "brick.h"
  6. #include "paddle.h"
  7. class Breakout : public QWidget {
  8. Q_OBJECT
  9. public:
  10. Breakout(QWidget *parent = 0);
  11. ~Breakout();
  12. protected:
  13. void paintEvent(QPaintEvent *);
  14. void timerEvent(QTimerEvent *);
  15. void keyPressEvent(QKeyEvent *);
  16. void keyReleaseEvent(QKeyEvent *);
  17. void drawObjects(QPainter *);
  18. void finishGame(QPainter *, QString);
  19. void moveObjects();
  20. void startGame();
  21. void pauseGame();
  22. void stopGame();
  23. void victory();
  24. void checkCollision();
  25. private:
  26. int x;
  27. int timerId;
  28. static const int N_OF_BRICKS = 30;
  29. static const int DELAY = 10;
  30. static const int BOTTOM_EDGE = 400;
  31. Ball *ball;
  32. Paddle *paddle;
  33. Brick *bricks[N_OF_BRICKS];
  34. bool gameOver;
  35. bool gameWon;
  36. bool gameStarted;
  37. bool paused;
  38. };

这是突破对象的头文件。

  1. void keyPressEvent(QKeyEvent *);
  2. void keyReleaseEvent(QKeyEvent *);

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

  1. int x;
  2. int timerId;

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

  1. static const int N_OF_BRICKS = 30;

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

  1. static const int DELAY = 10;

DELAY常数控制游戏的速度。

  1. static const int BOTTOM_EDGE = 400;

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

  1. Ball *ball;
  2. Paddle *paddle;
  3. Brick *bricks[N_OF_BRICKS];

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

  1. bool gameOver;
  2. bool gameWon;
  3. bool gameStarted;
  4. bool paused;

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

breakout.cpp

  1. #include <QPainter>
  2. #include <QApplication>
  3. #include "breakout.h"
  4. Breakout::Breakout(QWidget *parent)
  5. : QWidget(parent) {
  6. x = 0;
  7. gameOver = false;
  8. gameWon = false;
  9. paused = false;
  10. gameStarted = false;
  11. ball = new Ball();
  12. paddle = new Paddle();
  13. int k = 0;
  14. for (int i=0; i<5; i++) {
  15. for (int j=0; j<6; j++) {
  16. bricks[k] = new Brick(j*40+30, i*10+50);
  17. k++;
  18. }
  19. }
  20. }
  21. Breakout::~Breakout() {
  22. delete ball;
  23. delete paddle;
  24. for (int i=0; i<N_OF_BRICKS; i++) {
  25. delete bricks[i];
  26. }
  27. }
  28. void Breakout::paintEvent(QPaintEvent *e) {
  29. Q_UNUSED(e);
  30. QPainter painter(this);
  31. if (gameOver) {
  32. finishGame(&painter, "Game lost");
  33. } else if(gameWon) {
  34. finishGame(&painter, "Victory");
  35. }
  36. else {
  37. drawObjects(&painter);
  38. }
  39. }
  40. void Breakout::finishGame(QPainter *painter, QString message) {
  41. QFont font("Courier", 15, QFont::DemiBold);
  42. QFontMetrics fm(font);
  43. int textWidth = fm.width(message);
  44. painter->setFont(font);
  45. int h = height();
  46. int w = width();
  47. painter->translate(QPoint(w/2, h/2));
  48. painter->drawText(-textWidth/2, 0, message);
  49. }
  50. void Breakout::drawObjects(QPainter *painter) {
  51. painter->drawImage(ball->getRect(), ball->getImage());
  52. painter->drawImage(paddle->getRect(), paddle->getImage());
  53. for (int i=0; i<N_OF_BRICKS; i++) {
  54. if (!bricks[i]->isDestroyed()) {
  55. painter->drawImage(bricks[i]->getRect(), bricks[i]->getImage());
  56. }
  57. }
  58. }
  59. void Breakout::timerEvent(QTimerEvent *e) {
  60. Q_UNUSED(e);
  61. moveObjects();
  62. checkCollision();
  63. repaint();
  64. }
  65. void Breakout::moveObjects() {
  66. ball->autoMove();
  67. paddle->move();
  68. }
  69. void Breakout::keyReleaseEvent(QKeyEvent *e) {
  70. int dx = 0;
  71. switch (e->key()) {
  72. case Qt::Key_Left:
  73. dx = 0;
  74. paddle->setDx(dx);
  75. break;
  76. case Qt::Key_Right:
  77. dx = 0;
  78. paddle->setDx(dx);
  79. break;
  80. }
  81. }
  82. void Breakout::keyPressEvent(QKeyEvent *e) {
  83. int dx = 0;
  84. switch (e->key()) {
  85. case Qt::Key_Left:
  86. dx = -1;
  87. paddle->setDx(dx);
  88. break;
  89. case Qt::Key_Right:
  90. dx = 1;
  91. paddle->setDx(dx);
  92. break;
  93. case Qt::Key_P:
  94. pauseGame();
  95. break;
  96. case Qt::Key_Space:
  97. startGame();
  98. break;
  99. case Qt::Key_Escape:
  100. qApp->exit();
  101. break;
  102. default:
  103. QWidget::keyPressEvent(e);
  104. }
  105. }
  106. void Breakout::startGame() {
  107. if (!gameStarted) {
  108. ball->resetState();
  109. paddle->resetState();
  110. for (int i=0; i<N_OF_BRICKS; i++) {
  111. bricks[i]->setDestroyed(false);
  112. }
  113. gameOver = false;
  114. gameWon = false;
  115. gameStarted = true;
  116. timerId = startTimer(DELAY);
  117. }
  118. }
  119. void Breakout::pauseGame() {
  120. if (paused) {
  121. timerId = startTimer(DELAY);
  122. paused = false;
  123. } else {
  124. paused = true;
  125. killTimer(timerId);
  126. }
  127. }
  128. void Breakout::stopGame() {
  129. killTimer(timerId);
  130. gameOver = true;
  131. gameStarted = false;
  132. }
  133. void Breakout::victory() {
  134. killTimer(timerId);
  135. gameWon = true;
  136. gameStarted = false;
  137. }
  138. void Breakout::checkCollision() {
  139. if (ball->getRect().bottom() > BOTTOM_EDGE) {
  140. stopGame();
  141. }
  142. for (int i=0, j=0; i<N_OF_BRICKS; i++) {
  143. if (bricks[i]->isDestroyed()) {
  144. j++;
  145. }
  146. if (j == N_OF_BRICKS) {
  147. victory();
  148. }
  149. }
  150. if ((ball->getRect()).intersects(paddle->getRect())) {
  151. int paddleLPos = paddle->getRect().left();
  152. int ballLPos = ball->getRect().left();
  153. int first = paddleLPos + 8;
  154. int second = paddleLPos + 16;
  155. int third = paddleLPos + 24;
  156. int fourth = paddleLPos + 32;
  157. if (ballLPos < first) {
  158. ball->setXDir(-1);
  159. ball->setYDir(-1);
  160. }
  161. if (ballLPos >= first && ballLPos < second) {
  162. ball->setXDir(-1);
  163. ball->setYDir(-1*ball->getYDir());
  164. }
  165. if (ballLPos >= second && ballLPos < third) {
  166. ball->setXDir(0);
  167. ball->setYDir(-1);
  168. }
  169. if (ballLPos >= third && ballLPos < fourth) {
  170. ball->setXDir(1);
  171. ball->setYDir(-1*ball->getYDir());
  172. }
  173. if (ballLPos > fourth) {
  174. ball->setXDir(1);
  175. ball->setYDir(-1);
  176. }
  177. }
  178. for (int i=0; i<N_OF_BRICKS; i++) {
  179. if ((ball->getRect()).intersects(bricks[i]->getRect())) {
  180. int ballLeft = ball->getRect().left();
  181. int ballHeight = ball->getRect().height();
  182. int ballWidth = ball->getRect().width();
  183. int ballTop = ball->getRect().top();
  184. QPoint pointRight(ballLeft + ballWidth + 1, ballTop);
  185. QPoint pointLeft(ballLeft - 1, ballTop);
  186. QPoint pointTop(ballLeft, ballTop -1);
  187. QPoint pointBottom(ballLeft, ballTop + ballHeight + 1);
  188. if (!bricks[i]->isDestroyed()) {
  189. if(bricks[i]->getRect().contains(pointRight)) {
  190. ball->setXDir(-1);
  191. }
  192. else if(bricks[i]->getRect().contains(pointLeft)) {
  193. ball->setXDir(1);
  194. }
  195. if(bricks[i]->getRect().contains(pointTop)) {
  196. ball->setYDir(1);
  197. }
  198. else if(bricks[i]->getRect().contains(pointBottom)) {
  199. ball->setYDir(-1);
  200. }
  201. bricks[i]->setDestroyed(true);
  202. }
  203. }
  204. }
  205. }

breakout.cpp文件中,我们有游戏逻辑。

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

在打砖块对象的构造器中,我们实例化了三十个砖块。

  1. void Breakout::paintEvent(QPaintEvent *e) {
  2. Q_UNUSED(e);
  3. QPainter painter(this);
  4. if (gameOver) {
  5. finishGame(&painter, "Game lost");
  6. } else if(gameWon) {
  7. finishGame(&painter, "Victory");
  8. }
  9. else {
  10. drawObjects(&painter);
  11. }
  12. }

根据gameOvergameWon变量,我们要么用消息结束游戏,要么在窗口上绘制游戏对象。

  1. void Breakout::finishGame(QPainter *painter, QString message) {
  2. QFont font("Courier", 15, QFont::DemiBold);
  3. QFontMetrics fm(font);
  4. int textWidth = fm.width(message);
  5. painter->setFont(font);
  6. int h = height();
  7. int w = width();
  8. painter->translate(QPoint(w/2, h/2));
  9. painter->drawText(-textWidth/2, 0, message);
  10. }

finishGame()方法在窗口中心绘制一条最终消息。 它是"Game Over""Victory"QFontMetrics' width()用于计算字符串的宽度。

  1. void Breakout::drawObjects(QPainter *painter) {
  2. painter->drawImage(ball->getRect(), ball->getImage());
  3. painter->drawImage(paddle->getRect(), paddle->getImage());
  4. for (int i=0; i<N_OF_BRICKS; i++) {
  5. if (!bricks[i]->isDestroyed()) {
  6. painter->drawImage(bricks[i]->getRect(), bricks[i]->getImage());
  7. }
  8. }
  9. }

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

  1. void Breakout::timerEvent(QTimerEvent *e) {
  2. Q_UNUSED(e);
  3. moveObjects();
  4. checkCollision();
  5. repaint();
  6. }

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

  1. void Breakout::moveObjects() {
  2. ball->autoMove();
  3. paddle->move();
  4. }

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

  1. void Breakout::keyReleaseEvent(QKeyEvent *e) {
  2. int dx = 0;
  3. switch (e->key()) {
  4. case Qt::Key_Left:
  5. dx = 0;
  6. paddle->setDx(dx);
  7. break;
  8. case Qt::Key_Right:
  9. dx = 0;
  10. paddle->setDx(dx);
  11. break;
  12. }
  13. }

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

  1. void Breakout::keyPressEvent(QKeyEvent *e) {
  2. int dx = 0;
  3. switch (e->key()) {
  4. case Qt::Key_Left:
  5. dx = -1;
  6. paddle->setDx(dx);
  7. break;
  8. case Qt::Key_Right:
  9. dx = 1;
  10. paddle->setDx(dx);
  11. break;
  12. case Qt::Key_P:
  13. pauseGame();
  14. break;
  15. case Qt::Key_Space:
  16. startGame();
  17. break;
  18. case Qt::Key_Escape:
  19. qApp->exit();
  20. break;
  21. default:
  22. QWidget::keyPressEvent(e);
  23. }
  24. }

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

  1. void Breakout::startGame() {
  2. if (!gameStarted) {
  3. ball->resetState();
  4. paddle->resetState();
  5. for (int i=0; i<N_OF_BRICKS; i++) {
  6. bricks[i]->setDestroyed(false);
  7. }
  8. gameOver = false;
  9. gameWon = false;
  10. gameStarted = true;
  11. timerId = startTimer(DELAY);
  12. }
  13. }

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

  1. void Breakout::pauseGame() {
  2. if (paused) {
  3. timerId = startTimer(DELAY);
  4. paused = false;
  5. } else {
  6. paused = true;
  7. killTimer(timerId);
  8. }
  9. }

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

  1. void Breakout::stopGame() {
  2. killTimer(timerId);
  3. gameOver = true;
  4. gameStarted = false;
  5. }

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

  1. void Breakout::checkCollision() {
  2. if (ball->getRect().bottom() > BOTTOM_EDGE) {
  3. stopGame();
  4. }
  5. ...
  6. }

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

  1. for (int i=0, j=0; i<N_OF_BRICKS; i++) {
  2. if (bricks[i]->isDestroyed()) {
  3. j++;
  4. }
  5. if (j == N_OF_BRICKS) {
  6. victory();
  7. }
  8. }

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

  1. if (ballLPos < first) {
  2. ball->setXDir(-1);
  3. ball->setYDir(-1);
  4. }

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

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

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

main.cpp

  1. #include <QApplication>
  2. #include "breakout.h"
  3. int main(int argc, char *argv[]) {
  4. QApplication app(argc, argv);
  5. Breakout window;
  6. window.resize(300, 400);
  7. window.setWindowTitle("Breakout");
  8. window.show();
  9. return app.exec();
  10. }

这是主文件。

Qt4 中的打砖块游戏 - 图1

图:打砖块游戏

这是 Qt4 中的打砖块游戏。