第一部分:实验分析与设计(可加页)

一、实验目的和具体内容

1.实验目的

本实验旨在通过使用C++中的MFC框架和相关技术,设计和实现一个基于GUI的欢乐连连看游戏应用程序。通过完成本实验,学生将会:
1.了解MFC框架的基本概念和架构,包括应用程序、文档视图模型、窗口类、消息处理等内容;
2.掌握MFC中常用的控件和组件的使用方法,如按钮、文本框、列表框、菜单、对话框等,并学习如何将这些控件和组件集成到应用程序中;
3.学习MFC中的绘图技术,了解如何使用GDI+绘制基本图形、图片、文本等,以及如何实现游戏界面的绘制和更新;
4.通过设计和实现欢乐连连看游戏,提高学生的逻辑思维能力和程序设计能力,特别是对于游戏算法和游戏逻辑的设计和实现能力;
5.通过实践,加深对于图形用户界面设计的理解和掌握,了解如何设计和实现美观、易用、交互性强的用户界面。
总之,本实验是一次全面的MFC编程实践,旨在帮助学生深入了解和掌握MFC框架,C++编程和相关技术,并提高其数据结构算法编程能力和图形用户界面设计能力。

2.实验内容

本次数据结构实验要求实现一个快乐连连看小游戏。该游戏通过图形用户界面(GUI)实现,使用C++语言的MFC(Microsoft Foundation Class)功能库开发。
游戏的基本规则是,玩家需要在一定的时间内通过连线将相同的图案消除。每个图案都有两个相同的匹配项,当玩家成功连通两个匹配项时,它们将被消除。当所有图案都被消除时,游戏结束。如果玩家未能在规定时间内完成游戏,则游戏也会结束。
为了实现这个游戏,需要使用数据结构来存储图案和其匹配项之间的关系。可以使用二维数组或链表来存储图案的位置,同时也需要存储每个图案的类型和状态信息(如是否已被消除)。
玩家可以通过点击鼠标来选择两个图案,并尝试通过连线将它们连接起来。连接必须在同一直线上,而且不能穿过其他图案或障碍物。如果两个图案之间的路径是可行的,则它们将被消除,并给予玩家一定的分数。如果路径不可行,则玩家必须选择另外两个图案来继续游戏。
除了游戏规则和数据结构之外,还需要考虑一些其他的实现问题。例如,如何显示图案、计时、计分等。还可以为游戏添加音效和背景音乐,以提高玩家的体验。
本次实验的目标是通过实现这个小游戏来练习使用C++语言和MFC库,以及设计和实现基本的数据结构和算法。通过本次实验,学生可以学习如何设计和开发图形用户界面,掌握基本的面向对象编程思想和程序设计技巧。同时,也可以提高学生的编程能力和解决问题的能力。

二、分析与设计

1.数据结构的设计:

在此次实验中,数据结构设计没有使用很多,主要运用的部分为:
(1)、点的消去。
在点的消去中,我们需要通过数据结构算法来寻找一条可以联通的路径,首先是一条直线的连法,在这个算法中,我们不需要额外的设计,只需要判断两个点所在的直线上是否有非空的点即可。在两点判断中,我们计算后需要将两个点的坐标传入。如果不满足要求,我们就进行两线一直角的链接判断。
其次是一折连线,顾名思义就是通过一次直角折叠达到连接的效果,如右图所示,由于这种情况为一个拐点的判断,有一定的特殊性,我们可以先判断以两个点为顶点,形成的长方形的另外两个顶点是否为空,如果是空,我们可以使用两点之间直线判断分别判断这两个点之间的两条折现直线是否分别为空。如果为空,即将这三个点按顺序传入数组中,并且返回真值并进行下一步操作,如果不为空,则进行下一步两直角三折线判断。
三折线判断中,我采用的是较为普遍的强制判断,遍历第一个点所在的行,列上所有的点,再调用两点寻找方法来找到是否有三折路径的笨办法,由于没有专门将其写到一个类中,所以参数无法直接传递,我们必须通过修改类中定义的public类型数据,使用函数直接调用才能达到这个目的,注意在使用完后需要将其改回来。
三折线还有优化的潜能,比如在遍历行和列时,我们可以从顶点开始遍历,向上向下延申,这样可以减少很多不必要的循环,但是寻找到合适的方式来从顶点处遍历是个问题,我们可以先循环找到所有需要遍历的点并将其存入数组中,然后遍历整个数组,实现算法的优化。

