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 += widgets
TARGET = 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());
// 将满行列表清空为 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 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 重复了,删掉一个即可。