目录
写在前头
这姑且也算是笔者第一篇博客,写来还是多少有点小激动的,一口气写了上万字,希望各位能积极点赞关注收藏留言与作者互动,让作者体验一下csdn社区的良好氛围
你是否还在为整日面对着无聊的黑色窗口感到厌倦?
你是否对游戏制作感兴趣却无从下手而感到苦恼?
亦或是不知道自己该学点什么?
我向你强烈推荐这款Easyx库,这款功能极其强大的库能满足你对动画(绝大部分)的想象,还能作为一个技能点常亮于你的技能树之上。
所以这篇文章就将带领大家完成一个基于easyx库的扫雷项目,在这篇文章中你可以学到包括但不限于:
1、库的下载与部分函数的使用 2、扫雷的基本逻辑与实现 3、基于easyx库的图片载入
4、文件打包
在这里不得不说的是,这篇文章会教一些图形化实现,如果你不想看扫雷的逻辑可以直接跳转至图像化实现。但这篇文章不会拘泥于对easyx库函数的解释,更多的是一个运用实例,如果你想了解更详细的easyx函数请移步至easyx库文档,此外作者认为学习函数并调用他们去完成一个项目远比单调的背函数学到的东西更多,这个项目的品尝方法有很多,比如直接用已有的逻辑自行实现图形化,或者把本应由鼠标点击输入方法改为键盘输入,亦或是看看已完工的项目的代码和自己有什么差别,其中作者对每段代码都进行了解读,可能过于冗长,所以作者最建议的是,直接去看项目的总代码,自行理解与实操,有不懂的地方再回去看作者解读,还有记得点赞关注评论收藏转发希望读者们自行选择适合自己的学习道路。
其中头文件主要为声明和少量解释,main文件是整个扫雷的实现流程,game文件是各种函数
该项目的完成离不开easyx库,在此表达对easyx库作者及各位大能的感谢,也希望各位多多了解easyx库,遇到各类问题也可以去easyx库官网查看解决方法easyx官网
PS:有任何问题或学术交流加Q:2908692481,来找作者玩。
图片文件和游戏本体都在这:作者真是大公无私啊,快快点赞 提取码:hjnb
最终效果预览
我们的最终结果大致是

