用MFC实现的俄罗斯方块,上学期期末复习着无聊的时候做出来的,每天做一点,最后居然顺利做出来.做了之后才发现,要做成这个小游戏,值得考虑的地方还是很多的.想了想就有以下的一些细节,总结起来有以下几点:
1. 关于方块结构的确定,最初考虑的是用4*4的数组来表示一个方块,但这就意味着每个方块会造成远大于自身的存储空间的浪费,而且按照这种思路,方块每切换一次形状,就要对一个4*4的数组数据进行重写,时间和空间效率都不高.在考虑换种表示方法的时候,突然观察到,其实俄罗斯方块里的每个方块都是由4个格子组成的,只要定义一个大小为4的结点数组就能很好的保存方块的位置了.于是Node结构就这样成了.
typedef struct node
{
int x;
int y;
}Node;
2. 有了这个结构之后,就该考虑如何表示方块了,因为使用的是MFC,而上学期又系统的学了C++,于是考虑了用面向对象的方法来实现这几个方块,从面向对象的角度来看,这几个方块确实具有惊人的相似度,首先,它们都是由四个格子组成,其次,都能由键盘可控制移动,然后,都能通过某个按键(本程序定义用空格)来切换形态,最后,也是理所当然的能够被显示出来.所以,我为这些方块定义了一个名叫CBase 的抽象类作为它们的基类,而由于这些方块的左右移动和着陆方法都是相同的,所以在基类中就实现了这几种方法,充分体现了面向对象的代码重用机制,最重要的是也省去了写重复代码的麻烦.
class CBase
{
public:
Node node[4]; //用于存放结点
CBitmap bm; //格子的图片
int State; //用于标识当前的形态
bool bmLoaded; //图片是否成功导入
public:
virtual void Set(int x,int y)=0; //初始化方块出现的位置
virtual void Draw(CDC *pDC,CWnd *pWnd)=0; //显示方块
virtual void Change(const CBackGround bk)=0; //切换形态
bool Landed(const CBackGround bk); //是否已经着陆
void Down(); //落下
void RMove(const CBackGround bk); //右移
void LMove(const CBackGround bk); //左移
void Show(CDC *pDC);
//Show和Draw不同,专门用于显示下一个方块
void RefreshShowRect(CWnd* pWnd); //刷新下一个方块
};
3. 有了这个基类后,方块类只用重写继承来的虚函数就可以,让自己成为特定的方块了,在重写这些虚函数时,最重要的当然是显示的问题了,在做这个程序之前,看见一篇文章,介绍如何使用无闪烁重绘,所以就把这种方法用进来了,其结果就是为了实现无闪烁,计算了大量的方块移动后需要重绘的区域,实在是痛苦…..好在最后还是把这个功能实现了.在代码里也可以看到Draw方法里面很多行都用在计算坐标然后调用InvalidateRect()上了.
4. 俄罗斯方块总共有7种方块,每个方块就对应一个继承于CBase的类.在这些类中另一个重要的实现细节就是如何判断方块成功着陆了.在俄罗斯方块中,成功着陆分为两种情况,第一就是落到最下层的”地面”上,第二就是落在了其他方块上面.显然这两种着陆方式是由本质不同的.如果要成功实现这两种方式,至此就必须考虑怎样把已着陆的方块和玩家正控制的方块联系起来.从这个问题出发,构造了CBackGround类,已着陆的类就好像背景一般的存在.那么,第一种着陆方式就可以直接判断当前方块的格子y坐标是否有和地面相同的,而第二种着陆,就判断当前方块格子y坐标是否有和CBackGround中格子相同的.那么着陆的问题就解决了.以下便是CBackGround类
class CBackGround
{
public:
Node node[1200]; //格子
int top; //把所有格子看成栈,top就是栈顶指针
int nCleared; //是否符合消去的条件
public:
CBackGround(); //构造函数
void Draw(CDC *pDC,CWnd *pWnd); //显示所有的格子
void GetNode(CBase *pBase); //得到当前方块的节点并贮存
void Clear(CWnd *pWnd); //消去
bool GameOver(); //游戏结束
int GetClearedNum(); //用于计分
};
5. 构造这个类,还考虑其他的一些问题.比如,1.既然是用于表示已着陆的方块的类,那么关于消去的方法也理所应当在这个类里面实现,2.当前方块成功着陆后,就由当前方块变成了CBackGround的组成,GetNode()负责完成这个转变3.当CBackGround中的某个节点的y坐标到某指定值时,就应该意味着游戏结束了.4.关于计分,首先在这个类中记录消去次数然后把这个次数传给一个CCounter计数器来统计和显示当前分值,个人设定的公式就是10*消去次数.
6. 这些自定义类完成后,程序也到了最后阶段了,那就是在MFC中使用的问题.首先,在CMainFrame类中添加WM_GETMINMAXINFO消息,在消息响应函数中,把窗口的大小固定成了300*400;接着,为CView类添加一系列将要使用的对象,其中有CBase类指针m_pBase,通过这个指针,就可以new出不同的方块了.
7. 说到方块的产生,就不得不提另一个问题了.俄罗斯方块中方块的产生都是随机的,每次运行游戏产生的方块都有可能不同.这是一种真的随机,而Random()函数产生的却都是伪随机数,每次运行产生的结果是固定的,不符合要求.于是,考虑了用读取系统时间值并对7取模的方式来产生编号为0—6的方块,这样就模拟出了需求的随机效果.
8. 对于俄罗斯方块这个游戏的描述应该是,打开游戏后,有各种下落的方块,并能显示下一个出现的方块,用户可以控制当前方块直到它着陆,控制包括左右移动快速下移以及切换形态,当方块成功着陆后,控制下一个方块,如此循环.当一行充满格子时,该行被削去,增加计分值.
当方块累积到一定高度时游戏结束.能清晰描述后,该游戏的需求分析就完了,对于CView部分的代码,就是这些功能的具体实现.
9. 最后要说一下算法的问题,在写消去算法的时候,本来为了简化,我把多行的消去考虑为一行消去的递归形式,本来觉得这种考虑是正确的,起初也没有出现问题,但在某一次玩的时候,居然出现了消不动的情况,经过几次测试才发现,最早的算法是判断与当前方块着陆点有关的行数是否满足消去条件,然后逐行递归消去.但是俄罗斯方块也可以隔行消去,比如3行5行满足消去条件而4行不满足,由于考虑不足,出问题是自然的了.在改进之后,我采用了记录与当前方块着陆点相关的4行并判断它们是否满足消去条件.