官方博客: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 += 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 函数已经失效了,我们改成:

// 解决 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();
// 可能以前返回主菜单时隐藏了boxGroup
boxGroup->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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