Qt 项目实战 | 俄罗斯方块
Qt 项目实战 | 俄罗斯方块游戏架构实现游戏逻辑游戏流程实现基本游戏功能设计小方块设计方块组添加游戏场景添加主函数 测试踩坑点1:rotate 失效踩坑点2:items 方法报错踩坑点3:setCodecForTr 失效踩坑点4:不要在中文路径下运行 Qt 项目踩坑点5:multiple definition of `qMain(int, char**)'测试效果 游戏优化添加满行销毁动画添加游戏级别设置添加游戏控制按钮和面板踩坑点1:error: no matching function for call to 'QGraphicsTextItem::QGraphicsTextItem(int, QGraphicsScene*&)'踩坑点2:error: no matching function for call to 'QGraphicsScene::items(int, int, int, int, Qt::ItemSelectionMode)' 添加背景音乐和音效添加程序启动画面 运行效果资源下载
官方博客:https://www.yafeilinux.com/
Qt开源社区:https://www.qter.org/
参考书:《Qt 及 Qt Quick 开发实战精解》
Qt 项目实战 | 俄罗斯方块
开发环境:Qt Creator 4.6.2 Based on Qt 5.9.6
游戏架构
在这个游戏中,有一个区域用来摆放方块,该区域宽为10,高为20,以小正方形为单位,它可以看作是拥有20行10列的一个网格。标准的游戏中一共有7种方块,它们都是由4个小正方形组成的规则图形,依据形状分别用字母I、J、L、O、S、T和Z来命名。
这里使用图形视图框架来实现整个游戏的设计。小正方形由OneBox来表示,它继承自QGraphicsObject类,之所以继承自这个类,是因为这样就可以使用信号和槽机制,话可以使用属性动画。小正方形就是一个宽和高都为20像素的正方形图形项。游戏中的方块游戏由方块组BoxGroup类来实现,继承自QObject和QGraphicsItemGroup类,这样该类也可以使用信号和槽机制。方块组是一个宽和高都是80像素的图形项组,其中包含了4个小方块,通过设置小方块的位置来实现7种标准的方块图形。它们的形状和位置如下图,在BoxGroup类中实现了方块图形的创建、移动和碰撞检测。
本项目由三个类构成:
OneBox 类:继承自 QGraphicsObject 类。表示小正方形,可以使用信号与槽机制和属性动画。BoxGroup 类:继承自 QObject 类和 QGraphicsItemGroup 类。表示游戏中的方块图形,可以使用信号与槽机制,实现了方块图形的创建、移动和碰撞检测。MyView 类:实现了游戏场景。整个游戏场景宽800像素,高500像素。方块移动区域宽200像素,高400像素,纵向每20个像素被视作一行,共有20行;横行也是每20个像素视作一列,所以共有10列,该区域可以看作一个由20行10列20×20像素的方格组成的网格。方块组在方块移动区域的初始位置为上方正中间,但方块组的最上方一行小正方形在方块移动区域以外,这样可以保证方块组完全出现在移动区域的最上方,方块组每移动一次,就是移动一个方格的位置。场景还设置了下一个要出现方块的提示方块、游戏暂停等控制按钮和游戏分数级别的显示文本。
游戏场景示意图:
实现游戏逻辑
当游戏开始后,首先创建一个新的方块组,并将其添加到场景中的方块移动区域上方。然后进行碰撞检测,如果这时已经发生了碰撞,那么游戏结束;如果没有发生碰撞,就可以使用键盘的方向键对其进行旋转变形或者左右移动。当到达指定事件时方块组会自动下移一个方格,这时再次判断是否发生碰撞,如果发生了碰撞,先消除满行的方格,然后出现新的方块组,并继续进行整个流程。其中方程块的移动、旋转、碰撞检测等都在BoxGroup类中进行;游戏的开始、结束、出现新的方程组、消除满行等都在MyView类中进行。
游戏流程
游戏流程图:
七种方块图形:
方块组的左移、右移、下移和旋转都是先进行该操作,然后判断是否发生碰撞,比如发生了碰撞就再进行反向操作。比如,使用方向键左移方块组,那么就先将方块组左移一格,然后进行碰撞检测,看是否与边界线或者其他方块碰撞了,如果发生了碰撞,那么就再移过来,即右移一个。
方块组移动和旋转:
碰撞检测:对于方块组的碰撞检测,其实是使用方块组中的4个小方块来进行的,这样就不用再为每个方块图形都设置一个碰撞检测时使用的形状。要进行碰撞检测时,对每一个小方块都使用函数来获取与它们碰撞的图形项的数目,因为现在小方块在方块组中,所以应该只有方块组与它们碰撞了(由于我们对小方块的形状进行了设置,所以挨着的四个小方块相互间不会被检测出发生了碰撞),也就是说与它们碰撞的图形项数目应该不会大于1,如果有哪个小方块发现与它碰撞的图形项的数目大于1,那么说明已经发生了碰撞。
游戏结束:当一个新的方块组出现时,就立即对齐进行碰撞检测,如果它一出现就与其他方块发生了碰撞,说明游戏已经结束了,这时由方块组发射游戏结束信号。
消除满行:游戏开始后,每当出现一个新的方块以前,都判断游戏移动区域的每一行是否已经拥有10个小方块。如果有一行已经拥有了10个小方块,说明改行已满,那么就销毁该行的所有小方块,然后让该行上面的所有小方块都下移一格。
实现基本游戏功能
新建空的 Qt 项目,项目名 myGame。
myGame.pro 中新增代码:
QT += widgetsTARGET = myGame
这也是个踩坑点,在这里提前说了。
添加资源文件,名称为 myImages,添加图片:
设计小方块
新建 mybox.h,添加 OneBox 类的定义:
#ifndef MYBOX_H#define MYBOX_H#include <QGraphicsItemGroup>#include <QGraphicsObject>// 小方块类class OneBox : public QGraphicsObject{private: QColor brushColor;public: OneBox(const QColor& color = Qt::red); QRectF boundingRect() const; void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget); QPainterPath shape() const;};#endif // MYBOX_H
新建 mybox.cpp,添加 OneBox 类的实现代码:
#include "mybox.h"#include <QPainter>OneBox::OneBox(const QColor& color) { brushColor = color; }QRectF OneBox::boundingRect() const{ qreal penWidth = 1; return QRectF(-10 - penWidth / 2, -10 - penWidth / 2, 20 + penWidth, 20 + penWidth);}void OneBox::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){ // 为小方块使用贴图 painter->drawPixmap(-10, -10, 20, 20, QPixmap(":/images/box.gif")); painter->setBrush(brushColor); QColor penColor = brushColor; // 将颜色的透明度降低 penColor.setAlpha(20); painter->setPen(penColor); painter->drawRect(-10, -10, 20, 20);}QPainterPath OneBox::shape() const{ QPainterPath path; // 形状比边框矩形小 0.5 像素,这样方块组中的小方块才不会发生碰撞 path.addRect(-9.5, -9.5, 19, 19); return path;}
设计方块组
在 mybox.h 中添加头文件:
#include <QGraphicsItemGroup>
再添加 BoxGroup 类的定义:
// 方块组类class BoxGroup : public QObject, public QGraphicsItemGroup{ Q_OBJECTprivate: BoxShape currentShape; QTransform oldTransform; QTimer* timer;protected: void keyPressEvent(QKeyEvent* event);public: enum BoxShape { IShape, JShape, LShape, OShape, SShape, TShape, ZShape, RandomShape }; BoxGroup(); QRectF boundingRect() const; bool isColliding(); void createBox(const QPointF& point = QPointF(0, 0), BoxShape shape = RandomShape); void clearBoxGroup(bool destroyBox = false); BoxShape getCurrentShape() { return currentShape; }signals: void needNewBox(); void gameFinished();public slots: void moveOneStep(); void startTimer(int interval); void stopTimer();};
到 mybox.cpp 中添加头文件:
#include <QKeyEvent>#include <QTimer>
添加 BoxGroup 类的实现代码:
// 方块组类void BoxGroup::keyPressEvent(QKeyEvent* event){ switch (event->key()) { case Qt::Key_Down: moveBy(0, 20); if (isColliding()) { moveBy(0, -20); // 将小方块从方块组中移除到场景中 clearBoxGroup(); // 需要显示新的方块 emit needNewBox(); } break; case Qt::Key_Left: moveBy(-20, 0); if (isColliding()) moveBy(20, 0); break; case Qt::Key_Right: moveBy(20, 0); if (isColliding()) moveBy(-20, 0); break; case Qt::Key_Up: rotate(90); if (isColliding()) rotate(-90); break; // 空格键实现坠落 case Qt::Key_Space: moveBy(0, 20); while (!isColliding()) { moveBy(0, 20); } moveBy(0, -20); clearBoxGroup(); emit needNewBox(); break; }}BoxGroup::BoxGroup(){ setFlags(QGraphicsItem::ItemIsFocusable); // 保存变换矩阵,当 BoxGroup 进行旋转后,可以使用它来进行恢复 oldTransform = transform(); timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(moveOneStep())); currentShape = RandomShape;}QRectF BoxGroup::boundingRect() const{ qreal penWidth = 1; return QRectF(-40 - penWidth / 2, -40 - penWidth / 2, 80 + penWidth, 80 + penWidth);}// 碰撞检测bool BoxGroup::isColliding(){ QList<QGraphicsItem*> itemList = childItems(); QGraphicsItem* item; // 使用方块组中的每一个小方块来进行判断 foreach (item, itemList) { if (item->collidingItems().count() > 1) return true; } return false;}// 创建方块void BoxGroup::createBox(const QPointF& point, BoxShape shape){ static const QColor colorTable[7] = { QColor(200, 0, 0, 100), QColor(255, 200, 0, 100), QColor(0, 0, 200, 100), QColor(0, 200, 0, 100), QColor(0, 200, 255, 100), QColor(200, 0, 255, 100), QColor(150, 100, 100, 100) }; int shapeID = shape; if (shape == RandomShape) { // 产生 0-6 之间的随机数 shapeID = qrand() % 7; } QColor color = colorTable[shapeID]; QList<OneBox*> list; //恢复方块组的变换矩阵 setTransform(oldTransform); for (int i = 0; i < 4; ++i) { OneBox* temp = new OneBox(color); list << temp; addToGroup(temp); } switch (shapeID) { case IShape: currentShape = IShape; list.at(0)->setPos(-30, -10); list.at(1)->setPos(-10, -10); list.at(2)->setPos(10, -10); list.at(3)->setPos(30, -10); break; case JShape: currentShape = JShape; list.at(0)->setPos(10, -10); list.at(1)->setPos(10, 10); list.at(2)->setPos(-10, 30); list.at(3)->setPos(10, 30); break; case LShape: currentShape = LShape; list.at(0)->setPos(-10, -10); list.at(1)->setPos(-10, 10); list.at(2)->setPos(-10, 30); list.at(3)->setPos(10, 30); break; case OShape: currentShape = OShape; list.at(0)->setPos(-10, -10); list.at(1)->setPos(10, -10); list.at(2)->setPos(-10, 10); list.at(3)->setPos(10, 10); break; case SShape: currentShape = SShape; list.at(0)->setPos(10, -10); list.at(1)->setPos(30, -10); list.at(2)->setPos(-10, 10); list.at(3)->setPos(10, 10); break; case TShape: currentShape = TShape; list.at(0)->setPos(-10, -10); list.at(1)->setPos(10, -10); list.at(2)->setPos(30, -10); list.at(3)->setPos(10, 10); break; case ZShape: currentShape = ZShape; list.at(0)->setPos(-10, -10); list.at(1)->setPos(10, -10); list.at(2)->setPos(10, 10); list.at(3)->setPos(30, 10); break; default: break; } // 设置位置 setPos(point); // 如果开始就发生碰撞,说明已经结束游戏 if (isColliding()) { stopTimer(); emit gameFinished(); }}// 删除方块组中的所有小方块void BoxGroup::clearBoxGroup(bool destroyBox){ QList<QGraphicsItem*> itemList = childItems(); QGraphicsItem* item; foreach (item, itemList) { removeFromGroup(item); if (destroyBox) { OneBox* box = (OneBox*)item; box->deleteLater(); } }}// 向下移动一步void BoxGroup::moveOneStep(){ moveBy(0, 20); if (isColliding()) { moveBy(0, -20); // 将小方块从方块组中移除到场景中 clearBoxGroup(); emit needNewBox(); }}// 开启定时器void BoxGroup::startTimer(int interval) { timer->start(interval); }// 停止定时器void BoxGroup::stopTimer() { timer->stop(); }
添加游戏场景
新建一个 C++ 类,类名为 MyView,基类为 GraphicsView,继承自 QWidget:
更改 myview.h:
#ifndef MYVIEW_H#define MYVIEW_H#include <QGraphicsView>#include <QWidget>class BoxGroup;class MyView : public GraphicsView{private: BoxGroup* boxGroup; BoxGroup* nextBoxGroup; QGraphicsLineItem* topLine; QGraphicsLineItem* bottomLine; QGraphicsLineItem* leftLine; QGraphicsLineItem* rightLine; qreal gameSpeed; QList<int> rows; void initView(); void initGame(); void updateScore(const int fullRowNum = 0);public: explicit MyView(QWidget* parent = 0);public slots: void startGame(); void clearFullRows(); void moveBox(); void gameOver();};#endif // MYVIEW_H
更改 myview.cpp:
#include "myview.h"#include <QIcon>#include "mybox.h"// 游戏的初始速度static const qreal INITSPEED = 500;// 初始化游戏界面void MyView::initView(){ // 使用抗锯齿渲染 setRenderHint(QPainter::Antialiasing); // 设置缓存背景,这样可以加快渲染速度 setCacheMode(CacheBackground); setWindowTitle(tr("MyBox方块游戏")); setWindowIcon(QIcon(":/images/icon.png")); setMinimumSize(810, 510); setMaximumSize(810, 510); // 设置场景 QGraphicsScene* scene = new QGraphicsScene; scene->setSceneRect(5, 5, 800, 500); scene->setBackgroundBrush(QPixmap(":/images/background.png")); setScene(scene); // 方块可移动区域的四条边界线 topLine = scene->addLine(197, 47, 403, 47); bottomLine = scene->addLine(197, 453, 403, 453); leftLine = scene->addLine(197, 47, 197, 453); rightLine = scene->addLine(403, 47, 403, 453); // 当前方块组和提示方块组 boxGroup = new BoxGroup; connect(boxGroup, SIGNAL(needNewBox()), this, SLOT(clearFullRows())); connect(boxGroup, SIGNAL(gameFinished()), this, SLOT(gameOver())); scene->addItem(boxGroup); nextBoxGroup = new BoxGroup; scene->addItem(nextBoxGroup); startGame();}// 初始化游戏void MyView::initGame(){ boxGroup->createBox(QPointF(300, 70)); boxGroup->setFocus(); boxGroup->startTimer(INITSPEED); gameSpeed = INITSPEED; nextBoxGroup->createBox(QPointF(500, 70));}// 更新分数void MyView::updateScore(const int fullRowNum) {}MyView::MyView(QWidget* parent) : QGraphicsView(parent) { initView(); }// 开始游戏void MyView::startGame() { initGame(); }// 清空满行void MyView::clearFullRows(){ // 获取比一行方格较大的矩形中包含的所有小方块 for (int y = 429; y > 50; y -= 20) { QList<QGraphicsItem*> list = scene()->items(199, y, 202, 22, Qt::ContainsItemShape); // 如果该行已满 if (list.count() == 10) { foreach (QGraphicsItem* item, list) { OneBox* box = (OneBox*)item; box->deleteLater(); } // 保存满行的位置 rows << y; } } if (rows.count() > 0) { // 如果有满行,下移满行上面的各行再出现新的方块组 moveBox(); } else // 如果没有满行,则直接出现新的方块组 { boxGroup->createBox(QPointF(300, 70), nextBoxGroup->getCurrentShape()); // 清空并销毁提示方块组中的所有小方块 nextBoxGroup->clearBoxGroup(true); nextBoxGroup->createBox(QPointF(500, 70)); }}// 下移满行上面的所有小方块void MyView::moveBox(){ // 从位置最靠上的满行开始 for (int i = rows.count(); i > 0; i--) { int row = rows.at(i - 1); foreach (QGraphicsItem* item, scene()->items(199, 49, 202, row - 47, Qt::ContainsItemShape)) { item->moveBy(0, 20); } } // 更新分数 updateScore(rows.count()); // 将满行列表清空为 0 rows.clear(); // 等所有行下移以后再出现新的方块组 boxGroup->createBox(QPointF(300, 70), nextBoxGroup->getCurrentShape()); nextBoxGroup->clearBoxGroup(true); nextBoxGroup->createBox(QPointF(500, 70));}// 游戏结束void MyView::gameOver() {}
添加主函数
新建 main.cpp,添加代码:
#include <QApplication>#include <QTextCodec>#include <QTime>#include "myview.h"int main(int argc, char* argv[]){ QApplication app(argc, argv); QTextCodec::setCodecForTr(QTextCodec::codecForLocale()); // 设置随机数的初始值 qsrand(QTime(0, 0, 0).secsTo(QTime::currentTime())); MyView view; view.show(); return app.exec();}
测试
运行程序。
果不其然的报错了。
主要是一些 Qt4 和 Qt5 的差别带来的问题。
踩坑点1:rotate 失效
函数 void BoxGroup::keyPressEvent(QKeyEvent* event) 原代码:
void BoxGroup::keyPressEvent(QKeyEvent *event){ switch (event->key()) { case Qt::Key_Down : moveBy(0, 20); if (isColliding()) { moveBy(0, -20); // 将小方块从方块组中移除到场景中 clearBoxGroup(); // 需要显示新的方块 emit needNewBox(); } break; case Qt::Key_Left : moveBy(-20, 0); if (isColliding()) moveBy(20, 0); break; case Qt::Key_Right : moveBy(20, 0); if (isColliding()) moveBy(-20, 0); break; case Qt::Key_Up : rotate(90); if(isColliding()) rotate(-90); break; // 空格键实现坠落 case Qt::Key_Space : moveBy(0, 20); while (!isColliding()) { moveBy(0, 20); } moveBy(0, -20); clearBoxGroup(); emit needNewBox(); break; }}
其中的 rotate 函数失效。
在 Qt5 中,QGraphicsItem::rotate 已经不再使用,而是使用 setRotation。
修改为:
void BoxGroup::keyPressEvent(QKeyEvent* event){ qreal oldRotate; switch (event->key()) { // 下移 case Qt::Key_Down: moveBy(0, 20); if (isColliding()) { moveBy(0, -20); // 将小方块从方块组中移除到场景中 clearBoxGroup(); // 需要显示新的方块 emit needNewBox(); } break; // 左移 case Qt::Key_Left: moveBy(-20, 0); if (isColliding()) moveBy(20, 0); break; // 右移 case Qt::Key_Right: moveBy(20, 0); if (isColliding()) moveBy(-20, 0); break; // 旋转 case Qt::Key_Up: // 在 Qt5 中,QGraphicsItem::rotate 已经不再使用,而是使用 setRotation /* old code */ // rotate(90); // if (isColliding()) // rotate(-90); // break; /* old code */ oldRotate = rotation(); if (oldRotate >= 360) { oldRotate = 0; } setRotation(oldRotate + 90); if (isColliding()) { setRotation(oldRotate - 90); } break; // 空格键实现坠落 case Qt::Key_Space: moveBy(0, 20); while (!isColliding()) { moveBy(0, 20); } moveBy(0, -20); clearBoxGroup(); emit needNewBox(); break; }}
参考博客:Qt及Qt Quick开发实战精解项目二俄罗斯方块 rotate失效方法报错
踩坑点2:items 方法报错
在 void MyView::clearFullRows() 函数里有这样一行代码:
QList<QGraphicsItem*> list = scene()->items(199, y, 202, 22, Qt::ContainsItemShape);
报错信息:
myview.cpp:75:47: error: no matching member function for call to 'items'qgraphicsscene.h:158:28: note: candidate function not viable: requires at most 4 arguments, but 5 were providedqgraphicsscene.h:159:28: note: candidate function not viable: requires at most 4 arguments, but 5 were providedqgraphicsscene.h:160:28: note: candidate function not viable: requires at most 4 arguments, but 5 were providedqgraphicsscene.h:161:28: note: candidate function not viable: requires at most 4 arguments, but 5 were providedqgraphicsscene.h:175:35: note: candidate function not viable: requires at least 6 arguments, but 5 were providedqgraphicsscene.h:156:28: note: candidate function not viable: allows at most single argument 'order', but 5 arguments were provided
大概意思是参数不匹配。
修改为:
QList<QGraphicsItem*> list = scene()->items(199, y, 202, 22, Qt::ContainsItemShape, Qt::AscendingOrder);
新增的一项 Qt::AscendingOrder 的意思是对 QList 的内容正序排序。
参考博客:Qt及Qt Quick开发实战精解项目二俄罗斯方块 items方法报错
踩坑点3:setCodecForTr 失效
在 main.cpp 中有这样一行代码:
QTextCodec::setCodecForTr(QTextCodec::codecForLocale());
这行代码主要解决 Qt 中文乱码的问题。
但是在 Qt5 中 setCodecForTr 函数已经失效了,我们改成:
// 解决 Qt 中文乱码问题QTextCodec* codec = QTextCodec::codecForName("UTF-8");QTextCodec::setCodecForLocale(codec);QTextCodec::setCodecForCStrings(codec);QTextCodec::setCodecForTr(codec);
这个视个人电脑使用的编码决定。
踩坑点4:不要在中文路径下运行 Qt 项目
就是这样,喵~
踩坑点5:multiple definition of `qMain(int, char**)’
报错信息:
error: multiple definition of `qMain(int, char**)'
这是在 pro 文件中出的问题,频繁的添加以及移除文件,导致 HEADERS 以及 SOURCES 中会重复添加。
这里 main.cpp 重复了,删掉一个即可。
测试效果
游戏优化
添加满行销毁动画
在 myview.cpp 中添加头文件:
#include <QPropertyAnimation>#include <QGraphicsBlurEffect>#include <QTimer>
修改 clearFullRows() 函数:
void MyView::clearFullRows(){ // 获取比一行方格较大的矩形中包含的所有小方块 for (int y = 429; y > 50; y -= 20) { // QList<QGraphicsItem*> list = scene()->items(199, y, 202, 22, Qt::ContainsItemShape); QList<QGraphicsItem*> list = scene()->items(199, y, 202, 22, Qt::ContainsItemShape, Qt::AscendingOrder); // 如果该行已满 if (list.count() == 10) { foreach (QGraphicsItem* item, list) { OneBox* box = (OneBox*)item; // box->deleteLater(); QGraphicsBlurEffect* blurEffect = new QGraphicsBlurEffect; box->setGraphicsEffect(blurEffect); QPropertyAnimation* animation = new QPropertyAnimation(box, "scale"); animation->setEasingCurve(QEasingCurve::OutBounce); animation->setDuration(250); animation->setStartValue(4); animation->setEndValue(0.25); animation->start(QAbstractAnimation::DeleteWhenStopped); connect(animation, SIGNAL(finished()), box, SLOT(deleteLater())); } // 保存满行的位置 rows << y; } } // 如果有满行,下移满行上面的各行再出现新的方块组 if (rows.count() > 0) { // moveBox(); QTimer::singleShot(400, this, SLOT(moveBox())); } else // 如果没有满行,则直接出现新的方块组 { boxGroup->createBox(QPointF(300, 70), nextBoxGroup->getCurrentShape()); // 清空并销毁提示方块组中的所有小方块 nextBoxGroup->clearBoxGroup(true); nextBoxGroup->createBox(QPointF(500, 70)); }}
为小方块设置模糊效果,为小方块添加先放大再缩小的属性动画。
使用了只执行一次的定时器,其目的是等待所有小方块都销毁后再移动满行上面的小方块。
添加游戏级别设置
在 myview.h 的 private 中添加两个变量:
QGraphicsTextItem* gameScoreText;QGraphicsTextItem* gameLevelText;
在myview.cpp 的 initView() 函数中调用 startGame() 槽前添加代码:
// 得分文本gameScoreText = new QGraphicsTextItem(0, scene);gameScoreText->setFont(QFont("Times", 20, QFont::Bold));gameScoreText->setPos(650, 350);// 级别文本gameLevelText = new QGraphicsTextItem(0, scene);gameLevelText->setFont(QFont("Times", 30, QFont::Bold));gameLevelText->setPos(20, 150);
注意,上面是书中的代码,是错的,修改为下面代码:
// 得分文本gameScoreText = new QGraphicsTextItem();gameScoreText->setFont(QFont("Times", 20, QFont::Bold));gameScoreText->setPos(650, 350);// 级别文本gameLevelText = new QGraphicsTextItem();gameLevelText->setFont(QFont("Times", 30, QFont::Bold));gameLevelText->setPos(20, 150);scene->addItem(gameLevelText);scene->addItem(gameScoreText);
再到initGame() 中添加代码:
scene()->setBackgroundBrush(QPixmap(":/images/background01.png"));gameScoreText->setHtml(tr("<font color=red>0</font>"));gameLevelText->setHtml(tr("<font color=white>第<br>一<br>幕</font>"));
最后到 updateScore() 函数中添加代码:
// 更新分数void MyView::updateScore(const int fullRowNum){ int score = fullRowNum * 100; int currentScore = gameScoreText->toPlainText().toInt(); currentScore += score; // 显示当前分数 gameScoreText->setHtml(tr("<font color=red>%1</font>").arg(currentScore)); // 判断级别 if (currentScore < 500) { // 第一级,什么都不用做 } else if (currentScore < 1000) { // 第二级 gameLevelText->setHtml(tr("<font color=white>第<br>二<br>幕</font>")); scene()->setBackgroundBrush(QPixmap(":/images/background02.png")); gameSpeed = 300; boxGroup->stopTimer(); boxGroup->startTimer(gameSpeed); } else { // 添加下一个级别的设置 }}
测试:
添加游戏控制按钮和面板
在 myview.h 添加私有槽:
void restartGame();void finishGame();void pauseGame();void returnGame();
在 myview.h 添加私有变量:
QGraphicsWidget *maskWidget; // 遮罩面板// 各种按钮QGraphicsWidget *startButton;QGraphicsWidget *finishButton;QGraphicsWidget *restartButton;QGraphicsWidget *pauseButton;QGraphicsWidget *optionButton;QGraphicsWidget *returnButton;QGraphicsWidget *helpButton;QGraphicsWidget *exitButton;QGraphicsWidget *showMenuButton;// 各种文本QGraphicsTextItem *gameWelcomeText;QGraphicsTextItem *gamePausedText;QGraphicsTextItem *gameOverText;
在 myview.cpp 添加头文件:
#include <QPushButton>#include <QGraphicsProxyWidget>#include <QApplication>#include <QLabel>#include <QFileInfo>
在 initGame() 函数中,删除代码:
startGame();
添加代码:
/*****************下面是2-4中添加的代码,部分代码在书中省略了*************/// 设置初始为隐藏状态topLine->hide();bottomLine->hide();leftLine->hide();rightLine->hide();gameScoreText->hide();gameLevelText->hide();// 黑色遮罩QWidget *mask = new QWidget;mask->setAutoFillBackground(true);mask->setPalette(QPalette(QColor(0, 0, 0, 80)));mask->resize(900, 600);maskWidget = scene->addWidget(mask);maskWidget->setPos(-50, -50);// 设置其Z值为1,这样可以处于Z值为0的图形项上面maskWidget->setZValue(1);// 选项面板QWidget *option = new QWidget;QPushButton *optionCloseButton = new QPushButton(tr("关 闭"), option);QPalette palette;palette.setColor(QPalette::ButtonText, Qt::black);optionCloseButton->setPalette(palette);optionCloseButton->move(120, 300);connect(optionCloseButton, SIGNAL(clicked()), option, SLOT(hide()));option->setAutoFillBackground(true);option->setPalette(QPalette(QColor(0, 0, 0, 180)));option->resize(300, 400);QGraphicsWidget *optionWidget = scene->addWidget(option);optionWidget->setPos(250, 50);optionWidget->setZValue(3);optionWidget->hide();// 帮助面板QWidget *help = new QWidget;QPushButton *helpCloseButton = new QPushButton(tr("关 闭"), help);helpCloseButton->setPalette(palette);helpCloseButton->move(120, 300);connect(helpCloseButton, SIGNAL(clicked()), help, SLOT(hide()));help->setAutoFillBackground(true);help->setPalette(QPalette(QColor(0, 0, 0, 180)));help->resize(300, 400);QGraphicsWidget *helpWidget = scene->addWidget(help);helpWidget->setPos(250, 50);helpWidget->setZValue(3);helpWidget->hide();QLabel *helpLabel = new QLabel(help);helpLabel->setText(tr("<h1><font color=white>yafeilinux作品" "<br>www.yafeilinux.com</font></h1>"));helpLabel->setAlignment(Qt::AlignCenter);helpLabel->move(30, 150);// 游戏欢迎文本gameWelcomeText = new QGraphicsTextItem(0, scene);gameWelcomeText->setHtml(tr("<font color=white>MyBox方块游戏</font>"));gameWelcomeText->setFont(QFont("Times", 30, QFont::Bold));gameWelcomeText->setPos(250, 100);gameWelcomeText->setZValue(2);// 游戏暂停文本gamePausedText = new QGraphicsTextItem(0, scene);gamePausedText->setHtml(tr("<font color=white>游戏暂停中!</font>"));gamePausedText->setFont(QFont("Times", 30, QFont::Bold));gamePausedText->setPos(300, 100);gamePausedText->setZValue(2);gamePausedText->hide();// 游戏结束文本gameOverText = new QGraphicsTextItem(0, scene);gameOverText->setHtml(tr("<font color=white>游戏结束!</font>"));gameOverText->setFont(QFont("Times", 30, QFont::Bold));gameOverText->setPos(320, 100);gameOverText->setZValue(2);gameOverText->hide();// 游戏中使用的按钮QPushButton *button1 = new QPushButton(tr("开 始"));QPushButton *button2 = new QPushButton(tr("选 项"));QPushButton *button3 = new QPushButton(tr("帮 助"));QPushButton *button4 = new QPushButton(tr("退 出"));QPushButton *button5 = new QPushButton(tr("重新开始"));QPushButton *button6 = new QPushButton(tr("暂 停"));QPushButton *button7 = new QPushButton(tr("主 菜 单"));QPushButton *button8 = new QPushButton(tr("返回游戏"));QPushButton *button9 = new QPushButton(tr("结束游戏"));connect(button1, SIGNAL(clicked()), this, SLOT(startGame()));connect(button2, SIGNAL(clicked()), option, SLOT(show()));connect(button3, SIGNAL(clicked()), help, SLOT(show()));connect(button4, SIGNAL(clicked()), qApp, SLOT(quit()));connect(button5, SIGNAL(clicked()), this, SLOT(restartGame()));connect(button6, SIGNAL(clicked()), this, SLOT(pauseGame()));connect(button7, SIGNAL(clicked()), this, SLOT(finishGame()));connect(button8, SIGNAL(clicked()), this, SLOT(returnGame()));connect(button9, SIGNAL(clicked()), this, SLOT(finishGame()));startButton = scene->addWidget(button1);optionButton = scene->addWidget(button2);helpButton = scene->addWidget(button3);exitButton = scene->addWidget(button4);restartButton = scene->addWidget(button5);pauseButton = scene->addWidget(button6);showMenuButton = scene->addWidget(button7);returnButton = scene->addWidget(button8);finishButton = scene->addWidget(button9);startButton->setPos(370, 200);optionButton->setPos(370, 250);helpButton->setPos(370, 300);exitButton->setPos(370, 350);restartButton->setPos(600, 150);pauseButton->setPos(600, 200);showMenuButton->setPos(600, 250);returnButton->setPos(370, 200);finishButton->setPos(370, 250);startButton->setZValue(2);optionButton->setZValue(2);helpButton->setZValue(2);exitButton->setZValue(2);restartButton->setZValue(2);returnButton->setZValue(2);finishButton->setZValue(2);restartButton->hide();finishButton->hide();pauseButton->hide();showMenuButton->hide();returnButton->hide();/*****************上面是2-4中添加的代码,部分代码在书中省略了*************/
在 startGame() 中调用 initGame() 函数前添加代码:
gameWelcomeText->hide();startButton->hide();optionButton->hide();helpButton->hide();exitButton->hide();maskWidget->hide();
在 initGame() 函数的最后添加代码:
restartButton->show();pauseButton->show();showMenuButton->show();gameScoreText->show();gameLevelText->show();topLine->show();bottomLine->show();leftLine->show();rightLine->show();// 可能以前返回主菜单时隐藏了boxGroupboxGroup->show();
修改 gameOver() 槽:
// 游戏结束void MyView::gameOver(){ pauseButton->hide(); showMenuButton->hide(); maskWidget->show(); gameOverText->show(); restartButton->setPos(370, 200); finishButton->show();}
添加其他槽:
// 重新开始游戏void MyView::restartGame(){ maskWidget->hide(); gameOverText->hide(); finishButton->hide(); restartButton->setPos(600, 150); // 销毁提示方块组和当前方块移动区域中的所有小方块 nextBoxGroup->clearBoxGroup(true); boxGroup->clearBoxGroup(); boxGroup->hide(); foreach (QGraphicsItem* item, scene()->items(199, 49, 202, 402, Qt::ContainsItemShape, Qt::AscendingOrder)) { // 先从场景中移除小方块,因为使用deleteLater()是在返回主事件循环后才销毁 // 小方块的,为了在出现新的方块组时不发生碰撞,所以需要先从场景中移除小方块 scene()->removeItem(item); OneBox* box = (OneBox*)item; box->deleteLater(); } initGame();}// 结束当前游戏void MyView::finishGame(){ gameOverText->hide(); finishButton->hide(); restartButton->setPos(600, 150); restartButton->hide(); pauseButton->hide(); showMenuButton->hide(); gameScoreText->hide(); gameLevelText->hide(); topLine->hide(); bottomLine->hide(); leftLine->hide(); rightLine->hide(); nextBoxGroup->clearBoxGroup(true); boxGroup->clearBoxGroup(); boxGroup->hide(); foreach (QGraphicsItem* item, scene()->items(199, 49, 202, 402, Qt::ContainsItemShape)) { OneBox* box = (OneBox*)item; box->deleteLater(); } // 可能是在进行游戏时按下“主菜单”按钮 maskWidget->show(); gameWelcomeText->show(); startButton->show(); optionButton->show(); helpButton->show(); exitButton->show(); scene()->setBackgroundBrush(QPixmap(":/images/background.png"));}// 暂停游戏void MyView::pauseGame(){ boxGroup->stopTimer(); restartButton->hide(); pauseButton->hide(); showMenuButton->hide(); maskWidget->show(); gamePausedText->show(); returnButton->show();}// 返回游戏,处于暂停状态时void MyView::returnGame(){ returnButton->hide(); gamePausedText->hide(); maskWidget->hide(); restartButton->show(); pauseButton->show(); showMenuButton->show(); boxGroup->startTimer(gameSpeed);}
踩坑点1:error: no matching function for call to ‘QGraphicsTextItem::QGraphicsTextItem(int, QGraphicsScene*&)’
报错信息:
error: no matching function for call to 'QGraphicsTextItem::QGraphicsTextItem(int, QGraphicsScene*&)' gameWelcomeText = new QGraphicsTextItem(0, scene);
^
将下面代码:
// 游戏欢迎文本gameWelcomeText = new QGraphicsTextItem(0, scene);gameWelcomeText->setHtml(tr("<font color=white>MyBox方块游戏</font>"));gameWelcomeText->setFont(QFont("Times", 30, QFont::Bold));gameWelcomeText->setPos(250, 100);gameWelcomeText->setZValue(2);// 游戏暂停文本gamePausedText = new QGraphicsTextItem(0, scene);gamePausedText->setHtml(tr("<font color=white>游戏暂停中!</font>"));gamePausedText->setFont(QFont("Times", 30, QFont::Bold));gamePausedText->setPos(300, 100);gamePausedText->setZValue(2);gamePausedText->hide();// 游戏结束文本gameOverText = new QGraphicsTextItem(0, scene);gameOverText->setHtml(tr("<font color=white>游戏结束!</font>"));gameOverText->setFont(QFont("Times", 30, QFont::Bold));gameOverText->setPos(320, 100);gameOverText->setZValue(2);gameOverText->hide();
修改为:
// 游戏欢迎文本gameWelcomeText = new QGraphicsTextItem();gameWelcomeText->setHtml(tr("<font color=white>MyBox方块游戏</font>"));gameWelcomeText->setFont(QFont("Times", 30, QFont::Bold));gameWelcomeText->setPos(250, 100);gameWelcomeText->setZValue(2);// 游戏暂停文本gamePausedText = new QGraphicsTextItem();gamePausedText->setHtml(tr("<font color=white>游戏暂停中!</font>"));gamePausedText->setFont(QFont("Times", 30, QFont::Bold));gamePausedText->setPos(300, 100);gamePausedText->setZValue(2);gamePausedText->hide();// 游戏结束文本gameOverText = new QGraphicsTextItem();gameOverText->setHtml(tr("<font color=white>游戏结束!</font>"));gameOverText->setFont(QFont("Times", 30, QFont::Bold));gameOverText->setPos(320, 100);gameOverText->setZValue(2);gameOverText->hide();scene->addItem(gameWelcomeText);scene->addItem(gamePausedText);scene->addItem(gameOverText);
为了进行游戏时总是当前方块组获得焦点,我们要重写视图的键盘按下事件处理函数。
myview.h 新增代码:
protected: void keyPressEvent(QKeyEvent* event);
然后到 myview.cpp 添加定义:
// 如果正在进行游戏,当键盘按下时总是方块组获得焦点void MyView::keyPressEvent(QKeyEvent* event){ if (pauseButton->isVisible()) boxGroup->setFocus(); else boxGroup->clearFocus(); QGraphicsView::keyPressEvent(event);}
踩坑点2:error: no matching function for call to ‘QGraphicsScene::items(int, int, int, int, Qt::ItemSelectionMode)’
报错信息:
error: no matching function for call to 'QGraphicsScene::items(int, int, int, int, Qt::ItemSelectionMode)' foreach (QGraphicsItem* item, scene()->items(199, 49, 202, 402, Qt::ContainsItemShape))
原因与之前的踩坑点2:items 方法报错相同。
错误代码:
foreach (QGraphicsItem *item, scene()->items(199, 49, 202, 402, Qt::ContainsItemShape)){OneBox *box = (OneBox *)item;box->deleteLater();}
修改为:
foreach (QGraphicsItem *item, scene()->items(199, 49, 202, 402, Qt::ContainsItemShape, Qt::AscendingOrder)){OneBox *box = (OneBox *)item;box->deleteLater();}
添加背景音乐和音效
在 myGame.pro 添加代码:
QT += phonon
报错:
Project ERROR: Unknown module(s) in QT: phonon
因为 Qt5 不支持 phonon,所以此部分省略。
安装 Qt 4.8.6:Qt 4.8.6 的下载与安装
注:实测发现,不用安装 Qt Creator 3.3.0 也能在原来的 Qt Creator 上编写项目,只需要改一下构建。
在 myview.h 添加头文件:
#include <phonon>
添加槽声明:
void aboutToFinish();
添加私有对象定义:
// 音乐部件Phonon::MediaObject *backgroundMusic;Phonon::MediaObject *clearRowSound;
在 myview.cpp 添加代码:
// 声音文件路径static const QString SOUNDPATH = "../myGame/sounds/";
然后到 initView() 函数最后添加代码:
// 设置声音backgroundMusic = new Phonon::MediaObject(this);clearRowSound = new Phonon::MediaObject(this);Phonon::AudioOutput *audio1 = new Phonon::AudioOutput(Phonon::MusicCategory, this);Phonon::AudioOutput *audio2 = new Phonon::AudioOutput(Phonon::MusicCategory, this);Phonon::createPath(backgroundMusic, audio1);Phonon::createPath(clearRowSound, audio2);// 设置音量控制部件,它们显示在选项面板上Phonon::VolumeSlider *volume1 = new Phonon::VolumeSlider(audio1, option);Phonon::VolumeSlider *volume2 = new Phonon::VolumeSlider(audio2, option);QLabel *volumeLabel1 = new QLabel(tr("音乐:"), option);QLabel *volumeLabel2 = new QLabel(tr("音效:"), option);volume1->move(100, 100);volume2->move(100, 200);volumeLabel1->move(60, 105);volumeLabel2->move(60, 205);connect(backgroundMusic, SIGNAL(aboutToFinish()), this, SLOT(aboutToFinish()));// 因为播放完毕后会进入暂停状态,再调用play()将无法进行播放,需要在播放完毕后使其进入停止状态connect(clearRowSound, SIGNAL(finished()), clearRowSound, SLOT(stop()));backgroundMusic->setCurrentSource(Phonon::MediaSource(SOUNDPATH + "background.mp3"));clearRowSound->setCurrentSource(Phonon::MediaSource(SOUNDPATH + "clearRow.mp3"));backgroundMusic->play();
在 initGame() 函数的最后添加代码:
backgroundMusic->setCurrentSource(Phonon::MediaSource(SOUNDPATH + "background01.mp3"));backgroundMusic->play();
在 finishGame() 函数的最后添加代码:
backgroundMusic->setCurrentSource(Phonon::MediaSource(SOUNDPATH + "background.mp3"));backgroundMusic->play();
修改 updateScore() 函数:
// 更新分数void MyView::updateScore(const int fullRowNum){ int score = fullRowNum * 100; int currentScore = gameScoreText->toPlainText().toInt(); currentScore += score; // 显示当前分数 gameScoreText->setHtml(tr("<font color=red>%1</font>").arg(currentScore)); // 判断级别 if (currentScore < 500) { // 第一级,什么都不用做 } else if (currentScore < 1000) { // 第二级 gameLevelText->setHtml(tr("<font color=white>第<br>二<br>幕</font>")); scene()->setBackgroundBrush(QPixmap(":/images/background02.png")); gameSpeed = 300; boxGroup->stopTimer(); boxGroup->startTimer(gameSpeed); if (QFileInfo(backgroundMusic->currentSource().fileName()).baseName() != "background02") { backgroundMusic->setCurrentSource(Phonon::MediaSource(SOUNDPATH + "background02.mp3")); backgroundMusic->play(); } } else { // 添加下一个级别的设置 }}
添加 aboutToFinish() 槽定义:
// 背景音乐将要播放完毕时继续重新播放void MyView::aboutToFinish(){ backgroundMusic->enqueue(backgroundMusic->currentSource());}
添加程序启动画面
在 main.cpp 中添加头文件:
#include <QSplashScreen>
在主函数创建 view 对象前添加代码:
QPixmap pix(":/images/logo.png");QSplashScreen splash(pix);splash.resize(pix.size());splash.show();app.processEvents();
在调用 show() 函数后添加代码:
splash.finish(&view);
运行效果
运行程序,主界面出现前会在屏幕的中心出现启动画面:
开始界面:
点击“帮助”:
点击选项:
游戏界面:
暂停:
游戏结束:
资源下载
GitHub:UestcXiye/Tetris
CSDN:Qt 项目:俄罗斯方块.zip