Qt5之QGraphicsItem编写Tetris俄罗斯方块游戏

背景

使用Qt5.12.9的QGraphicsItem来实现俄罗斯方块,现在是C++版本,下来还会有python版本,以及方便的接口,来接入算法,由机器人玩俄罗斯方块。

思路

  • CustomGraphBase类继承自QGraphicsObject,提供必要的虚函数。

  • CustomGraphTetrisBlock类继承自CustomGraphBase,实现最小方块,分边框类型(0)与方块类型(1)。

  • CustomGraphTetrisText类继承自CustomGraphBase,显示文字,类型为5。

  • Tetris类组合CustomGraphTetrisBlock,显示俄罗斯方块。

  • Game类为游戏逻辑控制类。

    该游戏传统的编程方式,是用一个二维数组来控制游戏空间,类似迷宫的方式。其实选择QGraphicsItem来实现就是一种很另类的选择,其实用gdi来做更方便,这种规模,QGraphicsItem没有优势,只是个人学习探索的选择。
    我没有用二维数组来控制游戏空间,而是在边沿上用了一圏CustomGraphTetrisBlock来定义游戏空间,因为所有的items都能方便的在scene上检索到,所以看一个方块是否能移动,就需要检索自己的周围是否已经被其它方块占据。这里有一点,在方块进行旋转的时候,就要判断区分组成自己的block和别人的方块。

效果图

tetris.png

关键代码分析

功能尽量内聚,类CustomGraphTetrisBlock封装小方块,Tetris类组合了Block,封装了俄罗斯方块的绝大部分操作,类Game游戏的整体流程。

CustomGraphBase自定义图元基类

class CustomGraphBase : public QGraphicsObject
{
    Q_OBJECT
public:
    CustomGraphBase();
public:
    virtual QRectF boundingRect() const = 0;  //占位区域,必须准确,才能很好的显示与清除
    virtual int type() const = 0;             
    virtual void relocate() = 0;              //移动,重定位
    virtual bool isActive() { return false; };//未落地的方块
    virtual int getBlockType() { return 0; }; //方块类型,主要区别边沿方块
};

CustomGraphTetrisBlock 最小方块,组成俄罗斯方块的基本元素

paint 重绘操作,需要操作边沿方块,边沿方块只占位,不显示。要注意prepareGeometryChange()函数的使用,不能放在这个函数中,不然会不停的重绘,占用大量CPU资源。具体原理我还没研究透,我将其放到relocateb函数中了。

void CustomGraphTetrisBlock::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget /*= nullptr*/)
{
    if (blockType) {
        painter->drawRoundedRect(
            0,
            0,
            BLOCKSIDEWIDTH,
            BLOCKSIDEWIDTH,
            2, 2
        );
    }
    //prepareGeometryChange();
}

relocate元素重定位,只需将其放到scene上正确的坐标

void CustomGraphTetrisBlock::relocate()
{
    this->setPos(pos * BLOCKSIDEWIDTH);
    prepareGeometryChange();
}

Tetris类,俄罗斯方块类

七类方块的定义

QVector<QVector<int>> SHAPES = {
    {1, 1, 1, 1},
    {0, 1, 1, 1, 0 , 1},
    {1, 1, 1, 0, 0, 0, 1},
    {0, 1, 1, 0, 0, 1, 1},
    {1, 1, 0, 0, 0, 1, 1},
    {0, 1, 1, 0, 1, 1},
    {0, 1, 0, 0, 1, 1, 1} 
};

俄罗斯方块的构建

QVector<int> curShape = SHAPES[shape % SHAPES.size()];
    for (int i = 0; i < curShape.size(); i++) {
        if (curShape[i]) {
            data[1 + i / sideLen][i % sideLen] = true;
            CustomGraphTetrisBlock* block = new CustomGraphTetrisBlock(pos + QPoint(i % sideLen, 1 + i / sideLen), 2, shape);
            blocks.push_back(block);   //存储组成该方块的所有元素,在落到底之前需要由Tetris类控制其运动
            MainWindow::GetApp()->GetScene()->addItem(block);                                   //加入block到scene,显示方块
        }
    }

