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 游戏。