基本MFC类框架的俄罗斯方块游戏
一、设计目的
PC游戏早已在IT行业形成了庞大的产业链,很多IT企业都凭借着自己雄厚的游戏开发技术而占据电子游戏娱乐市场。俄罗斯方块是一款风靡全球的PC机游戏和掌上机游戏,它造成的轰动与创造的经济价值可以说是游戏史上的一件大事。这款游戏看似简单却变化无穷。通过该游戏设计,达到以下目的:
- 了解Visual C++下可视化编程的特点,熟悉其相应的各种操作和技巧
- 了解随机函数的使用、动态存储空间的分配和回收、位图资源的引入和操作
- 掌握MFC类家族中,键盘消息、菜单、工具栏、定时器、画笔画刷的使用
- 掌握俄罗斯方块游戏开发的基本原理,强化基本编程能力和游戏开发技巧
- 学会PC游戏设计的重要理念,区分开游戏逻辑代码和游戏地图逻辑坐标
二、任务内容
本设计要求采用Visual C++下可视化编程,充分利用MFC类家族中的类和库函数,实现游戏方块预览、游戏方块控制、游戏显示更新、游戏分数、游戏等级、游戏帮助等功能。对该游戏系统功能模块进行详细分析,写出详细设计说明文档,编写程序代码,代码量要求不少于300行。调试程序使其能正确运行。
三、设计要求
具体要求如下:
- 屏幕中央有一个矩形“容器”,选择“开始”菜单或“开始”工具按钮后,俄罗斯方块的部件随机产生并在容器中自由下落
- 游戏过程中,当在矩形“容器”中,出现一个游戏方块时,必须在游戏方块的预览区域中出现下一方块,这样利于游戏玩家控制游戏的策略
- 有七种标准俄罗斯方块部件,通过各种判断,实现游戏方块并随着键盘上的上键顺时针旋转90度,随着下键加速下落,随着左、右各键分别左、右移动
- 当部件下落达“容器”底部或已停止的部件时,停止下落;当同一行部件完整拼接上时,该行消失,其他行向下移动,在适当位置显示当前累计分数
- 当部件总行数超过矩形容器的高度或没有足够的空间产生新的部件时,提示“游戏结束”信息并停止游戏运行
- 游戏计分原则:一次消去一行加10分,同时消去2行、3行、4行分别加30分、50分、70分。该游戏设置有3个级别,分别为1——简单、2——普通、3——较难,难度越大,方块下落的速度越快
- 游戏帮助功能:玩家进入游戏,可以通过菜单中的“帮助”菜单或工具栏上的“?”工具按钮,获得游戏如何操作的友情提示
四、设计说明
4.1 需求分析
基本思路是采用MFC AppWizard应用程序的SDI程序框架,设计该游戏,实现方块预览、方块控制、显示更新、游戏记分、游戏等级、游戏帮助等功能。方块控制,直接由玩家通过键盘控制,游戏区域的方块部件根据玩家具体键盘的操作左右移动、旋转、加速下落。游戏地图即矩形“容器”,根据具体数据,直接通过画笔画刷绘制。
主要的设计难点:
- 如何用内部数据结构表示当前部件的状态
- 如何旋转方块、判断部件放置位置及使满行消失
- 如何实现方快的预览功能
- 怎样动态地分配内存
4.2 游戏运行主要流程分析
4.3 设计难点的解决方案
所有的部件及已停止的部件均用小方格来表示。整个游戏区域对应一个二维数组,数组元素为0时,表示空白;为1时,表示已有方格。该数组存储所有已经不能移动的部件。部件采用一维数组表示,这些一维数组实际表是一个n×n的矩阵。如表示一个方块使用一个2×2的矩阵,实际存储{1,1,1,1};如表示一个长条使用一个4×4的矩阵,实际存储{0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0}。
实际显示的时候,先画出已停止的方块,然后计算出正在下落的部件的正确坐标位置并其将画出,同时在固定位置画出预览方块。
旋转部件时,只需变换存储部件的矩阵即矩阵的转置,使其对应为转换后的形态,并将其显示出即可。
判断部件是否可以下落、旋转、左移、右移时,将表示部件的数组对应到游戏区域的二维数组中,再判断是否允许操作。当部件无法再操作时,将部件数组中对应填入游戏区域中对应的二维数组。
使一层消失可以通过判断游戏区域中的二维数组是否某一行全为1;如果该数组的第一行全为1或已没有足够的空白使新的部件可以加入到游戏区域中,则游戏结束。
4.4 数据结构设计
4.4.1 游戏方块Componet结构体
typedef struct tagComponet
{
int intComID; //部件的ID号
int ntDimension; //存储该部件所需的数组维数
int* pintArray; //指向存储该部件的数组
}Componet;
Componet结构体表示某个游戏方块具体的属性,每个游戏方块对应有一个编号,且有对应存储该部件的数组,用指针指向该数组,并设定存储该部件所需数组(n×n)的维数。
4.4.2 Componet结构体数组
初始化游戏方块,即定义MAXCOM个Componet类型的结构体,并初始化(MAXCOM为7)。例如:
m_Componets[6].intComID=6;
m_Componets[6].intDimension=4;
m_Componets[6].pintArray=new int[16];
m_Componets[6].pintArray[0]=0;
m_Componets[6].pintArray[1]=1;
m_Componets[6].pintArray[2]=0;
m_Componets[6].pintArray[3]=0; //0 1 0 0
m_Componets[6].pintArray[4]=0; //0 1 0 0
m_Componets[6].pintArray[5]=1; //0 1 0 0
m_Componets[6].pintArray[6]=0; //0 1 0 0
m_Componets[6].pintArray[7]=0;
m_Componets[6].pintArray[8]=0;
m_Componets[6].pintArray[9]=1;
m_Componets[6].pintArray[10]=0;
m_Componets[6].pintArray[11]=0;
m_Componets[6].pintArray[12]=0;
m_Componets[6].pintArray[13]=1;
m_Componets[6].pintArray[14]=0;
m_Componets[6].pintArray[15]=0;
以上代码是对长条这个方块的定义和初始化。其余形状的6种方块的定义见附录2的程序清单CELSblockView::CELSblockView()该析构函数中。
4.5 功能模块设计
4.5.1 游戏方块预览
新游戏方块将在如图4所示的右边4×4的正方形小方块中预览。使用随机函数rand( )来产生0~6之间的游戏方块编号,其中的正方形小方块的大小为SIZE×SIZE。SIZE为设定的方块像素大小。
4.5.2 游戏方块控制
左移的实现过程
- 判断在矩形“容器”中能否左移。这一判断必须满足如下两个条件:游戏方块整体左移一位后,游戏方块不能超越“矩形”容器的左边线,否则越界;并且在游戏方块有值(值为1)的位置,“矩形”容器必须是没有被占用的(占用时,值为1)。若满足这两个条件,则执行左移动作,否则不执行左移动作
- 清除左移前的游戏方块
- 在左移一位的位置,重新显示此游戏方块(具体如何清除和显示方块的函数将在功能函数设计中阐述)
右移的实现过程
- 判断在矩形“容器”中能否右移。这一判断必须满足如下两个条件:游戏方块整体右移一位后,游戏方块不能超越“矩形”容器的右边线,否则越界;并且在游戏方块有值(值为1)的位置,“矩形”容器必须是没有被占用的(占用时,值为1)。若满足这两个条件,则执行右移动作,否则不执行右移动作
- 清除右移前的游戏方块
- 在右移一位的位置,重新显示此游戏方块
向下加速下落(下移)的实现过程
- 判断在当前的“矩形”容器中,能否下移。这一判断必须满足以下两个条件:游戏方块整体下移一位后,游戏方块不能超过“矩形”容器的底边线,否则越界;并且在游戏方块有值(值为1)的位置,“矩形”容器必须是没有被占用的(占用时,值为1)。若满足这两个条件,则执行下移动作,否则不执行下移动作
- 清除下移前的游戏方块
- 在下移一位的位置,重新显示此游戏方块
旋转的实现过程
- 判断在当前的“矩形”容器中,能否旋转。这一判断必须满足以下两个条件:游戏方块整体旋转后,游戏方块不能超过“矩形”容器的左边线、、右边线、底边线,否则越界;并且在游戏方块有值(值为1)的位置,“矩形”容器必须是没有被占用的(占用时,值为1)。若满足这两个条件,则执行旋转动作,否则不执行旋转动作
- 清除旋转前的游戏方块
- 在“矩形”容器中显示区域不变的位置,显示旋转后的游戏方块
游戏显示更新
- 当游戏方块左右移动、下落、旋转时,要先清除先前的游戏方块,重新绘制游戏方块。当消除满行时,不能移动的方块下移后,进行重新绘制
- 清除游戏方块的过程为:用刷新函数刷新相应的游戏区域,清除先前显示的方块,执行相应的操作后,再次调用刷新函数刷新此区域,显示将要显示的游戏方块
游戏帮助功能
- 在帮助对话框中,直接添加静态文本即可
4.6 功能函数设计
在MFC视图类中添加功能构造函数,主要的功能函数设计如下:
- CELSblockView()
- 函数原型:CELSblockView::CELSblockView()
- CELSblockView()初始化游戏的各项数据,“矩形”容器(游戏地图)、游戏级别、当前部件的ID、游戏分数、随机函数。定义并初始化MAXCOM个方块部件信息(MAXCOM为7),且在对应的析构函数中释放回收内存
- OnDraw()
- 函数原型:void CELSblockView::OnDraw(CDC* pDC)
- OnDraw()导入背景图片,画游戏区域、不能移动的方块、下落的部件,显示得分、级别、下一个方块
- OnKeyDown()
- 函数原型:void CELSblockView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
- OnKeyDown()对键盘信息处理,读取键盘操作值,对当前部件,调用CanLeft()、CanRight()、CanRotate()、CanDown(),并实现左移、右移、旋转、加速下落
- OnTimer()
- 函数原型:void CELSblockView::OnTimer(UINT nIDEvent)
- OnTimer()是定时器消息处理函数,显示预览部件,调用CanDown()判断能否下移,实现方块自由下落,调用Disappear()消去满行,调用CheckFail()判断游戏是否结束,调用NewComponet()产生新部件
- NewComponet()
- 函数原型:void CELSblockView::NewComponet(void)
- NewComponet()产生新部件的函数,并将新部件的信息拷贝到当前部件的结构体m_CurrentCom中
- CanDown()
- 函数原型:bool CELSblockView::CanDown(void)或false
- **CanLeft() **
- 函数原型:bool CELSblockView::CanLeft(void)
- CanLeft()判断游戏方块是否可以左移,并返回值true或false
- CanRight()
- 函数原型:bool CELSblockView::CanRight(void)
- CanRight()判断游戏方块是否可以右移,并返回值true或false
- CanRotate()
- 函数原型:bool CELSblockView::CanRotate(void)
- CanRotate()判断游戏方块是否可以旋转,并返回值true或false
- CanNew():
- 函数原型:bool CELSblockView::CanNew(void)
- CanNew()判断是否可以在“矩形”容器中产生新的游戏方块,并返回值true或false
- CheckFail()
- 函数原型:bool CELSblockView::CheckFail(void)
- CheckFail()判断游戏是否结束,并返回值true或false
- Disappear()
- 函数原型:CELSblockView::Disappear(void)
- Disappear()判断是否有满行,且消去满行,记录消去行数,并按规则增加和显示游戏分数
- MyInvalidateRect()
- 函数原型:Void CELSblockView::MyInvalidateRect(POINT ptStart,int intDimension)
- MyInvalidateRect()刷新了一个以ptStart为坐上角,长度为intDimension且不超出游戏区域的正方形区域
- OnGameStart()
- 函数原型:void CELSblockView::OnGameStart()
- OnGameStart()游戏开始的菜单和工具栏消息函数
- OnGameEnd( )
- 函数原型:void CELSblockView::OnGameEnd()
- OnGameEnd( )游戏结束的菜单和工具栏消息函数
其他相关的功能实现函数:
- PreCreateWindow( )
- 函数原型:BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
- PreCreateWindow( )设置生成主窗体的大小、风格
一些常见的消息函数,再此略去。
4.7 主要函数调用流程
4.8 编码和实现
- 在Visual C++平台下,利用App Wizard生成一个名为ELSblock的SDI程序框架,其他选项均可用缺省设置
- 在“Resources View”菜单中,选取Menu资源项中菜单ID“ID_MAINFRAME”中,添加“游戏”和“设置”菜单,再增加相应的子菜单,并设置其相应的属性,见表1,在Toolbar资源项中,编辑工具栏,并设置相应的属性;拷贝文件clxj.bmp到res文件夹下,引入Bitmap资源,引用res文件夹下clxj.bmp,生成背景位图。然后建立类向导,添加相应的消息函数和位图引入代码
ID | Caption | Prompt |
---|---|---|
ID_GAME_START | 开始 | 游戏开始 |
ID_GAME_END | 结束 | 游戏结束 |
ID_SET_LEVEL1 | 简单 | 游戏级别1 |
ID_SET_LEVEL2 | 普通 | 游戏级别2 |
ID_SET_LEVEL3 | 较难 | 游戏级别3 |
- 在“ClassView” 菜单中,在CELSblockView类中,“Add Windows Message Handler”对话框,“添加Windows消息/时间”窗口中添加WM_KEYDOWN键盘和WM_TIMER定时器消息事件
- 在生成的程序框架中CELSblockView类的头文件和源文件中添加相应的代码
4.9 调试及运行
所有代码录入完成后,按F5编译,调试运行,生成的可执行程序如图3所示。游戏运行时的界面如图 4 所示。
在代码的输入过程中,主要以输入一个函数,编译一次的方式进行,所以在执行的时候基本没有输入的错误。
调试的过程中,出现的主要问题是,逻辑坐标的计算失误,导致预览方块不在视图窗口预定位置显示和分数无法刷新。刷新过程中,起初调用了Invalidate()函数,导致屏幕显示闪烁,后来改调用InvalidateRect()函数避免此此问题。同时,对游戏方块的清除和显示,采用自定义的MyInvalidateRect()刷新函数,使屏幕显示更加稳定。
调试的过程中,还发现,一些还没有解决的问题,游戏级别的更新和分数的更新之间没有相互关联的关系和消去满行的函数,不能够实现,这样的情况——如果矩形容器底部,有未满的行,但是中间有满行,这些满行无法正常消去。由于时间仓促,多次修改不得的情况下,暂时没有能找到合适的解决方案。
在前期的设计中,还有背景音乐的设计和网格提示等功能,由于用到关于多媒体技术等相关知识,时间仓促,也没有去实现。
生成的可执行程序图标
游戏运行时截图
五、游戏操作说明
- 玩家可以通过游戏菜单中的开始和结束菜单或工具栏上“开始”“结束”按钮开始和结束游戏
- 玩家可以通过设置菜单中的游戏级别和工具栏“1” “2” “3”按钮设置不同的游戏级别
- 游戏中,玩家通过键盘上的上、下、左、右键分别控制方块的旋转、加速下落、左移、右移
- 玩家还可以通过帮助菜单中的帮助菜单和工具栏上的“?”按钮获得游戏操作的友情提示和制作者的相关信息
六、心得与体会
俄罗斯方块游戏,这款一直风靡全球的游戏,90后的我们一点不陌生,一个看似很简单的游戏,却拥有着变幻无穷的魅力。用自己所学的知识去实现这样一款可堪称经典的游戏,真是一种快乐。
此次课程设计历时近25天,主要是前期的准备工具花去了大量的时间,包括对游戏的需求分析、可行性分析、数据结构设计、功能函数设计、算法设计及一些资料和资源的收集工作。主要的详细设计用了大约3天的时间,编写和修改代码总共用了3天的时间,书写和修改书面报告用来2天的时间。由于时间仓促,原先很多的想法和设计,在具体设计的时候,被我删掉。我会在寒假的时候,继续去完善它。
此次我的设计,依旧采用我传统的思想——“分而治之”。分而治之,即指在软件的开发过程中,将该软件模块化和对其功能细化,设计的过程中,逐个击破,但是这个过程中值得注意的是,不要忽略了一些变量的定义和各部分的接口是否一致,在这些工作完成后,我们做的就是堆积木般地去组装好软件。对于俄罗斯方块游戏,首先我对功能模块进行了如下划分:游戏预览功能、游戏显示功能、游戏方块操作、游戏帮助功能、游戏等级分数更新功能。其中最主要的是游戏的显示功能和游戏方块操作。这部分的设计,是该游戏设计的重点和难点。在进行详细的分析和逻辑坐标的计算之后,我又把这些功能的实现,分解成一个一个的子函数,通过对相关函数设计和函数间的相互调用去实现一个一个功能和游戏中具体操作。
这款游戏的设计过程中,我深感,实现其的方法真的很多。但是,我选择了一种易于初学者理解和掌握的方法,利用Visual C++可视化编程的特点,结合MFC类家族丰富的库函数,去实现了游戏显示这一难题,然后结合库函数构造了一些自己的函数,去实现具体的一些判断和操作。最后,处理和美化界面,包括利用随机函数,让方块可以随机变换颜色,并且在下落过程中,方块出现闪烁下降的特效等。