判断是否一条直线链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
bool CGameControl::LinkInOneLine(Vertex* avPath, int& nVexNum)
{
// TODO: 在此处添加实现代码.
int nCol1 = m_svSelFst.col;
int nCol2 = m_svSelSec.col;
int nRow1 = m_svSelFst.row;
int nRow2 = m_svSelSec.row;
if (nRow1 == nRow2){
for (int i = (nCol1>nCol2?nCol2:nCol1)+1; i <= (nCol1>nCol2 ? nCol1 : nCol2); i++){
if (i == (nCol1 > nCol2 ? nCol1 : nCol2)) {
avPath[nVexNum++] = m_svSelFst;
avPath[nVexNum++] = m_svSelSec;
return true;
}
if (m_pGameMap[nRow1][i] != -1) break;
}
}
else if(nCol1 == nCol2) {
for (int i = (nRow1 > nRow2 ? nRow2 : nRow1)+1; i <= (nRow1>nRow2 ? nRow1 : nRow2); i++){
if (i == (nRow1 > nRow2 ? nRow1 : nRow2)){
avPath[nVexNum++] = m_svSelFst;
avPath[nVexNum++] = m_svSelSec;
return true;
}
if (m_pGameMap[i][nCol1] != -1) break;
}
}
return false;
}

判断是否两条直线连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
bool CGameControl::LinkInTwoLine(Vertex* avPath, int& nVexNum)
{
// TODO: 在此处添加实现代码.
Vertex V1 = m_svSelFst;
Vertex V2 = m_svSelSec;
bool isLink;
if (m_pGameMap[V1.row][V2.col] == BLANK ) {
isLink = true;
//横着是否联通
for (int i = (V1.col > V2.col ? V2.col : V1.col) + 1; i < (V1.col > V2.col ? V1.col : V2.col); i++)
if (m_pGameMap[V1.row][i] != -1) {
isLink = false;
break;
}
//竖着是否连通
for (int i = (V1.row > V2.row ? V2.row : V1.row) + 1; i < (V1.row > V2.row ? V1.row : V2.row); i++)
if (m_pGameMap[i][V2.col] != -1) {
isLink = false;
break;
}

//判断是否有这个路径
if (isLink)
{
Vertex Vmid;
Vmid.row = V1.row;
Vmid.col = V2.col;
Vmid.info = -1;
avPath[nVexNum++] = V1;
avPath[nVexNum++] = Vmid;
avPath[nVexNum++] = V2;
return true;
}
}
if (m_pGameMap[V2.row][V1.col] == BLANK) {
isLink = true;
//横着是否联通
for (int i = (V1.col > V2.col ? V2.col : V1.col) + 1; i < (V1.col > V2.col ? V1.col : V2.col); i++)
if (m_pGameMap[V2.row][i] != -1) {
isLink = false;
break;
}
//竖着是否连通
for (int i = (V1.row > V2.row ? V2.row : V1.row) + 1; i < (V1.row > V2.row ? V1.row : V2.row); i++)
if (m_pGameMap[i][V1.col] != -1) {
isLink = false;
break;
}

//判断是否有这个路径
if (isLink)
{
Vertex Vmid;
Vmid.row = V2.row;
Vmid.col = V1.col;
Vmid.info = -1;
avPath[nVexNum++] = V1;
avPath[nVexNum++] = Vmid;
avPath[nVexNum++] = V2;
return true;
}
}
else return false;
}

