Qt5之QGraphicsItem編寫Tetris俄羅斯方塊遊戲

背景

使用Qt5.12.9的QGraphicsItem來實現俄羅斯方塊,如今是C++版本,下來還會有python版本,以及方便的接口,來接入算法,由機器人玩俄羅斯方塊。python

思路

  • CustomGraphBase類繼承自QGraphicsObject,提供必要的虛函數。
  • CustomGraphTetrisBlock類繼承自CustomGraphBase,實現最小方塊,分邊框類型(0)與方塊類型(1)。
  • CustomGraphTetrisText類繼承自CustomGraphBase,顯示文字,類型爲5。
  • Tetris類組合CustomGraphTetrisBlock,顯示俄羅斯方塊。
  • Game類爲遊戲邏輯控制類。linux

    該遊戲傳統的編程方式,是用一個二維數組來控制遊戲空間,相似迷宮的方式。其實選擇QGraphicsItem來實現就是一種很另類的選擇,其實用gdi來作更方便,這種規模,QGraphicsItem沒有優點,只是我的學習探索的選擇。
    我沒有用二維數組來控制遊戲空間,而是在邊沿上用了一圏CustomGraphTetrisBlock來定義遊戲空間,由於全部的items都能方便的在scene上檢索到,因此看一個方塊是否能移動,就須要檢索本身的周圍是否已經被其它方塊佔據。這裏有一點,在方塊進行旋轉的時候,就要判斷區分組成本身的block和別人的方塊。git

效果圖

關鍵代碼分析

功能儘可能內聚,類CustomGraphTetrisBlock封裝小方塊,Tetris類組合了Block,封裝了俄羅斯方塊的絕大部分操做,類Game遊戲的總體流程。github

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類,俄羅斯方塊類

七類方塊的定義windows

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函數檢測位置上是否有方塊ide

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
相關文章
相關標籤/搜索