俄羅斯方塊的資料結構及實現 struct of a tetris

wx紅杉樹發表於2007-10-16

最近對開源版本的QT中的俄羅斯方塊的實現做了一些分析,順便記錄一下。本文遵守GNU GPL。

 

一直想做自己的遊戲,俄羅斯方塊或者五子棋這樣的應該都是初學者想去完成的例子,最近找了一些關於tetris的程式碼,發現QT裡面的例子做的比較好,它用C++實現了一個很好的類,我也用Win32的GDI重寫了一個不用QT版本的。這裡記錄一下QT中tetrix的實現。

 

1、類結構:

 

QT的tetrix在其安裝目錄的examples/widgets/tetrix下(我使用的是QT4.2.3 for windows opensource mingw版本)。裡面有這樣兩個類: TetrixPiece和TetrixBoard。其中TetrixPiece定義了tetrix中各種形狀方塊,TetrixBoard實現了整個遊戲的實現以及邏輯。說實話,在看了幾天QT的程式碼後,發現用QT寫GUI的東西還是很方便地。裡面的模組十分清晰,寫出來的程式碼很漂亮,可讀性極高,比用MFC嚮導生成那些工程要清晰很多,也比用WIN32寫程式碼省好多筆墨。

 

2、資料結構:

 

在沒有選擇QT的tetrix之前,也從CodeGuru和CodeProject上找了一些tetrix的實現,但是,其中不是用了某些圖形庫(如cximage),就是資料結構定義很冗餘。當然,這之前,我自己也想過其資料結構的實現,您也可以先在想想,對於裡面的方塊,我們該如何定義它的資料結構呢?

 

最初我的想法是,定義一個4X4的矩陣(簡單的使用二維陣列),並定義幾個狀態數來表示每一個圖形。比如,要定義一個長條裝的圖形,我們可以需要這樣一個二維陣列:

 

1 0 0 0

1 0 0 0

1 0 0 0

1 0 0 0

 

這裡面1表示有圖形,0表示無,這樣我們可以寫出一個簡單的影象驅動,在給他傳送一個2維陣列的樣式後,幫助我們把它畫出來。那麼,對於一個長條狀的圖形,我們大概需要兩個這樣的矩陣。方塊好一點,可能只需要一個,但是裡面的Z形狀以及T形狀的圖形,每一個都需要4個這樣的陣來表示。這樣的實現固然可行,只是在其資料結構的定義時,程式碼中需要一個長長的靜態三維陣列來表示這些圖形。

 

對於tetrix的Board,實現起來應該簡單一些,複雜的是它裡面的邏輯控制。它只需要一個大的矩陣來儲存整個圖形區域的狀態就好了。

 

當然,還有其他的表示方法,我們就不多討論了。我們現在來看看QT例子中的實現: TetrixPiece.h and TetrixPiece.cpp

 

/****************************************************************************
** TetrixPiece.h

**
** Copyright (C) 2004-2007 Trolltech ASA. All rights reserved.
**
****************************************************************************/

#ifndef TETRIXPIECE_H
#define TETRIXPIECE_H

enum TetrixShape { NoShape, ZShape, SShape, LineShape, TShape, SquareShape,
                   LShape, MirroredLShape };

class TetrixPiece
{
public:
    TetrixPiece() { setShape(NoShape); }

    void setRandomShape();
    void setShape(TetrixShape shape);

    TetrixShape shape() const { return pieceShape; }
    int x(int index) const { return coords[index][0]; }
    int y(int index) const { return coords[index][1]; }
    int minX() const;
    int maxX() const;
    int minY() const;
    int maxY() const;
    TetrixPiece rotatedLeft() const;
    TetrixPiece rotatedRight() const;

private:
    void setX(int index, int x) { coords[index][0] = x; }
    void setY(int index, int y) { coords[index][1] = y; }

    TetrixShape pieceShape;
    int coords[4][2];
};

#endif

/****************************************************************************

** TetrixPiece.cpp

**
** Copyright (C) 2004-2007 Trolltech ASA. All rights reserved.
**
****************************************************************************/

#include <QtCore>

#include <stdlib.h>

#include "tetrixpiece.h"

void TetrixPiece::setRandomShape()
{
    setShape(TetrixShape(qrand() % 7 + 1));
}