有时间模块,也比较像传统的扫雷,作者认为这个扫雷放眼全网已经算完成度蛮高也蛮漂亮的了。
正文
还是碎碎念
在这个项目中笔者收获良多、其中最重要几点需要在讲解开始前告诉大家:
1、在完成项目的过程中,一定要有一个良好的思路,即明确整一个实现过程,比如先写完扫雷最核心的功能,再去研究鼠标的读入,再去研究图片的导入与输出。明确当前工作目标是保证工作速度的基础。
2、在完成项目的过程中,一定要多写批注,保证项目的可读性,至少你察觉某个模块可能出问题时,你得很多地找出这个模块。
前期工作(库的下载)
在这里浅谈easyx库的下载
1、打开easyx库官网,并点击红色的大大的下载按钮
2、下载完成后点开安装包,easyx库的安装包会自动识别安装路径,点击安装即可
3、然后你就可以打开你的vs创建项目,添加头文件#include<graphics.h>开始写代码啦。
扫雷逻辑实现
对该章节有任何问题请读者自行移步至扫雷网页版查看。
也欢迎来找作者讨论,加Q:2908692481
局前操作
格子生成
首先我们要明白扫雷中一个格子有多少个属性,作者认为有4个,分别是:
1、他有没有被点开 2、他是不是炸弹 3、他有没有被插旗子 4、他周围有多少个炸弹
然后就可以创建一个类,即对应着每一个格子
struct ele {
int data;
bool know, bomb, flag;
};
extern ele el[12][12];
因为这段代码是写在头文件的所以在声明el数组前要加上extern,等到时候作者有时间会写一篇关于头文件使用的心得,如果嫌麻烦可以直接全部代码写在一个源文件里面,这样就可以不用加extern。(12*12的格子是为了防止越界) 。
//刷新数值
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 12; j++) {
el[i][j].data = 0;
el[i][j].bomb = 0;
el[i][j].flag = 0;
el[i][j].know = 0;
}
}
每次游戏开始即没生成炸弹前,每个格子都不是炸弹、没被插旗子、没被点开、数字也为0(即周围有0个炸弹)。
地雷生成
然后可以开始生成地雷,作者选用了c++自带的rand()语句(贪图方便)
随机一个x,再随机一个y,如果这个格子不是地雷(即bomb==0)使得他变成炸弹,四周围格子中数字++,如果是炸弹就不进行以上操作。
//创造炸弹
srand(time(0));
int x, y;
for (int i = 0; i < 10; i++) {
x = 1 + rand() % 9;
y = 1 + rand() % 9;
if (el[x][y].bomb) {
i--;
continue;
}
int j, k;
for (j = -1; j <= 1; j++) {
for (k = -1; k <= 1; k++) {
if (j == 0 && k == 0)el[x + j][y + k].bomb = 1;
else el[x + j][y + k].data++;
}
};
}
其中srand()是放入随机种子,有srand()才能用rand()生成随机数,我把随机种子定为了time(0),即1970年1月1日00:00:00至当前的秒数,他是持续变化的,所以不用担心生成重复的结果。
游戏结束判定
怎么样才算游戏结束呢,很简单,就是把格子全都开完了,一个9*9棋盘有81个格子,有10个炸弹,也就是要打开71给格子才算胜利
extern int allspace;
allspace = 0;
设置当前已开格子为0个,循环格子数小于71,持续接收开格子的信号
while (allspace < 71) {
游戏内容
}
局前操作合并
如果你看到这里,那么恭喜你,你已经完成了所有的开局操作,我们可以把上述操作合并一下:
void refresh() {
//刷新数值
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 12; j++) {
el[i][j].data = 0;
el[i][j].bomb = 0;
el[i][j].flag = 0;
el[i][j].know = 0;
}
}
//刷新空格子数
allspace = 0;
//创造炸弹
srand(time(0));
int x, y;
for (int i = 0; i < 10; i++) {
x = 1 + rand() % 9;
y = 1 + rand() % 9;
if (el[x][y].bomb) {
i--;
continue;
}
int j, k;
for (j = -1; j <= 1; j++) {
for (k = -1; k <= 1; k++) {
if (j == 0 && k == 0)el[x + j][y + k].bomb = 1;
else el[x + j][y + k].data++;
}
}
}
}
局中操作
扫雷的操作并不复杂,无非就是开格子,插旗子,其中又分为了以下几种。
1、如果一个格子没被开过也没插旗子:
左键他就会开启他,如果他是炸弹游戏结束,不是炸弹就开启这个格子,如果他是数字0那就自动开四周的格子(因为他周围一定没有炸弹所有帮你开好了)如果他不是数字0就停止继续开四周的格子。
右键他就会为他插上旗子。
2、如果一个格子没被开过插了旗子:
左键他没有反应(旗子的作用就是防止误触与标识)。
右键他就会取消他的旗子。
3、如果一个格子被开过了:
左键他没有反应。
右键他的话(这是一个扫雷的小技巧吧,不实现也不影响扫雷的游玩)如果他在周围的旗子数和他显示的数字一样,那么就相当于你告诉系统,你已经猜好雷的位置了,就会开启周围九个中所有没插旗的格子,如果其中有炸弹那么直接游戏结束。
综上,局中的操作就只有两种,左键或右键一个格子。
创造一个循环,循环接受点击直到游戏结束。
以下操作均为接受到了一个点击,点击坐标为x,y,后续会讲到鼠标接收,可以自行跳转至讲鼠标接受的章节,也自行改为用键盘输入。
此处坐标为理想坐标而非实际画布坐标,注意区分
左键操作
左键操作各种情况可直接参考前文
if (/*是左键点击的*/) {
if (!el[x][y].flag)
{
if (el[x][y].bomb)break;
else open(x, y);
}
}
其中这个break会跳出循环,导致游戏提前结束。
开启操作参考前文。
open()代码如下:
void open(int x, int y) {//连锁挖矿
if (x < 1 || x > 9 || y < 1 || y > 9) return;//检查边界
//如果他是炸弹或已经被开过或是旗子,就不用继续开了
if (el[x][y].bomb || el[x][y].know || el[x][y].flag)return;
else if (el[x][y].data) {
el[x][y].know = 1;
allspace++;
}
else {
el[x][y].know = 1;
allspace++;
int i, j;
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i != 0 || j != 0)open(x + i, y + j);
}
}
}
}
右键操作
右键操作各种情况可直接参考前文。
else {
if (el[x][y].flag)el[x][y].flag = 0;
else if (!el[x][y].know)el[x][y].flag = 1;
else {
mouseback abc;
abc = open2(x, y);
if (abc.bb) {
x = abc.xx;
y = abc.yy;
break;
}
}
}
相信最后那个else大家肯定会看的一头雾水,不必惊慌,接着往下看。
先弄懂上面两个if,如果有旗变没旗,如果没旗还没开,就变有旗子。
其中mouseback是给类,类中有三个属性,int xx,int yy,bool bb;
第三个与后续图形化有关,因为踩到的雷要变成红色(普通雷为黑色)所有专门返回一个x和一个y
只需要知道,open2会开启他周围的8个格子,开启过程中如果没碰到雷,那么返回的bb为假,就不会提前break(结束游戏),如果开启过程中碰到了雷,返回bb为真,同时把踩到的雷的位置返回,这样就可以同时知道踩到了哪个雷(以便标红),又知道是不是结束了游戏。
open2的代码如下:
mouseback open2(int x, int y) {
int fg = 0, i, j;
mouseback abc = { 0,0,0 };
//会统计他周围的旗子数
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (el[x + i][y + j].flag)fg++;
}
}
//如果旗子数量和格子中的数字一样,那就开了附近8个格子
if (fg == el[x][y].data) {
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i != 0 || j != 0) {
if (!el[x + i][y + j].flag) {//如果是旗子就不开
if (!el[x + i][y + j].bomb) open(x + i, y + j);//如果不是炸弹就正常开
else {//如果是炸弹就返回bb为真
abc.xx = x + i;
abc.yy = y + j;
abc.bb = 1;
}
}
}
}
}
}
return abc;
}
逻辑操作合并
struct ele {
int data;
bool know, bomb, flag;
};
ele el[12][12];
int allspace = 0;
int main() {
int x, y ;
refresh();//创造炸弹
while (allspace < 71) {
//在这里接受点击,区分左右键,需要补充代码捏
//以下是根据点击进行操作
if (mb.bb) {
if (!el[x][y].flag)
{
if (el[x][y].bomb)break;
else open(x, y);
}
}
else {
if (el[x][y].flag)el[x][y].flag = 0;
else if (!el[x][y].know)el[x][y].flag = 1;
else {
mouseback abc;
abc = open2(x, y);
if (abc.bb) {
x = abc.xx;
y = abc.yy;
break;
}
}
}
}
}
如果你看到了这里,恭喜你,你已经完成了学习完所有逻辑上的操作,接下来只要学习鼠标点击和图形化就好了
图形化实现
这里,我将会开始开始教学如何制图、读入与输出图片、输出文字
对任何代码感到不解可移步至easyx官网的函数说明文档,对任何库任何语言来说,善用函数说明文档可是能事半功倍的,也欢迎来找作者讨论,加Q:2908692481。
基础介绍
首先对一些基础概念进行粗略介绍。
要记住的是,其实easyx库里面很多函数的作用就是字面意思,这点真的很重要。
颜色
就像小学上美术课教的一样,任何颜色都是通过三原色混合而成,电脑也一样,只不过三原色分别是红、绿、蓝。用RGB(int red,int green,int blue)表示,每种颜色都可以用rgb来合成出来,参数范围是0——255,当然也有一些颜色比如“BLACK”就等价于RGB(0,0,0),可以直接用的。
还有诸如“RED”,“GREEN”等,可以移步至easyx库文档查看