判断三条直线是否可以连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
bool CGameControl::LinkInThreeLine(Vertex* avPath, int& nVexNum)
{
// TODO: 在此处添加实现代码.
Vertex V1 = m_svSelFst;
Vertex V2 = m_svSelSec;

//判断是否可以通过三条线进行连接
//先在一竖列上找到一个联通的点,判断每一个点是否可以进行两点连接
for (int i = 0; i < MAX_ROW; i++)
{
m_svSelFst = V1;
m_svSelSec = V2;

Vertex Vmid;
Vmid.row = i;
Vmid.col = V1.col;
Vmid.info = -1;

if ((m_svSelFst.row == Vmid.row && m_svSelFst.col == Vmid.col)||
(m_svSelSec.row == Vmid.row && m_svSelSec.col == Vmid.col))
{
continue;
}

if (m_pGameMap[i][V1.col] != -1)
{
continue;
}
m_svSelSec = Vmid;

if (LinkInOneLine(avPath ,nVexNum) == true)
{
m_svSelSec = V2;
m_svSelFst = Vmid;
if (LinkInTwoLine(avPath, nVexNum) == true)
{
m_svSelFst = V1;
m_svSelSec = V2;
return true;
}
else {
nVexNum = 0;
}
}
else {
nVexNum = 0;
}
}


//先在一横行上找到一个联通的点,判断每一个点是否可以进行两点连接
for (int i = 0; i < MAX_COL; i++)
{
m_svSelFst = V1;
m_svSelSec = V2;
Vertex Vmid;
Vmid.row = V1.row;
Vmid.col = i;
Vmid.info = -1;
if ((m_svSelFst.row == Vmid.row && m_svSelFst.col == Vmid.col) ||
(m_svSelSec.row == Vmid.row && m_svSelSec.col == Vmid.col))
{
continue;
}

if (m_pGameMap[V1.row][i] != -1)
{
continue;
}
m_svSelSec = Vmid;

if (LinkInOneLine(avPath, nVexNum) == true)
{
m_svSelSec = V2;
m_svSelFst = Vmid;
if (LinkInTwoLine(avPath, nVexNum) == true)
{
m_svSelFst = V1;
m_svSelSec = V2;
return true;
}
else {
nVexNum = 0;
}
}
else {
nVexNum = 0;
}
}
m_svSelFst = V1;
m_svSelSec = V2;
return false;
}

2.核心算法的设计

在核心算法中,首先我讲一下整个项目的代码结构,我们先创建了一个Dialog,名为:IDD_LINKGAME_DIALOG,为其创建了一个CLinkGameDlg类,再在其中写所有的文件操作,如右图:由于图片大小限制,我们禁用了他的放大键使其不可调整,还定义了他的图标,名称,除此之外添加了几个按钮用于和用户的下一步交互,第一个按钮可以实现打开一个拥有计时条的页面,第二个按钮可以点开一个没有进度条的娱乐模式,随意玩耍。第三个按钮由于事件原因没有写关卡模式,排行榜是从文件中直接读取,并将其排序后放到页面上即可。排行榜我们单独创建了一个窗口,ID为:IDD_DIG_RANK,类为CRankDlg,这里面实现所有的排序等等功能。
其次,基本模式和休闲模式,我也创建了两个类,分别是IDD_GAME_DIALOG和IDD_ENJOY_DIALOG,这两个也创建了两个类进行处理。代码部分基本相同,只有一些小设置不同,由于娱乐模式和休闲模式只有一个进度条的区别,这里我们使用基本模式进行讲解。这里我大致分为两部分,其中一部分时左上方的消消乐区域,在这个区域任意位置点击,我们都会重新将其所在的行列上的小方块进行画圈。这样可以提高用户的体验度,在右上方有四个按钮,第一个按钮是开始游戏,点击之后会在后台创建一张相应大小的数组,将数据先按照顺序放入,后面使用随机数将其打乱,以保证所有数目都是双数,最后图片可以消完我们将数据存在一个CGameControl类型的m_CGameC变量中,控制所有的后台处理。第一个按钮在开始后会被禁用,防止图片的重复生成。第二个按钮是暂停游戏,点击后会将左边区域覆盖一张风景图片,这样防止玩家在暂停的时候还在偷看(bushi),导致游戏的不平衡,影响选手最终成绩,第三个按钮是提示一下,我们的提示有着次数限制,如果次数到达一定的数量,将不能点击提示,你只能使用第四个按钮,重新排布来进行查找,重新排布也可以设置有时间限制,这样的话就能达到一定的平衡,实现排行榜的准确度。
在页面的下方还有两个按钮,分别是设置和帮助,帮助顾名思义就是介绍了一下这个游戏的玩法,这个只要创建一个MessageBox即可,而设置就需要创新创建一个页面来实现其功能。如右图所示:这里我添加了两个类别的设置,分别是主题设置和音乐设置,音乐有三种不同的背景音乐和开关按钮,音量调节,主题有三种主题,除了主题默认的一种,还有舍友做的另外两种我将其放在了上面增加其可玩性。点击确定按钮后会实现你所做的修改,音乐是使用PlaySound()函数播放音乐,音量调节使用了CSlide控件,调用系统的音乐来实现音量调节功能。下面两张是我的其他两种主题。