void TetrixPiece::setShape(TetrixShape shape)
{
    static const int coordsTable[8][4][2] = {
        { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
        { { 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 } },
        { { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } },
        { { 0, -1 },  { 0, 0 },   { 0, 1 },   { 0, 2 } },
        { { -1, 0 },  { 0, 0 },   { 1, 0 },   { 0, 1 } },
        { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
        { { -1, -1 }, { 0, -1 },  { 0, 0 },   { 0, 1 } },
        { { 1, -1 },  { 0, -1 },  { 0, 0 },   { 0, 1 } }
    };

    for (int i = 0; i < 4 ; i++) {
        for (int j = 0; j < 2; ++j)
            coords[i][j] = coordsTable[shape][i][j];
    }
    pieceShape = shape;
}

int TetrixPiece::minX() const
{
    int min = coords[0][0];
    for (int i = 1; i < 4; ++i)
        min = qMin(min, coords[i][0]);
    return min;
}

int TetrixPiece::maxX() const
{
    int max = coords[0][0];
    for (int i = 1; i < 4; ++i)
        max = qMax(max, coords[i][0]);
    return max;
}

int TetrixPiece::minY() const
{
    int min = coords[0][1];
    for (int i = 1; i < 4; ++i)
        min = qMin(min, coords[i][1]);
    return min;
}

int TetrixPiece::maxY() const
{
    int max = coords[0][1];
    for (int i = 1; i < 4; ++i)
        max = qMax(max, coords[i][1]);
    return max;
}

TetrixPiece TetrixPiece::rotatedLeft() const
{
    if (pieceShape == SquareShape)
        return *this;

    TetrixPiece result;
    result.pieceShape = pieceShape;
    for (int i = 0; i < 4; ++i) {
        result.setX(i, y(i));
        result.setY(i, -x(i));
    }
    return result;
}

TetrixPiece TetrixPiece::rotatedRight() const
{
    if (pieceShape == SquareShape)
        return *this;

    TetrixPiece result;
    result.pieceShape = pieceShape;
    for (int i = 0; i < 4; ++i) {
        result.setX(i, -y(i));
        result.setY(i, x(i));
    }
    return result;
}
 

首先看一下TetrixShape ,這個enum定義了一些圖形的索引方法。用於表示各式的圖形。我們可以在setShape方法中找到這個static const int coordsTable[8][4][2]。靜態的三維陣列,就是它,按照enum的順序,定義了各個圖形的真實表示。

 然後我們看看它到底是如何表示的,我們隨便找一個ZShape吧,它的定義是:{ 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 }  用矩陣表示出來就是

 0   -1

 0    0

-1    0

-1    1

 

這個明顯沒有上面我們討論的那個直觀,當初我看這個資料結構時候也是迷惑了好一陣。這個矩陣是如何表示Z圖形的呢?

 

瞭解了這個類中的成員函式以及後面TetrixBoard的實現才明白,TetrixPiece用4對座標來表示一個圖形。你看不管是哪種圖形,其實它都是由4個方塊組成的,所以,我們可以用4組座標來表示一個圖形。

看看這個Zshape,按照這個座標來瞄點得到

   -1   0  1

-1      *

 0 *    *

 1 *

 

看,形狀出來了吧。在這個二維陣列中,前面的代表x座標,後面的代表y座標。理解了這個,那麼下面的這些函式的意義就比較好懂了。

    int x(int index) const { return coords[index][0]; }  //返回圖形的第index個座標的x
    int y(int index) const { return coords[index][1]; }  //返回圖形的第index個座標的y
    int minX() const;                //最小x座標值
    int maxX() const;               //最大x座標值
    int minY() const;               //最小y座標
    int maxY() const;               //最大y座標  上面這4個函式,用於繪製圖形時確定圖形的外形大小。
    TetrixPiece rotatedLeft() const;  //座標轉換向左向右。
    TetrixPiece rotatedRight() const;

 

 

關於 TetrixPiece我們就聊這麼多了。有了這些東西,看明白這個例子已經很容易鳥。

下面我們擇一些有意思的地方說說:

 

void TetrixBoard::drawSquare(QPainter &painter, int x, int y, TetrixShape shape)

   這個函式,是用來畫方塊的,QPainter 類似於DGI中的DC,x和y指出在什麼座標上畫,最後給出畫的方塊是什麼圖形的(這個tetrix裡面不用方塊用不同圖形表示)。你可以看到在這個原始碼中有兩個這樣靜態的陣列,他們都被定義到了成員函式裡,一個是上面的coordsTable,一個是這個函式裡面的colorTable。說實話,我對c++一知半解,並不知道這樣的實現會不會有什麼不好的地方,但是這種什麼時候需要什麼時候用,儘量把相關程式碼寫到一起的風格,是哥們我很讚許地。

 

 bool TetrixBoard::tryMove(const TetrixPiece &newPiece, int newX, int newY)

   這個函式用於移動圖形,給出try,說明它的返回值也很有用,在判斷類似碰撞時起作用,就是讓它不能隨便Move。

 

        for (int i = 0; i < 4; ++i) {
            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            drawSquare(painter, rect.left() + x * squareWidth(),
                       boardTop + (BoardHeight - y - 1) * squareHeight(),
                       curPiece.shape());
        }
上面這段程式碼是用於繪製一個圖形的程式碼,對於每一對座標,計算了x和y的值,我們看到,它作了一些計算,主要是將座標原點重新計算,讓drawSquare在繪製的時候,可以按照一個合適的順序完成。這裡還有一個需要說明的,這組座標,在通過計算max-min的值得到了它x和y座標各自所佔的大小。
還拿上面的ZShape做例子:

 0   -1

 0    0

-1    0

-1    1


程式通過下面的規則來計算:MAX(x)-MIN(x)+1 為x軸所佔的大小即0-(-1)+1=2   ,MAX(y)-MIN(y)+1 為y軸所佔的大小即1-(-1)+1=3。

我們可以跟一下對於繪製ZShape時候的情況:假設Zshape剛從最上面掉下來。
    curX = BoardWidth / 2 + 1=10/2+1=6  //居中
    curY = BoardHeight - 1 + curPiece.minY()= 22-1+(-1)=22

    x=curX+curPiece.x(i)=5[6]

    y=curY - curPiece.y(i)=23[22][21]


   drawSquare(painter, rect.left() + x * squareWidth(), //最左邊座標+x座標
                          boardTop + (BoardHeight - y - 1) * squareHeight(), //最上端座標 +相對最高處的修正 圖形只顯示一個方塊.
                          curPiece.shape());



還有一個比較有意思的函式

TetrixShape &shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }

 

這個函式在使用的時候可以作為一個表示式的左值,like:shapeAt(1,2)=3; 我不熟悉c++的語法,但這個的確是我第一次遇見。shapeAt(1,2)=3;

相當於board[(2* BoardWidth) + 1]=3;

 

 大概就能想到這麼多要說的東西了,如果大家有興趣可以email和我討論tetrix的事情。

 

 

相關文章