坐标
我们用以下函数创造出画布
initgraphic(480,640/*,EX_SHOWCONSOLE*/)
大小就是480*640个像素,其x轴和我们所熟知的一样,只不过y轴掉了过来。
值得注意的是,画布出现时我们的命令窗口直接消失了,其中被注释掉的EX_SHOWCONSOLE就是用来显示命令提示窗的。
PS:如果出现了闪一下的情况可以加上代码 system("pause");
文字输出
图片导入与输出
这里我们要先定义一些用于存放图片地址的变量
IMAGE k[9], uk, face, f[2], b[2], t[10];
然后调用easyx库内函数 loadimage(IMAGE* img, L"路径", x轴长度, y轴长度);
不填xy轴长度,图片原本是多少像素他就是多少像素,单独不填y轴就按比例缩放。
至于路径,被分为绝对路径、相对路径,一般推荐用相对路径,毕竟每个人存放图片的绝对路径大多都不一样嘛。
绝对路径即,打开你的文件管理器,点一点这个条条,就可以看见这堆东西,从盘符开始,一直到文件名字为止(这里只是到文件所在的文件夹位置,还需加上文件比如图片名字.jpg这样)。
E:\vs\马赛克\马赛克\pic\k10.jpg

至于相对路径即文件相对当前cpp文件或exe文件的位置,比如说

作者将他们放到了一个盘里,路径和读取代码就是
loadimage(k, L"./pic/k10.jpg", 16 * MUL, 16 * MUL);
上下两个路径对我的电脑来说是等价的,但对你的电脑不一定,因为你存的地方可能不是e盘。
记得k作为一个数组开头,视为一个指针,然后L是为了防止报错加就对了,./是当前exe所在文件夹的意思,同一个文件夹下的(./)文件夹(pic/)里的图片(k10.jpg)就是他的相对路径了,至于MUL是multiplicative倍数的缩写,是用于统一管理应用图像化大小的。
因为要导入很多张图片我就不在这里写出代码了,大家可以自行移步至后文的总代码处,此处只需要知道数组k内存放着数字图片,uk存放着没被打开的格子图片,face存放着作者的帅脸,f数组即旗子图片,b数组即炸弹图片,t数组即时间数字图片。
输出图片的easyx库内函数呢是putimage(int x,int y, IMAGE *img);
x,y就是输出的图片的左上角坐标,*img就是图片存放的地址了。
为了方便输出作者自行打包了以下函数(就比如我想输出el[1][1]的图像,就不需要再写xy轴了,每次都要算很麻烦的)
//由于数组[x]中的内容要画在x-1格内,所以以下均有x=x-1.y=y-1
//画一个旗子,其中n==0时为普通旗子,==1时为插做了的旗子
void flag(int x, int y, int n) {
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, f + n);
}
//画一个未被开过的格子
void rec(int x, int y){
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, &uk);
}
//画一个被开过的格子
void num(int x, int y, int num) {
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, k + num);
}
//画一个炸弹,其中n==0时为普通炸弹,==1时为踩到的炸弹
void bomb(int x, int y, int n) {
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, b + n);
}
//作者专属大帅脸
void fc() {
putimage(68 * MUL, 8 * MUL, &face);
}
文字的输出
文字输出有许多函数,作者贪图方便所以做了一个打包
//输出一段文字str,居中于角坐标分别为x,y,dx,dy的矩形,字体大小为b
void str_out(int x, int y, int dx, int dy, LPCTSTR str, int b) {
settextcolor(RGB(0, 0, 0));
settextstyle(b * MUL, 0, L"宋体");
outtextxy(((dx - x) * MUL - textwidth(str)) / 2, ((dy - y) * MUL - textheight(str)) / 2, str);
}
第一句就字面意思,设置set文字text文字color颜色(),这个函数只有一个参数,参数为前面颜色那里讲到的。
第二句其实也是字面意思,设置(set)文字(text)格式(style),函数有三个参数,第一个为字体高度,第二个为字体长度,如果填0就自适应,第三个参数比较复杂,是字体格式,简单用可以按作者的用法
L”电脑内自带的任意字体名字“,复杂的话可以设定粗细啊弧度啊,可以移步至easyx库文档查看
第三句也是字面意思输出(out)文字(text)到左上角坐标为xy的地方,那么这个函数有三个参数,第一个参数为左上角的x轴坐标,第二个参数为左上角的y轴坐标,第三个参数为要输出的字符串。
在第三句中,作者又用到了两个函数,分别的textwidth()和textheight(),他们都只有一个参数即一个字符串,作用是计算出这个字符串的长度和宽度,这就不得不提到局中输出的原理

