扫雷
0
目录
-
前言
-
游戏三部曲
-
游戏设计
-
函数说明
-
程序打包
1
前言
终极目标:打造多关卡扫雷游戏
制作环境:
VS2015
支持:VC++2010 VS各个版本
easyx图形库(点我)
一直想发表扫雷这种锻炼思维的游戏,其实扫雷弄个标题栏可以随意选择挑战难度是效果最佳的,但是呢easyx图形库没有标题栏,所以就委屈各位看官一级一级打上去了。由于调试过程中玩的次数太多了,录视频就录这几关吧,后面的难度递增,需要你们去挑战了。
这篇文章会很长,但是,绝对干货,多知识点整合,也是数组的应用,外加一个递归调用。希望你们能够看到最后并制作这个有趣的游戏。然后~~~发给同学装×。
2
游戏设计
首先,我们来看一下游戏布局
游戏外圈我又加了一圈,这一圈是为了防止递归调用时出现无法访问内存的情况
当我们点击第一个格子的时候,如果是数字 0 也就是空白,那么需要递归自动翻开九宫格内的格子(否则玩家一个格子一个格子的点毫无游戏体验感),如果没有外圈,就会出现无法访问的情况,所有定义数组的时候要空出一片区域。
游戏设计:
设计 | 代表数字 |
---|---|
未翻开的格子 | 0 |
雷所在格子 | -1 |
周围雷的数量(隐藏) | 1~8 |
踩到雷 | 9 |
空白~八个雷 | 10~18 |
标记 | >18 |
然后的数组应用和 拼图 类似
-
定义一个足够大的数组和控制游戏地图大小的变量
-
定义雷的数量和翻开的次数
int map[30][30] = { 0 }; //地图 用map_row map_col控制地图大小
int map_row, map_col; //地图大小
int times; //翻开的此时
int mine; //雷的数量
-
初始化窗口大小
initgraph(map_col * 25, map_row * 25); //初始化地图
-
设置雷的位置
int mine_r, mine_c; //雷所在行和列
for (int i = 0; i < mine;)
{
mine_r = rand() % map_row + 1; //mine_r 1 ~ 8
mine_c = rand() % map_col + 1;
if (map[mine_r][mine_c] == 0)
{
map[mine_r][mine_c] = -1; //在数组中放雷
i++; //放入雷后雷的数量加一
}
}
-
计算空白处雷的数量
for (int i = 1; i <= map_row; i++)
{
for (int j = 1; j <= map_col; j++)
{
if (map[i][j] != -1)
{
//检查周围雷的个数
for (int m = i - 1; m <= i + 1; m++)
{
for (int n = j - 1; n <= j + 1; n++)
{
if (map[m][n] == -1) { //附近有雷,数字加一
map[i][j]++;
}
}
}
}
}
}
这样,一个扫雷游戏基本设计就完成了,接下来设计鼠标操作
-
定义一个鼠标消息,然后获取这个鼠标消息
MOUSEMSG msg;//鼠标消息
int row, col;//鼠标点击的行和列
msg = GetMouseMsg(); //获取鼠标消息
row = msg.y / 25 + 1; //鼠标点击行
col = msg.x / 25 + 1; //鼠标点击列
-
对鼠标消息进行相应的数组操作
switch (msg.uMsg)
{
case WM_LBUTTONDOWN://左键按下
break;
case WM_RBUTTONDOWN://右键按下 WM_RBUTTONDOWN
break;
}
-
递归在扫雷中的应用
void Empty(int row, int col)
{
//递归实现以 0 为 九宫格的区域 自动翻开
map[row][col] += 10; //翻开当前位置
times++; //翻开次数+1
for (int i = row - 1; i <= row + 1; i++)
{
for (int j = col - 1; j <= col + 1; j++)
{
//外圈空白区域不操作 仅对可以看到的区域进行递归调用
if (i >= 1 && i <= map_row && j >= 1 && j <= map_col)
{
if (map[i][j] < 9) //小于9 是未操作,未翻开的区域
{
if (map[i][j] == 0) //0的周围九宫格内无雷,翻开
{
Empty(i, j);
}
else
{
map[i][j] += 10; //有雷的不翻,让玩家思考是否需要翻开
times++;
}
}
}
}
}
}
3
游戏三部曲
1、加载游戏数据(初始化 init();)
2、绘制图形(绘图 DrawMap();)
3、玩家操作(数据更新 Play();)
init(); //初始化
DrawMap(); //绘图
while (1)
{
Play(); //玩家操作
Judg(); //判断输赢
}
先把大纲列出来
01
主函数 main()
int main()
{
init(); //初始化
DrawMap(); //绘制图形
while (1)
{
Play(); //玩家操作
Judg(); //判断输赢
}
getchar();
closegraph();
return 0;
}
02
初始化函数 GameInit()
需要初始化的数据:
-
地图的行和列map_row,map_col
-
素材图片 loadimage();
-
地图数据
这里我将地图数据单独拿出来初始化,是为了设计多关卡
srand((unsigned)time(NULL)); //设置随机种子
map_row = 10; //地图行
map_col = 10; //地图列
isfail = 0;
pass = 0;
//加载图片
loadimage(&imgs[0], L"素材/0.bmp", 25, 25);
loadimage(&imgs[1], L"素材/1.bmp", 25, 25);
......
loadimage(&imgs[13], L"素材/雷2.bmp", 25, 25);
initmap();
03
绘图函数 DrawMap()
地图的绘制就要考虑外圈空出来的区域不能绘制出来
void DrawMap()
{
BeginBatchDraw();
for (int y = 1; y <= map_row; y++)
{
for (int x = 1; x <= map_col; x++)
{
if (map[y][x] > 18)
{
//标记部分额外处理
putimage((x - 1) * 25, (y - 1) * 25, &imgs[9]);
continue;
}
switch (map[y][x])
{
//制作初期可以加上下面代码看看雷放到了哪里
//case -1: //雷
// putimage(x * 25, y * 25, &imgs[12]);
// break;
case 9: // -1 + 10 = 9 踩到雷了
putimage((x - 1) *25, (y - 1) *25, &imgs[13]);
isfail = 1;
break;
case 10: //没有雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[0]);
break;
case 11: //有一个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[1]);
break;
case 12: //有两个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[2]);
break;
case 13: //有三个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[3]);
break;
case 14: //有四个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[4]);
break;
case 15: //有五个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[5]);
break;
case 16: //有六个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[6]);
break;
case 17: //有七个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[7]);
break;
case 18: //有八个雷
putimage((x - 1) * 25, (y - 1) * 25, &imgs[8]);
break;
default: //未点击翻开d额格子
putimage((x - 1) * 25, (y - 1) * 25, &imgs[10]);
break;
}
}
}
EndBatchDraw();
}
04
数据更新函数 Play()
当玩家对鼠标进行点击操作(分左键和右键)时,进行相应的数据处理
switch (msg.uMsg)
{
case WM_LBUTTONDOWN://左键按下
if (map[row][col] >= 9)//翻开过的 和标记的 不进行操作
{
break;
}
if (map[row][col] == 0)//翻开 0项 和 周围的图片
{
Empty(row,col);
}
else//翻开点击的图片
{
map[row][col] += 10;//翻开
times++; //翻开次数 +1
}
break;
case WM_RBUTTONDOWN://右键按下 WM_RBUTTONDOWN
if (map[row][col] >= 9 && map[row][col]<19)//翻开的不进行操作
{
break;
}
if (map[row][col] >= 19)//标记的取消标记
{
map[row][col] -= 20;
}
else//没有翻开的加20
{
map[row][col] += 20; //20是随意的数字,只要足够大就行
}
break;
}
数据处理完成之后记得更新地图
DrawMap();
05
判断输赢
判断输赢为了避免重复的搜索,定义了一个isfail变量来判断输,根据翻牌次数和雷的数量及地图大小判断赢。
当绘制出现 9 即踩到雷时将isfail置一
case 9: // -1 + 10 = 9 踩到雷了
putimage((x - 1) *25, (y - 1) *25, &imgs[13]);
isfail = 1;
判断输赢是提供是否进入下一关,和是否重新开始本关卡。
void Judg()
{
//先查找是否点开了雷
if (isfail)
{
if (MessageBox(GetHWnd(), L"你踩到雷了,是否重新开始本关", L"失败", MB_YESNO) == IDYES) //GetHWnd()是获取一个顶置窗口
{
mine -= 5;
isfail = 0;
initmap();
DrawMap(); //渲染地图
}
else
{
_exit(0);
}
}
if (times == map_row * map_col - mine) //所有点 - 雷 = 非雷区
{
if (MessageBox(GetHWnd(), L"是否进入下一关", L"成功", MB_YESNO) == IDYES) {
closegraph();
//重新开始游戏
initmap();
DrawMap(); //渲染地图
}
else
_exit(0);
}
}
4
函数说明
-
exit(0)
exit(0):退出程序 有缺陷,建议使用新版_exit(0);
-
GetHWnd()
GetHWnd():获取顶层窗口,让弹框至于最前面,不至于让人忽略
然后也没有其他特殊的函数需要说明,如果对easyx里面的基本函数不清楚建议参考easyx提供的文档,在文档里面有每个函数的使用例子,阅读文档是个好习惯。
5
程序打包
什么是程序打包?
你安装软件的时候有安装向导,就是把可执行文件(.exe文件)和依赖库(包括素材)保存在一个目录下,并创建桌面链接。