hasTetrisBlock函数检测位置上是否有方块

CustomGraphTetrisBlock* Tetris::hasTetrisBlock(int x, int y)
{
    auto items = MainWindow::GetApp()->GetScene()->items(QPointF((x + 0.5) * BLOCKSIDEWIDTH, (y + 0.5) * BLOCKSIDEWIDTH));
    foreach (auto al , items)
    {
        if (!(((CustomGraphBase*)al)->isActive()) && (((CustomGraphBase*)al)->type()) == TETRISBLOCKTYPE) {      //要区别组合俄罗斯方块本身的block与其它的block
            return (CustomGraphTetrisBlock*)al;  //返回方块,提供给清除行操作用
        }
    }
    return nullptr;
}

rotate函数进行俄罗斯方块的旋转

bool Tetris::rotate()
{
    int i, j, t, lenHalf = sideLen / 2, lenJ;
    for (i = 0; i < lenHalf; i++)
    {
        lenJ = sideLen - i - 1;
        for (j = i; j < lenJ; j++)
        {        //先行判断是否能旋转,要移动的点不为0时,判断目标点是否已经有block存在
            int lenI = sideLen - j - 1;
            if (data[i][j] && this->hasTetrisBlock(pos.x() + lenJ, pos.y() + j) ||
                data[lenI][i] && this->hasTetrisBlock(pos.x() + j, pos.y() + i) ||
                data[lenJ][lenI] && this->hasTetrisBlock(pos.x() + i, pos.y() + lenI) ||
                data[j][lenJ] && this->hasTetrisBlock(pos.x() + lenI, pos.y() + lenJ)){
                return false;
            }
        }
    }
    for (i = 0; i < lenHalf; i++)
    {       //选择了顺时针90度旋转,使用了螺旋移动算法,网上可以容易搜索到说明。
        lenJ = sideLen - i - 1;
        for (j = i; j < lenJ; j++)
        {
            int lenI = sideLen - j - 1;
            t = data[i][j];
            data[i][j] = data[lenI][i];
            data[lenI][i] = data[lenJ][lenI];
            data[lenJ][lenI] = data[j][lenJ];
            data[j][lenJ] = t;
        }
    }
    this->relocate();
    return true;
}

cleanRow函数实现行清除

int Tetris::cleanRow()
{       //该清除算法效率不高,是以一行来处理的,这块以后可以优化。
    int h = 19, levelCount = 0;
    while (h >= 0) {
        int count = 0;
        for (int i = 0; i < 10; i++) {  //判断是否行满
            if (!this->hasTetrisBlock(i, h)) {
                count++;
            }
        }
        if (count == 0) {               //行满,需要清除并整体下移
            int level = h;
            levelCount++;
            bool first = true;
            while (level >= 0) {
                int ct = 0;
                for (int j = 0; j < 10; j++) {
                    if(first)      //第一个外循环删除满行上的图元,后面是整体下移
                        this->erase(j, level);
                    CustomGraphTetrisBlock* block = this->hasTetrisBlock(j, level - 1);
                    if (!block) {
                        ct++;
                    }
                    else {
                        block->relocate(QPoint(j, level));     //下移一个位置
                    }
                }
                first = false;
                if (ct == 10) {           //一行上都没有图元,工作完成,提前结束
                    break;
                }
                else {
                    level--;
                }
            }
        }
        else if (count == 10) {
            break;
        }
        else {
            h--;
        }
    }
    return levelCount;
}

源代码及运行方法

项目采用cmake组织,请安装cmake3.10以上版本。

cmake -Bbuild .
cd build
cmake --build . --config Release

注:本项目采用方案能跨平台运行,已经适配过windows,linux,mac。

源代码:

https://gitee.com/zhoutk/qtetris.git

https://gitee.com/zhoutk/qtdemo/tree/master/tetrisGraphicsItem

https://github.com/zhoutk/qtDemo/tree/master/tetrisGraphicsItem
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容