Qt5 中的 Breakout 游戏
最后修改于 2023 年 10 月 18 日
在 Qt5 教程的这一部分中,我们将创建一个简单的 Breakout 游戏克隆。
Breakout 是 Atari Inc. 开发的一款街机游戏。该游戏于 1976 年创建。在这个游戏中,玩家移动一个挡板并弹射一个球。目标是摧毁窗口顶部的砖块。
开发
在我们的游戏中,我们有一个挡板,一个球和三十个砖块。使用计时器来创建一个游戏周期。我们不使用角度,我们只是简单地改变方向:上、下、左和右。代码的灵感来自于 PyBreakout 游戏,该游戏由 Nathan Dawson 在 PyGame 库中开发。
这个游戏有意设计得简单。没有奖励、关卡或分数。这样更容易理解。
Qt5 库是为创建计算机应用程序而开发的。然而,它也可以用于创建游戏。开发一个电脑游戏是学习更多关于 Qt5 的好方法。
#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
是代表挡板对象初始坐标的常量。
#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
将挡板移动到其初始位置。
#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。
#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
标志被设置,则不会在窗口上绘制砖块。
#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
变量存储球的移动方向。
#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
方法在每个游戏周期被调用,用于在屏幕上移动球。如果它击中边界,则球的方向会发生变化。如果球穿过底部边缘,则球不会反弹回来——游戏结束。
#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; };
这是 Breakout 对象的头文件。
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;
这四个变量代表了游戏的各种状态。
#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
方法在窗口中心绘制一条最终消息。它可以是“游戏失败”或“胜利”。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; } }
当玩家释放 Left 光标键或 Right 光标键时,我们将挡板的 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
方法中,我们监听与我们的游戏相关的按键事件。Left 和 Right 光标键移动挡板对象。它们设置 dx
变量,该变量随后添加到挡板的 x 坐标。 P 键暂停游戏,Space 键开始游戏。 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 方向;它向下移动。
#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 中的 Breakout 游戏。