前阵子发过一个帖子,上传了自己写的俄罗斯方块。但是由于工作比较忙没时间写说明,现在补上。
俄罗斯方块写过好几次了,每次的感觉都不一样,都有新的收获。就像达芬奇画鸡蛋一样,虽然都是画同样的鸡蛋,但是每次都有不同的收获。
先来看看我们需要的是一个怎么样的程序。
首先要有2个大功能:1.开始游戏 2.退出游戏。其中要编程的主要工作都集中在“开始游戏”之后的过程中。俄罗斯方块的游戏规则相信大家一定都不陌生,也许还有不少人都是骨灰级玩家了,但 是在此还是有必要说明一下游戏的规则,因为从编程和玩家的两个不同角度来观察游戏规则,还是有微妙的区别的——侧重的东西不同。
游戏 在一个m*n的矩形框内进行。游戏开始时,矩形框的顶部会随机出现一个由四个小方块构成的形状,每过一个很短的时间(我们称这个时间为一个tick),它 就会下落一格,直到它碰到矩形框的底部,然后再过一个tick它就会固定在矩形框的底部,成为固定块。接着再过一个tick顶部又会出现下一个随机形状, 同样每隔一个tick都会下落,直到接触到底部或者接触到下面的固定块时,再过一个tick它也会成为固定块,再过一个tick之后会进行检查,发现有充 满方块的行则会消除它,同时顶部出现下一个随机形状。直到顶部出现的随机形状在刚出现时就与固定块重叠,表示游戏结束。
说的有点罗嗦,但是细细分析一下每隔tick之间发生事情的顺序对理清程序思路有好处。
接下来开始分析程序的设计了。
照理应该从大的设计一步一步往下细分的,但由于时间有限,我就反过来从小的往大的讲了,请谅解。我会尽量把问题说明白。
显然俄罗斯方块是一个简单的游戏,因为它都是基于矩阵的。既然是面向对象的方法来写,那么我们首先应该封装一个矩阵类,权且叫做CMatrix吧。
class CMatrix //矩阵类,这是一个整形矩阵类型
{
public:
//默认构造函数。
CMatrix();
//根据构造函数参数创建指定大小的矩阵
CMatrix(int width, int height);
//根据构造函数参数创建指定大小的矩阵,并为矩阵的每个元素统一分配一个初始值。
CMatrix(int width, int height, int initValue);
virtual ~CMatrix();
//重新设置矩阵的尺寸。
void ResetSize(int width, int height);
//将左右元素设置为一个值。
void SetAll(int value);
//设置row行col列的元素值为value。
void SetAt(int row, int col, int value);
//获取矩阵宽度。
int GetWidth() const;
//获取矩阵高度。
int GetHeight() const;
//获取row行col列元素的值。
int GetAt(int row, int col) const;
//旋转矩阵,参数为是否顺时针。
bool Rotate(bool clockWise = true);
//获取第row行指针,调用者可以通过“(CMatrix对象)[行][列]”来获取或设置元素。
int* operator[](int row) const;
//重载等号运算符,其实也可以写拷贝构造函数。
CMatrix& operator=(CMatrix &srcMat);
protected:
//这些都是私有的成员数据,意义可以从名字上看出。
int *m_pData;
int m_width;
int m_height;
protected:
//这里都些私有的函数,都是被上面的公共函数所使用的。
//释放数据资源。
void ReleaseData();
//为数据分配资源。
void InitData(int width, int height);
//设置数据内容。
void SetData(int initValue);
//内存块拷贝。
static void MemCopy(int *dest, int *src, int len);
};
接下来需要把下落的随机形状也封装成一个类,命名为CShape。
class CShape //该类实际上是把一个CMatrix类的对象封装了起来,并且组织了一些操作。
{
public:
CShape();
virtual ~CShape();
//创建一个随机形状,注意这里随机其实是指在俄罗斯方块中的7种形状中的某一种。参数是该形状的左上角坐标,默认(0,0)。
void CreateRandomShape(int posX = 0, int posY = 0);
//设置该形状的左上角坐标。
void SetPos(int posX, int posY);
//旋转形状,众所周知,俄罗斯方块游戏中的形状是可以旋转的。默认就是顺时针的旋转。
void Rotate();
//取消前次旋转,这个函数有它的用处,代码读下去就会明白的。
void CancelRotate();
//使形状向下移动一格。
void MoveDown();
//使形状向上移动一格。
void MoveUp();
//左一格。
void MoveLeft();
//右一格。
void MoveRight();
//获取当前形状的类型ID,分别代表7种形状。(0~6)
int GetShapeType() const;
//获取当前形状的左上角坐标。
POINT GetPos() const;
//获取形状占用的宽度。
int GetWidth() const;
//..............高度。
int GetHeight() const;
//重载[]操作符。
const int* operator[](int row) const;
//重载等号,也可以写一个拷贝构造函数。
CShape& operator=(CShape& srcShape);
protected:
//被封装的CMatrix对象。
CMatrix m_mat;
//形状的类型ID。
int m_type;
//一个比较微妙的值,暂时不解释,读代码会明白的。关系到形状的旋转,有些形状可以旋转出四种样子,但有些是由两种样子。
bool m_needJump;
//左上角X坐标。
int m_posX;
//......Y....。
int m_posY;
protected:
//这里都些私有的函数,都是被上面的公共函数所使用的。
//随机创建一个形状。
void RandCreate();
//随机旋转几次。
void RandRotate();
//旋转形状,参数表示是否顺时针。
void RotateShape(bool clockwise);
};
有了形状类之后,我们还需要一个底盘类,用来表示游戏中除了当前下落的形状之外的背景部分和已经固定的块。我们称这部分为底盘,类名为CBoard。
class CBoard //此类其实也是对一个CMatrix的对象进行了封装。
{
public:
//构造函数时默认的底盘大小是10*20。
CBoard();
virtual ~CBoard();
//重新设置底盘的大小,并且底盘被清空。这个函数好像没用到。但是整个编写过程用的是面向对象的思想,所以在写这个类的时候我把外面可能会调用的方法都写成了公共接口。
void ResetSize(int width, int height);
//设置row行col列的值为value。value的取值为两个宏,R_EMPTY和R_BLOCK,分别表示“空”和“有方块”状态。
void SetAt(int row, int col, int value);
//将形状shape结合到底盘中,成为底盘的一部分。
void UniteShape(const CShape& shape);
//清除所有排满方块的行中的方块,并使这些行上面的方块们下落下来。
int ClearRows();
//获取底盘宽度。
int GetWidth() const;
//........高度。
int GetHeight() const;
//获取row行col列的格子的内容,两种值:R_EMPTY和R_BLOCK。
int GetAt(int row, int col) const;
//获取底盘数据到指针的destBuffer所指的空间。该空间至少要有GetWidth()*GetHeight()*sizeof(int)个字节大小。
void GetBoardData(int *destBuffer) const;
//独立性检查,检查形状shape是否独立。shape如果和底盘上的固定块有部分重叠则不独立返回false,否则独立返回true。
bool SingleTest(const CShape& shape) const;
//边界检查,检查形状shape是否在边界内。在界内返回true,出界则返回false。
bool RangeTest(const CShape& shape) const;
//重载[]运算符。
const int* operator[](int index) const;
protected:
//被封装的矩阵对象。
CMatrix m_mat;
protected:
//这里都些私有的函数,都是被上面的公共函数所使用的。
//检查第index行是否全满。
bool IsRowFull(int index) const;
//检查第index行是否全空。
bool IsRowEmpty(int index) const;
//检查第index行是否全X。当full为trye时X=满,否则X=空。此函数为上面两个函数服务。
bool IsRowInStatus(int index, bool full) const;
//清除第index行中的所有方块。
void ClearRow(int index);
//使所有漂浮在半空的固定块下落。
void FallDown();
//拷贝第srcRow行到第destRow行。
void CopyRow(int destRow, int srcRow);
//状态检测,服务于SingleTest函数和RangeTest函数。
bool ShapeTest(const CShape& shape, bool coverTest) const;
};
现在有了形状类和底盘类,应该可以做游戏的逻辑部分了。我们把整个游戏逻辑封装在一个叫做CRussia的类中。
class CRussia
{
public:
CRussia();
virtual ~CRussia();
//初始化游戏,默认的底盘大小是10*20。
void InitGame(int width = 10, int height = 20);
//将当前形状向左移动一格,返回是否成功。
bool MoveShapeLeft();
//............右......................。
bool MoveShapeRight();
//时间流逝函数,表示经过一个tick的时间,一般返回true,返回false表示gameover了。
bool PassTick();
//旋转当前形状,返回是否成功。
bool RotateShape();
//获取底盘数据,填充到指针buffer中,buffer的大小至少要在(底盘宽度*底盘高度*sizeof(int))以上。数据内容为R_EMPTY或R_BLOCK。
void GetBoardInfo(int *buffer) const;
//获取底盘上第row行col列的数据,内容为R_EMPTY或R_BLOCK。
int GetBoardInfo(int row, int col) const;
//获取当前下落的形状的信息,返回值是一个ShaoeInfo结构类型,该数据结构的构成稍后说明。
ShapeInfo GetCurrentShape() const;
//获取下一个形状的信息,返回值也是ShapeInfo类型。
ShapeInfo GetNextShape() const;
//获取当前得分。
int GetScore() const;
//获取当前游戏状态,值:RS_NORMAL、RS_UNITE、RS_CLEAR或RS_GAMEOVER,表示四个游戏状态。Normal是普通状态,Unite表示当前
的tick发生了下落块结合到底盘固定块的事件,Clear表示当前tick发生了满行清除的事件,Gameover不用多说了。
int GetStatus() const;
protected:
//游戏的底盘。
CBoard m_board;
//当前的形状。
CShape m_currentShape;
//下一个形状。
CShape m_nextShape;
//当前形状是否存在。
bool m_hasShape;
//当前得分。
int m_score;
//当前游戏状态。
int m_status;
protected:
//这里都些私有的函数,都是被上面的公共函数所使用的。
//初始化当前下落快的位置。
void InitShapePos();
//尝试将出界的形状拽进界内,返回是否成功。
bool DragInRange();
//使当前形状下落一格,如果已经到底了就与底盘结合。
void ShapeDown();
//用下一个形状作为当前形状,并重新产生随机的下一个形状。
bool NextShape();
//获取形状shape的信息返回一个ShapeInfo结构。
ShapeInfo GetShapeInfo(const CShape &shape) const;
};
最后是ShapeInfo,这个结构表示一个形状的信息。
struct ShapeInfo
{
//这个布尔型值表示该数据结构是否有效,在一些函数返回一个ShapeInfo的时候如果这个值是false则表示此时不存在这个形状。
bool isValidated;
//type是这个形状的类型ID,对应俄罗斯方块中的7种形状,取值0~6。
int type;
//每个形状一定是由四个小方块构成,下面这个POINT[]型的blocks数组里面存储了四个小方块各自的坐标。
POINT blocks[4];
//构造。
ShapeInfo()
{
isValidated = false;
type = 0;
}
};
现在有了这些类之后,只需要套到MFC的界面框架里,然后编写键盘事件及绘制事件等函数就可以完成游戏了,这个过程不是我发布这篇短文的重点,有兴趣的看客可以去源码里看。在MFC里创建一个CRussia类的实例就可以通过调用它的接口函数来完成游戏了。
这次俄罗斯方块的编写,我主要是想提升一下自己面向对象的感觉,但是写下来还是觉得有些不舒服的地方。不过还是希望能够对新手有些帮助,更希望得到高人指点。一直都是通过网络的帮助来学习的,所以发这篇短文希望也能帮助别人。