在 Qt5 教程的这一部分中,我们创建了一个简单的打砖块游戏克隆。
打砖块是 Atari Inc. 开发的一款街机游戏。该游戏创建于 1976 年。在该游戏中,玩家移动桨叶并弹跳球。 目的是销毁窗口顶部的砖块。
开发
在我们的游戏中,我们只有一个桨,一个球和三十个砖头。 计时器用于创建游戏周期。 我们不使用角度,而是仅更改方向:顶部,底部,左侧和右侧。 该代码的灵感来自 Nathan Dawson 在 PyGame 库中开发的 PyBreakout 游戏。
游戏是故意简单的。 没有奖金,等级或分数。 这样更容易理解。
Qt5 库用于创建计算机应用。 但是,它也可以用于创建游戏。 开发计算机游戏是学习有关 Qt5 的好方法。
paddle.h
#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
#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;}
桨板可以向右或向左移动。
Paddle::Paddle() {dx = 0;image.load("paddle.png");rect = image.rect();resetState();}
在构造器中,我们启动dx变量并加载桨图像。 我们得到图像矩形并将图像移动到其初始位置。
void Paddle::move() {int x = rect.x() + dx;int y = rect.top();rect.moveTo(x, y);}
move()方法移动桨的矩形。 移动方向由dx变量控制。
void Paddle::resetState() {rect.moveTo(INITIAL_X, INITIAL_Y);}
resetState()将拨片移动到其初始位置。
brick.h
#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
#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类代表砖对象。
Brick::Brick(int x, int y) {image.load("brickie.png");destroyed = false;rect = image.rect();rect.translate(x, y);}
砖的构造器加载其图像,启动destroyed标志,然后将图像移至其初始位置。
bool Brick::isDestroyed() {return destroyed;}
砖块具有destroyed标志。 如果设置了destroyed标志,则不会在窗口上绘制砖块。
ball.h
#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
#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类表示球对象。
xdir = 1;ydir = -1;
开始时,球向东北方向移动。
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
#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;};
这是突破对象的头文件。
void keyPressEvent(QKeyEvent *);void keyReleaseEvent(QKeyEvent *);
使用光标键控制桨。 在游戏中,我们监听按键和按键释放事件。
int x;int timerId;
x变量存储桨的当前 x 位置。 timerId用于识别计时器对象。 当我们暂停游戏时,这是必需的。
static const int N_OF_BRICKS = 30;
N_OF_BRICKS常数存储游戏中的积木数量。
static const int DELAY = 10;
DELAY常数控制游戏的速度。
static const int BOTTOM_EDGE = 400;
当球通过底边时,比赛结束。
Ball *ball;Paddle *paddle;Brick *bricks[N_OF_BRICKS];
游戏包括一个球,一个球拍和一系列砖块。
bool gameOver;bool gameWon;bool gameStarted;bool paused;
这四个变量代表游戏的各种状态。
breakout.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文件中,我们有游戏逻辑。
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对象的构造器中,我们实例化了三十个砖块。
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变量,我们要么用消息结束游戏,要么在窗口上绘制游戏对象。
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()方法在窗口中心绘制一条最终消息。 它是"Game Over"或"Victory"。 QFontMetrics' width()用于计算字符串的宽度。
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()方法将它们绘制在窗口上。
void Breakout::timerEvent(QTimerEvent *e) {Q_UNUSED(e);moveObjects();checkCollision();repaint();}
在timerEvent()中,我们移动对象,检查球是否与桨或砖相撞,并生成绘图事件。
void Breakout::moveObjects() {ball->autoMove();paddle->move();}
moveObjects()方法移动球和桨对象。 他们自己的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;}}
当播放器释放左光标键或右光标键时,我们将板的dx变量设置为零。 结果,桨停止运动。
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键退出应用。
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()方法启动计时器。
void Breakout::pauseGame() {if (paused) {timerId = startTimer(DELAY);paused = false;} else {paused = true;killTimer(timerId);}}
pauseGame()用于暂停和开始暂停的游戏。 状态由paused变量控制。 我们还存储计时器的 ID。 为了暂停游戏,我们使用killTimer()方法终止计时器。 要重新启动它,我们调用startTimer()方法。
void Breakout::stopGame() {killTimer(timerId);gameOver = true;gameStarted = false;}
在stopGame()方法中,我们终止计时器并设置适当的标志。
void Breakout::checkCollision() {if (ball->getRect().bottom() > BOTTOM_EDGE) {stopGame();}...}
在checkCollision()方法中,我们对游戏进行碰撞检测。 如果球撞到底边,则比赛结束。
for (int i=0, j=0; i<N_OF_BRICKS; i++) {if (bricks[i]->isDestroyed()) {j++;}if (j == N_OF_BRICKS) {victory();}}
我们检查了多少砖被破坏了。 如果我们摧毁了所有积木,我们将赢得这场比赛。
if (ballLPos < first) {ball->setXDir(-1);ball->setYDir(-1);}
如果球碰到了桨的第一部分,我们会将球的方向更改为西北。
if(bricks[i]->getRect().contains(pointTop)) {ball->setYDir(1);}
如果球撞击砖的底部,我们将改变球的 y 方向; 它下降了。
main.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();}
这是主文件。

图:打砖块游戏
这是 Qt5 中的打砖块游戏。