图中我们可以直观的看到,外长的一半减内长的一半就是左上角的x轴坐标了,这个是个数学问题,没什么好解释的。
需要强调的是
可以看见其实easyx库里面很多函数的作用就是字面意思,这点真的很重要
因为作者希望防止越界,所以第一行第一个格子的是el[1][1]而不是el[0][0],而把图片放出来的时候却是要从原点从出发即0,0,所以需要有这个x-=1,y-=1的环节,算是一个设计的小失误了。
最终效果预览
我们的最终结果大致是

所以他被分成了四个区域,分别是左上角的文字区,中间的帅脸区,右边的计时区,下面的游戏区
初始化图像
void graph() {
initgraph(160 * MUL, 200 * MUL);
setbkcolor(RGB(181, 181, 181));//设定颜色
cleardevice();
loading();
fc();
str_out(0, 0, 68, 24, L"滑稽出品", 10);
}
其中easyx库内函数setbkcolor()是用于设置背景颜色的,在设置完背景颜色后,原来的背景颜色不会自己消失所以需要easyx库内函数cleardevice()清除原来背景颜色(这样用就对了)
初始化后图形大概是这样的:

绘制格子
我们只需要根据el数组中的内容调用上面图片导入与输出环节打包好的的函数就好了(不知道的自行移步),代码如下:
void outgraph() {
clearrectangle(0, 40 * MUL, 160 * MUL, 200 * MUL);
int x, y;
for (x = 1; x < 10; x++) {
for (y = 1; y < 10; y++) {
if (el[x][y].flag)flag(x, y, 0);
else if (!el[x][y].know)rec(x, y);
else num(x, y, el[x][y].data);
}
}
}
MUl为放大倍率。
如果他被插了旗子就在对应位置输出一个旗子,如果他没被开过就在对应位置输出,以此类推。
语句clearrectangle()是清除一个矩形范围内的所有内容,四个参数分别是这个矩形的四条边的xy轴坐标。由于我们只需要清除游戏区域内的内容,不需要清除作者的帅脸,所有选用该函数。
然后每次接受一个点击时都刷新一次游戏区域就好,也就是设置一个循环(点击,然后判断,然后刷新界面),效果大致如下


绘制结算界面
在游戏结束后,希望在左上角文字区域,输出输赢,并且显示所有炸弹等等。
其实就只需要清除左上角文字(用clearrectangle()即可),然后再输出一段文字。
而对于游戏界面刷新和绘制格子部分差不多,只不过需要多判断这个旗子插对了还是插错了,踩到的炸弹要变成红色(如果是开完全部格子即allspace==71那就肯定不是踩到炸弹了,不然最后一个开的格子肯定就是炸弹,把他改成红色就行)
以下是代码:
void answer(int dx, int dy) {
clearrectangle(0, 40 * MUL, 160 * MUL, 200 * MUL);
int x, y;
for (x = 1; x < 10; x++) {
for (y = 1; y < 10; y++) {
if (el[x][y].flag) {
if (el[x][y].bomb)flag(x, y, 0);//如果是炸弹说明旗子插对
else flag(x, y, 1);//如果不是炸弹说明旗子插错了
}
else if (el[x][y].bomb)bomb(x, y, 0);
else if (!el[x][y].know)rec(x, y);
else num(x, y, el[x][y].data);
}
}
if (el[dx][dy].bomb)bomb(dx, dy, 1);//画一个红色炸弹
//以下为输出文字
clearrectangle(0, 0, 68 * MUL, 40 * MUL);
if (allspace >= 71)str_out(0, 0, 68, 40, L"你赢了", 20);
else str_out(0, 0, 68, 40, L"你输了", 20);
}
效果图大致如下:


合并代码
int main() {
int x, y, start;
graph();//创造画布
refresh();//创造炸弹
while (allspace < 71) {
outgraph();
//这里是接受一个点击的代码,被省略掉了
if (mb.bb) {
if (!el[x][y].flag)
{
if (el[x][y].bomb)break;
else open(x, y);
}
}
else {
if (el[x][y].flag)el[x][y].flag = 0;
else if (!el[x][y].know)el[x][y].flag = 1;
else {
mouseback abc;
abc = open2(x, y);
if (abc.bb) {
x = abc.xx;
y = abc.yy;
break;
}
}
}
}
answer(x, y);//输出答案与结果
}
鼠标输入与时间输出
这两个模块都多少有点难度所有我决定把他们单独拿出来讲
鼠标输入
为了把鼠标读到的x轴值和y轴值和是用左键点击还是用右键点击的信息传回到主函数,先创建一个类。
struct mouseback {
int xx, yy;
bool bb;
};
然后我们要定义一个ExMessage类型的变量msg用于存放鼠标信息。
再用peekmessage()函数将信息存放至msg变量中。
ExMessage msg;
peekmessage(&msg, EX_MOUSE)
值得注意的是ExMessage变量也可用于存放键盘信息等信息,但在此我们只用来存放鼠标信息。
peekmessage函数的第一个参数是一个ExMessage类型变量的地址,用于存放读到的信息,第二个参数是告诉他该函数只读鼠标信息,如果想读其他类型信息可移步至easyx库文档了解详情。
peekmessage函数在没有读到东西是返回假,读到东西才返回真
打包的鼠标读入函数如下:
mouseback get_click() {
mouseback mb;
ExMessage msg;
while (1) {
while (peekmessage(&msg, EX_MOUSE)) {
//以下
if (msg.message == WM_LBUTTONDOWN || msg.message == WM_RBUTTONDOWN) {
mb.xx = msg.x;
mb.yy = msg.y;
if (msg.lbutton && !msg.rbutton) {
mb.bb = 1;
return mb;
break;
}
else if (!msg.lbutton && msg.rbutton) {
mb.bb = 0;
return mb;
break;
}
}
//以上
}
Sleep(40);
}
}
只建议修改两行注释间的代码和循坏外的代码,因为这个循环是从easyx官网抄过来的,然后如果随意修改可能会出现误读或一次点击多次读取的情况,这个问题与计算机组成原理i/o部分有关,作者还没学完捏。
循环,如果读到了鼠标信息就进入内层循坏msg.message是指msg的鼠标信息,等于WM_LBUTTONDOWN意为鼠标的左键被点击了,等于WM_RBUTTONDOWN意为鼠标右键被点击了,如果鼠标左键被点击了,msg.lbutton==1,否则==0,rbutton即右键。
然后我们只需要把读入的xy和左右键返回到主函数,然后根据实际xy坐标推算出理论xy(即数组el的xy)然后进行格子的开就好了,代码如下:
int main() {
int x, y, start;
mouseback mb;
graph();//创造画布
refresh();//创造炸弹
while (1) {
while (allspace < 71) {
outgraph();
while (1) {
mb = get_click();
if (mb.xx >= 8 * MUL && mb.xx <= 152 * MUL && mb.yy >= 48 * MUL && mb.yy <= 192 * MUL) {
break;
}
else if (mb.xx >= 68 * MUL && mb.xx <= 92 * MUL && mb.yy >= 8 * MUL && mb.yy <= 32 * MUL) {
refresh();
continue;
}
}
//以上是接受一个点击,以下是根据点击进行操作
x = ((mb.xx - 8 * MUL) / 16 / MUL) + 1;
y = ((mb.yy - 48 * MUL) / 16 / MUL) + 1;
if (mb.bb) {
if (!el[x][y].flag)
{
if (el[x][y].bomb)break;
else open(x, y);
}
}
else {
if (el[x][y].flag)el[x][y].flag = 0;
else if (!el[x][y].know)el[x][y].flag = 1;
else {
mouseback abc;
abc = open2(x, y);
if (abc.bb) {
x = abc.xx;
y = abc.yy;
break;
}
}
}
}
answer(x, y);//输出答案与结果
while (1) {//重新开始
mb = get_click();
if (mb.xx >= 68 * MUL && mb.xx <= 92 * MUL && mb.yy >= 8 * MUL && mb.yy <= 32 * MUL) {
refresh();
clearrectangle(0, 0, 68 * MUL, 40 * MUL);
fc();
str_out(0, 0, 68, 24, L"滑稽出品", 10);
break;
}
}
}
}
我们可以看到,其实说白了就是点了一个位置,判断那个位置算是哪个格子内,在哪个格子内就对哪个格子进行操作,此外还添加了和原版扫雷差不多的功能,只需要点击以下作者的帅脸,就可以重新开始游戏。
时间输出
作者选用了在每回合游戏开始之前保存当前时间,即:
int start=time(0);
然后在每次需要输出时间是再读取一次当前时间,与开始时间相减就是游戏所用的时间了
然后每次输出时间时都刷新一次输出时间位置就行,代码如下:
void time_out(int start){
clearrectangle(92 * MUL, 0, 160 * MUL, 40 * MUL);
int x1, x2, x3, x4, x = time(0) - start;
x1 = x % 10;
x2 = (x % 60) / 10;
x3 = (x / 60) % 10;
x4 = (x / 60) / 10;
putimage(99 * MUL, 9 * MUL, t + x4);
putimage(111 * MUL, 9 * MUL, t + x3);
putimage(129 * MUL, 9 * MUL, t + x2);
putimage(141 * MUL, 9 * MUL, t + x1);
setfillcolor(RGB(255, 0, 0));
solidcircle(126 * MUL, 15 * MUL, 2 * MUL);
solidcircle(126 * MUL, 25 * MUL, 2 * MUL);
}
x1,x2,x3,x4分别是计算位数的,比如16秒第一位是6,第二位是1,MUL是放大倍率
setfillcolor()函数即字面意思set(设置)fill(填充)color(颜色)
solidcircle()函数是绘制填充且无边线一个圆形,前两次参数是中心的xy轴坐标,第三个参数是半径。

