Qt 项目实战 | 俄罗斯方块
- Qt 项目实战 | 俄罗斯方块
- 游戏架构
- 实现游戏逻辑
- 游戏流程
- 实现基本游戏功能
- 设计小方块
- 设计方块组
- 添加游戏场景
- 添加主函数
- 测试
- 踩坑点1:rotate 失效
- 踩坑点2:items 方法报错
- 踩坑点3:setCodecForTr 失效
- 踩坑点4:不要在中文路径下运行 Qt 项目
- 踩坑点5:multiple definition of `qMain(int, char**)'
- 测试效果
官方博客:https://www.yafeilinux.com/
Qt开源社区:https://www.qter.org/
参考书:《Qt 及 Qt Quick 开发实战精解》
Qt 项目实战 | 俄罗斯方块
开发环境:Qt Creator 4.6.2 Based on Qt 5.9.6
游戏架构
本项目由三个类构成:
- OneBox 类:继承自 QGraphicsObject 类。表示小正方形,可以使用信号与槽机制和属性动画。
- BoxGroup 类:继承自 QObject 类和 QGraphicsItemGroup 类。表示游戏中的方块图形,可以使用信号与槽机制,实现了方块图形的创建、移动和碰撞检测。
- MyView 类:实现了游戏场景。
游戏场景示意图:
实现游戏逻辑
游戏流程
游戏流程图:
七种方块图形:
方块组移动和旋转:
- 碰撞检测:对每一个小方块都使用函数来获取与它们碰撞的图形项的数目,如果数目大于 1,说明已经发生了碰撞。
- 游戏结束:当一个新的方块组出现时,就立即对它进行碰撞检测,如果发现了碰撞,说明游戏结束,这时由方块组发射游戏结束信号。
- 消除满行:游戏开始后,每当出现新的方块前,都判断游戏移动区域的每一行是否已经拥有了满行的小方块。若满行,则销毁该行的所有小方块,然后让该行上面的方块都下移一格。
实现基本游戏功能
新建空的 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_OBJECT
private: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());// 将满行列表清空为 0rows.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 provided
qgraphicsscene.h:159:28: note: candidate function not viable: requires at most 4 arguments, but 5 were provided
qgraphicsscene.h:160:28: note: candidate function not viable: requires at most 4 arguments, but 5 were provided
qgraphicsscene.h:161:28: note: candidate function not viable: requires at most 4 arguments, but 5 were provided
qgraphicsscene.h:175:35: note: candidate function not viable: requires at least 6 arguments, but 5 were provided
qgraphicsscene.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 函数已经失效了,我们改成:
QTextCodec::setCodecForLocale(QTextCodec::codecForName("utf-8"));
这个视个人电脑使用的编码决定。
踩坑点4:不要在中文路径下运行 Qt 项目
就是这样,喵~
踩坑点5:multiple definition of `qMain(int, char**)’
报错信息:
error: multiple definition of `qMain(int, char**)'
这是在 pro 文件中出的问题,频繁的添加以及移除文件,导致 HEADERS 以及 SOURCES 中会重复添加。
这里 main.cpp 重复了,删掉一个即可。