以下是判断两个点是否链接的部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool CGameControl::Link(Vertex avPath[MAX_VERTEX_NUM], int& nVexnum)
{
if (m_svSelFst.row == m_svSelSec.row && m_svSelFst.col == m_svSelSec.col )
{
return false;
}
int nInfo1 = GetElement(m_svSelFst.row, m_svSelFst.col);
int nInfo2 = GetElement(m_svSelSec.row, m_svSelSec.col);

//判断两个点是否一致,只有一致才能消除
if (nInfo1 != nInfo2 || nInfo1 == BLANK || nInfo2 == BLANK) {
return false;
}

//现在实现点和点之间的判断;
if (LinkInOneLine(avPath,nVexnum)||
LinkInTwoLine(avPath,nVexnum) ||
LinkInThreeLine(avPath, nVexnum))
{
m_pGameMap[m_svSelFst.row][m_svSelFst.col] = -1;
m_pGameMap[m_svSelSec.row][m_svSelSec.col] = -1;
return true;
}

return false;
}

三、主要仪器设备及耗材

1.安装了Windows 11或其它版本的Windows操作系统的PC机1台

2.PC机系统上安装了Microsoft Visual Studio开发环境

第二部分:实验过程和结果(可加页)

一、源代码

请看链接里面的代码仓库,自己写的,可能有点乱,但是能用,不要嫌弃

MFC实现快乐连连看

二、调试说明(调试手段、过程及结果分析)

在调试的过程中,我出现了一些问题,比如在处理修改主题时,如果提前没有开始游戏会导致游戏的卡退,经过调试,我发现,是我采用的方法导致的问题,我的方法在修改主题页面消失后会重新初始化元素,但是此时我们的m_dcMask并没有选入位图中,所以会使这几行初始化无法实现,最终卡退,于是我添加了一个参数来判断游戏是否正在进行,如果不在进行将不能点击设置按钮进行修改,当然这样做也会妨碍背景音乐的设置,所以,在开始游戏之前,我们也可以点击主页中的设置提前设置我们的主题和背景音乐,在哪里我们的元素已经初始化,所以不会影响设置页面的设置,实现主题的变更实际上只是实现了元素图片路径的变更,我们在主页中写的函数只修改了路径,所以并不用加载图片,所以不会造成这些问题。还有一个问题就是图片的重加载导致的堆叠,这个方面,我想了很多方法,本来是在更新之前将所有的背景全部加载,但是思考之后发现这样所占用的算力比较大(虽然不影响),所以我在可能会堆叠的位置代码部分单独添加了一次重绘背景的代码,其余地方只是画所在小方框内一点点,来节约一点点算力。除此之外还有很多问题,比如计时器,进度条,表格的编辑,文件的读取,最终在我和室友的探讨下完成了这个实验报告。

第三部分:实验小结、收获与体会

本次实验我学习了如何使用C++的MFC控件来完成一个简单的游戏。在此过程中,我深刻认识到了软件开发的重要性和团队协作的必要性。
首先,通过这个实验,我学会了使用MFC控件创建窗口、按钮、标签等控件,并进行事件响应处理。我还学习了基本的图形界面设计思路,如窗口大小、字体颜色等设置。同时,我也学习到了如何通过类的继承来简化代码,提高程序可维护性。
其次,在整个实验的过程中,组内成员之间的沟通和协作非常重要。我们需要共同商讨程序的架构和功能,分工合作,不断地迭代完善。在实现的过程中,我们发现有些模块需要相互配合,因此需要耐心沟通,解决问题。通过这种方式,我们最终成功地完成了这个小游戏。
通过本次实验,我认识到了软件开发的重要性。软件开发并不是简单的“敲代码”,它需要我们有较强的逻辑思维能力、良好的编码习惯以及对整个项目的全局把握能力。另外,我也认识到了良好的团队协作能力对于软件开发的重要性。只有大家能够互相信任、互相支持,才能够顺利完成项目。
总之,这次实验收获颇丰。通过这个实验,我学会了使用MFC控件,同时也锻炼了我的团队协作和解决问题的能力。我相信这些技能和经验将对我的未来职业发展有很大的帮助。