效果图如上,可以看见中间那两个红点就是函数solidcircle是输出。
那么这个时间该输出在哪里呢,每次点击完刷新对吗。有聪明的读者就会发现,你思考的时候也就是没点下左右键的时候,鼠标动来动去的也在接受信息啊,你说的对,想?想也算时间!
我们对接受鼠标的点击的函数进行以下修改,并且把他加入到主函数当中,就大功告成了
代码如下:
mouseback get_click(int start) {
mouseback mb;
ExMessage msg;
while (1) {
time_out(start);
while (peekmessage(&msg, EX_MOUSE)) {
if (msg.message == WM_LBUTTONDOWN || msg.message == WM_RBUTTONDOWN) {
mb.xx = msg.x;
mb.yy = msg.y;
if (msg.lbutton && !msg.rbutton) {
mb.bb = 1;
return mb;
break;
}
else if (!msg.lbutton && msg.rbutton) {
mb.bb = 0;
return mb;
break;
}
}
}
Sleep(40);
}
}
int main() {
int x, y, start;
mouseback mb;
graph();//创造画布
refresh();//创造炸弹
while (1) {
start = time(0);
while (allspace < 71) {
time_out(start);
outgraph();
while (1) {
mb = get_click(start);
if (mb.xx >= 8 * MUL && mb.xx <= 152 * MUL && mb.yy >= 48 * MUL && mb.yy <= 192 * MUL) {
break;
}
else if (mb.xx >= 68 * MUL && mb.xx <= 92 * MUL && mb.yy >= 8 * MUL && mb.yy <= 32 * MUL) {
refresh();
start = time(0);
continue;
}
}
//以上是接受一个点击,以下是根据点击进行操作
x = ((mb.xx - 8 * MUL) / 16 / MUL) + 1;
y = ((mb.yy - 48 * MUL) / 16 / MUL) + 1;
if (mb.bb) {
if (!el[x][y].flag)
{
if (el[x][y].bomb)break;
else open(x, y);
}
}
else {
if (el[x][y].flag)el[x][y].flag = 0;
else if (!el[x][y].know)el[x][y].flag = 1;
else {
mouseback abc;
abc = open2(x, y);
if (abc.bb) {
x = abc.xx;
y = abc.yy;
break;
}
}
}
}
answer(x, y);//输出答案与结果
while (1) {//重新开始
mb = get_click();
if (mb.xx >= 68 * MUL && mb.xx <= 92 * MUL && mb.yy >= 8 * MUL && mb.yy <= 32 * MUL) {
refresh();
clearrectangle(0, 0, 68 * MUL, 40 * MUL);
fc();
str_out(0, 0, 68, 24, L"滑稽出品", 10);
break;
}
}
}
}
看到这里恭喜你已经学完了全部内容,你真是太厉害了。
代码
shead.h
#ifndef SHEADH
#define SHEADH
#include<graphics.h>
#include<iostream>
using namespace std;
#define MUL 1.5 //调整绘图大小
struct mouseback {
int xx, yy;
bool bb;
};
struct ele {
int data;
bool know, bomb, flag;
};
extern int allspace;
extern ele el[12][12];
//初始化制图
void graph();
//更新当前游戏模块
void outgraph();
//更新当前时间模块
void time_out(int start);
//游戏结束,输出游戏结果和答案
void answer(int dx, int dy);
//作者大帅脸
void fc();
//输出一段文字str,居中于角坐标分别为x,y,dx,dy的矩形,字体大小为b
void str_out(int x, int y, int dx, int dy, LPCTSTR str, int b);
//接受一个点击,有start的会刷新时间,没有的不会;
mouseback get_click(int start);
mouseback get_click();
//刷新数据
void refresh();
//左键打开格子
void open(int x, int y);
//右键打开格子
mouseback open2(int x, int y);
#endif
game.cpp
#include"shead.h"
IMAGE k[9], uk, face, f[2], b[2], t[10];
ele el[12][12];
int allspace = 0;
void loading(){
loadimage(k, L"./pic/k10.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 1, L"./pic/k11.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 2, L"./pic/k12.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 3, L"./pic/k13.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 4, L"./pic/k14.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 5, L"./pic/k15.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 6, L"./pic/k16.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 7, L"./pic/k17.jpg", 16 * MUL, 16 * MUL);
loadimage(k + 8, L"./pic/k18.jpg", 16 * MUL, 16 * MUL);
loadimage(&uk, L"./pic/k0.jpg", 16 * MUL, 16 * MUL);
loadimage(&face, L"./pic/face.jpg", 24 * MUL, 24 * MUL);
loadimage(f, L"./pic/f1.jpg", 16 * MUL, 16 * MUL);
loadimage(f + 1, L"./pic/f2.jpg", 16 * MUL, 16 * MUL);
loadimage(b, L"./pic/b0.jpg", 16 * MUL, 16 * MUL);
loadimage(b + 1, L"./pic/b1.jpg", 16 * MUL, 16 * MUL);
loadimage(t, L"./pic/m0.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 1, L"./pic/m1.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 2, L"./pic/m2.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 3, L"./pic/m3.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 4, L"./pic/m4.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 5, L"./pic/m5.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 6, L"./pic/m6.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 7, L"./pic/m7.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 8, L"./pic/m8.jpg", 12 * MUL, 22 * MUL);
loadimage(t + 9, L"./pic/m9.jpg", 12 * MUL, 22 * MUL);
}
void time_out(int start){
clearrectangle(92 * MUL, 0, 160 * MUL, 40 * MUL);
int x1, x2, x3, x4, x = time(0) - start;
x1 = x % 10;
x2 = (x % 60) / 10;
x3 = (x / 60) % 10;
x4 = (x / 60) / 10;
putimage(99 * MUL, 9 * MUL, t + x4);
putimage(111 * MUL, 9 * MUL, t + x3);
putimage(129 * MUL, 9 * MUL, t + x2);
putimage(141 * MUL, 9 * MUL, t + x1);
setfillcolor(RGB(255, 0, 0));
solidcircle(126 * MUL, 15 * MUL, 2 * MUL);
solidcircle(126 * MUL, 25 * MUL, 2 * MUL);
}
//由于数组[x]中的内容要画在x-1格内,所以以下均有x=x-1.y=y-1
//画一个旗子,其中n==0时为普通旗子,==1时为插做了的旗子
void flag(int x, int y, int n) {
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, f + n);
}
//画一个未被开过的格子
void rec(int x, int y){
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, &uk);
}
//画一个被开过的格子
void num(int x, int y, int num) {
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, k + num);
}
//画一个炸弹,其中n==0时为普通炸弹,==1时为踩到的炸弹
void bomb(int x, int y, int n) {
x -= 1, y -= 1;
putimage((x * 16 + 8) * MUL, (y * 16 + 48) * MUL, b + n);
}
//作者专属大帅脸
void fc() {
putimage(68 * MUL, 8 * MUL, &face);
}
void str_out(int x, int y, int dx, int dy, LPCTSTR str, int b) {
settextcolor(RGB(0, 0, 0));
settextstyle(b * MUL, 0, L"宋体");
outtextxy(((dx - x) * MUL - textwidth(str)) / 2, ((dy - y) * MUL - textheight(str)) / 2, str);
}
mouseback get_click(int start) {
mouseback mb;
ExMessage msg;
while (1) {
time_out(start);
while (peekmessage(&msg, EX_MOUSE)) {
if (msg.message == WM_LBUTTONDOWN || msg.message == WM_RBUTTONDOWN) {
mb.xx = msg.x;
mb.yy = msg.y;
if (msg.lbutton && !msg.rbutton) {
mb.bb = 1;
return mb;
break;
}
else if (!msg.lbutton && msg.rbutton) {
mb.bb = 0;
return mb;
break;
}
}
}
Sleep(40);
}
}
mouseback get_click() {
mouseback mb;
ExMessage msg;
while (1) {
while (peekmessage(&msg, EX_MOUSE)) {
if (msg.message == WM_LBUTTONDOWN || msg.message == WM_RBUTTONDOWN) {
mb.xx = msg.x;
mb.yy = msg.y;
if (msg.lbutton && !msg.rbutton) {
mb.bb = 1;
return mb;
break;
}
else if (!msg.lbutton && msg.rbutton) {
mb.bb = 0;
return mb;
break;
}
}
}
Sleep(40);
}
}
void graph() {
initgraph(160 * MUL, 200 * MUL);
setbkcolor(RGB(181, 181, 181));//设定颜色
cleardevice();
loading();
fc();
str_out(0, 0, 68, 24, L"滑稽出品", 10);
}
void refresh() {
//刷新数值
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 12; j++) {
el[i][j].data = 0;
el[i][j].bomb = 0;
el[i][j].flag = 0;
el[i][j].know = 0;
}
}
allspace = 0;
//创造炸弹
srand(time(0));
int x, y;
for (int i = 0; i < 10; i++) {
x = 1 + rand() % 9;
y = 1 + rand() % 9;
if (el[x][y].bomb) {
i--;
continue;
}
int j, k;
for (j = -1; j <= 1; j++) {
for (k = -1; k <= 1; k++) {
if (j == 0 && k == 0)el[x + j][y + k].bomb = 1;
else el[x + j][y + k].data++;
}
}
}
}
void outgraph() {
clearrectangle(0, 40 * MUL, 160 * MUL, 200 * MUL);
int x, y;
for (x = 1; x < 10; x++) {
for (y = 1; y < 10; y++) {
if (el[x][y].flag)flag(x, y, 0);
else if (!el[x][y].know)rec(x, y);
else num(x, y, el[x][y].data);
}
}
}
void answer(int dx, int dy) {
clearrectangle(0, 40 * MUL, 160 * MUL, 200 * MUL);
int x, y;
for (x = 1; x < 10; x++) {
for (y = 1; y < 10; y++) {
if (el[x][y].flag) {
if (el[x][y].bomb)flag(x, y, 0);
else flag(x, y, 1);
}
else if (el[x][y].bomb)bomb(x, y, 0);
else if (!el[x][y].know)rec(x, y);
else num(x, y, el[x][y].data);
}
}
if (el[dx][dy].bomb)bomb(dx, dy, 1);
clearrectangle(0, 0, 68 * MUL, 40 * MUL);
if (allspace >= 71)str_out(0, 0, 68, 40, L"你赢了", 20);
else str_out(0, 0, 68, 40, L"你输了", 20);
}
void open(int x, int y) {//连锁挖矿
if (x < 1 || x > 9 || y < 1 || y > 9) return;//检查边界
if (el[x][y].bomb || el[x][y].know || el[x][y].flag)return;
else if (el[x][y].data) {
el[x][y].know = 1;
allspace++;
}
else {
el[x][y].know = 1;
allspace++;
int i, j;
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i != 0 || j != 0)open(x + i, y + j);
}
}
}
}
mouseback open2(int x, int y) {
int fg = 0, i, j;
mouseback abc = { 0,0,0 };
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (el[x + i][y + j].flag)fg++;
}
}
if (fg == el[x][y].data) {
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i != 0 || j != 0) {
if (!el[x + i][y + j].flag) {
if (!el[x + i][y + j].bomb) open(x + i, y + j);
else {
abc.xx = x + i;
abc.yy = y + j;
abc.bb = 1;
}
}
}
}
}
}
return abc;
}
main.cpp
#include"shead.h"
using namespace std;
int main() {
int x, y, start;
mouseback mb;
graph();//创造画布
refresh();//创造炸弹
while (1) {
start = time(0);
while (allspace < 71) {
time_out(start);
outgraph();
while (1) {
mb = get_click(start);
if (mb.xx >= 8 * MUL && mb.xx <= 152 * MUL && mb.yy >= 48 * MUL && mb.yy <= 192 * MUL) {
break;
}
else if (mb.xx >= 68 * MUL && mb.xx <= 92 * MUL && mb.yy >= 8 * MUL && mb.yy <= 32 * MUL) {
refresh();
start = time(0);
continue;
}
}
//以上是接受一个点击,以下是根据点击进行操作
x = ((mb.xx - 8 * MUL) / 16 / MUL) + 1;
y = ((mb.yy - 48 * MUL) / 16 / MUL) + 1;
if (mb.bb) {
if (!el[x][y].flag)
{
if (el[x][y].bomb)break;
else open(x, y);
}
}
else {
if (el[x][y].flag)el[x][y].flag = 0;
else if (!el[x][y].know)el[x][y].flag = 1;
else {
mouseback abc;
abc = open2(x, y);
if (abc.bb) {
x = abc.xx;
y = abc.yy;
break;
}
}
}
}
answer(x, y);//输出答案与结果
while (1) {//重新开始
mb = get_click();
if (mb.xx >= 68 * MUL && mb.xx <= 92 * MUL && mb.yy >= 8 * MUL && mb.yy <= 32 * MUL) {
refresh();
clearrectangle(0, 0, 68 * MUL, 40 * MUL);
fc();
str_out(0, 0, 68, 24, L"滑稽出品", 10);
break;
}
}
}
}
其他内容
浅谈文件分配
作者大部分的函数打包写在了game.cpp中,主要的骨干函数写在别main.cpp中,诸多声明声明声明在了shead.h中。
这保证了main的易读性,从游戏开始,然后每轮做什么,再到游戏结束,还是比较清晰易读的。
至于头文件h中,不应该有函数的定义,也不应该有变量的定义,不然很容易造成重定义问题, 头文件主要用来做一些声明比如函数的声明与变量的声明,其中变量声明前面要加多一个extern,意为这是一个声明而不是一个定义。
那头文件有什么用呢?如果在game里面写函数,定义变量,想在main里面用到,最好的方法就是借助头文件了。
至于为什么不把代码全部写到一个源文件里,就设计cpp编译的工作原理了,如果有时间作者和会写一篇博客详谈一下。
打包
其实把图片文件和exe生成文件放在一个文件夹里,压缩发给别人是一个很不错的做法。
顺带一提生成的exe文件在以下位置,即项目



如果要打包的话根据以下步骤就好了
1、打开拓展搜索set找到这个拓展并下载


2、添加打包项目,创建



3、右键Application Folder添加项目输出


4、右键Application Folder添加文件夹,命名为pic,然后把你要用到的图片塞进去(不要把整个文件夹塞进去)

搞定后大概这样


5、可选项,你可以右键那个主输出,然后点第一个,那是创建快捷方式,然后把快捷方式放到User‘s desktop那是创建一个快捷方式。
6、右键扫雷,点击生成,再右键一次,点击打开文件夹,打开release,msi结尾就是安装包了,单发给别人是可以用的。



结尾
作者真的花了很多心思在这篇博客上,可怜可怜作者点点赞点点收藏点点关注转发评论吧,有任何问题欢迎评论私信,也欢迎来找作者讨论,加Q:2908692481。
基于EasyX库的扫雷项目详细分享

801

被折叠的 条评论
为什么被折叠?



