最近对开源版本的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) {
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的事情。