C++小白的第一个程序:基于图形界面的扫雷

一、前言

作为一个从未接触过C++的大一编程小白,自己写一个能够运行的游戏程序是一个看起来非常有难度的事情,事实也的确如此。从学习C++开始到写出基本上能够运行的程序,我只有四周的时间,因此做出来的实际效果与我心中所期待的还是有比较大的差距,整个游戏的可玩性也并不算高,只是粗略地展现了自己对于c++和编程一点粗浅的理解罢了。接下来我将展示一部分游戏中实现的功能和一些我在编写过程中遇到的问题,希望各位大佬不吝赐教。

二、 功能实现基本介绍

  1. 探雷:地图实现逻辑:生成初始值为0的二维数组,通过系统时间生成随机数,将随机数通过取模转化为的数组的具体位置,将这些位置的数据重新赋值为-1,再通过嵌套循环遍历数组,将雷周围的数字加一,最后将所有位置的数字统一增加20形成最终的地图数组。再定义一个图片数组,将所有图片格式化载入,同时建立图片与数组对应数字之间的关系,在图窗的对应位置加载对应的图片。使用了easyx中的鼠标信息结构体,通过鼠标所在的坐标判定鼠标所在的格子。当侦测到左键按下时,将对应数组对应位置的值减20,再通过对应数字加载对应图片的方式实现图形的变化。当点击到雷时,遍历地图数组,找出赋值为雷的位置并改变其数据,用同样的去遮罩方式使雷显现出来。设置了一个检测已打开格子的变量,遍历数组后判断已打开格子的数量并赋值,当已打开格子的数量与雷数之和为总格数时游戏结束。

     //数据变化
     void initmap(int map[R][C])
     {
     //把地图清零
     memset(map, 0, R * C * sizeof(int));
     for (int i = 0; i < 10;)//买下一定数量的雷
     {
         int r = rand() % R;
         int c = rand() % C;
         if (map[r][c] == 0)//避免重复埋雷
         {
             map[r][c] = -1;
             i++; //在成功埋雷后再计数
         }
     ​
     };
     ​
     for (int i = 0; i < 10; i++)//遍历数组,对雷周围实行数字增加,并使其与对应数字的图片对应
     {
         for (int j = 0; j < 10; j++)
         {
             if (map[i][j] == -1)
             {
                 for (int h = i - 1; h <= i + 1; h++)
                 {
                     for (int l = j - 1; l <= j + 1; l++)
                     {
                         if ((h >= 0 && h < R && l >= 0 && l < C) && map[h][l] != -1)
                         {
                             map[h][l]++;
                         }
     ​
                     }
                 }
             }
         }
     }
     ​
     //把所有格子盖起来,给每个格子都加密
     for (int i = 0; i < R; i++)
     {
         for (int j = 0; j < C; j++)
         {
             map[i][j] += 20;
         }
     }
    
  2. 右键插旗子和取消,插有旗子的位置无法探雷:当数组中对应数据对应的图片为遮罩图时,鼠标右键点击,对应位置的数据继续加20,加载出对应的旗子图,此时数组对应的数据不在鼠标左键的判定范围内,因此无法继续探测。当再次使用右键时,数据减20,重新显示遮罩图,达到取消旗子插入的效果。

  3. 代码结构化:程序中所使用的函数大部分在主函数外书写,在主函数中主要采用调用函数的形式。

  4. 游戏开始、结束界面:通过easyx的图形绘制功能绘制开始界面,通过MESSAGEBOX模态对话框来实现结束界面。可通过结束界面来退出程序

  5. 点击空白格时触发连击的功能:当点击的格子为空白格(周围无雷)时,继续遍历周围格子周围的格子(将该触发函数再次引用一遍),直至周围出现数字格子时停止遍历,因此可以实现在点击空白格子时对周围的格子实现连点直至出现数字格子

     void opennull(int map[R][C], int row, int col)//判断当前点击是不是空白
     {
     if (map[row][col] == 0);//空白后遍历数组,遇到空白则循环打开,遇到数字则打开数字后停下
     {
         for (int i =row - 1 ; i <= row + 1; i++)
         {
             for (int j = col - 1; j <= col + 1; j++)
             {
                 if ((i >= 0&&i < R&&j >= 0&&j < C) && map[i][j] == 20)
                 {
                     map[i][j] -= 20;
                     opennull(map, i, j);
                 }
                 if ((i >= 0 && i < R && j >= 0 && j < C) && map[i][j] > 19 && map[i][j] <= 28)
                 {
                     map[i][j] -= 20;
     ​
                 }
                 
             }
         }
     }
     }

  6. 实施时间显示:(该代码为引用后改编的代码)该代码的实现依赖于对系统时间的获取,在进入游戏界面时提取系统时间,之后进入游戏循环后,再次提取系统时间,将两次的系统时间相减即为游戏总时间,该值通常为一个浮点数,然后利用取整函数对该值取整数得到以整数形式输出的游戏时间。改编之处:由于原计时器程序不包括其他功能,因此其刷新率远远超出扫雷程序的应用要求(过高的刷新率导致了画面的严重频闪),我通过对该数据以一定的时间长度取模来降低刷新率,避免清屏操作时对游戏界面操作的影响。最终确定了以10ms取模的刷新率,实现了在游戏时间正常显示时不影响屏幕的刷新率。

  7. 游戏过程中的音乐播放:利用easyx的内置头文件\#pragma comment(lib,"winmm.lib")mciSendString函数实现了MP3格式音乐文件的播放。

  8. 实时显示未探雷的个数:取消旗子标记后地图会被重置,因此若先插旗子后再取消插旗,会导致数字超出预设范围,目前暂未有有效的解决思路。

  9. 游戏中退出或重开:游戏失败部分的退回菜单功能书写在主函数中,因此可以用跳转语句跳转至程序最开始的部分,但游戏成功的判定在主函数之外,无法使用跳转语句,目前暂无有效的解决思路。

  10. 第一次探雷不会被炸到:鼠标的操作不区分次数,且地图刷新在游戏的最开始,无法确保第一次点击时不会炸雷。目前考虑用while判断解决,若此时第一次判断为点击到雷,则重新编写地图后再次进入判定,直至该判定不生效为止。(补充:现在已经学习到了解决的方案,假设判定到第一次就踩到雷,则遍历数组,将第一次踩到的雷和某个不是雷的位置互换,再将两个格子周围的数字重置,从而达到第一次“永不踩雷”的效果)

三、编写过程中遇到的问题与解决方案

  1. 计时器带来画面频闪和无限清屏的解决:图形窗口的一切效果的实现本质上是色块在给定区域的叠加,因此每次叠加后如要在相同区域显示另外的内容,就必须用clear系列函数清屏,又由于程序执行的快速,清屏函数如若不给予条件限制,必然会导致在循环体种清屏操作的不断执行,所具有的最终视觉效果即是静止状态的背景颜色。因此,我在清屏时对于“游戏时间”这一变量进行了取模操作,以100毫秒为单位取模,若取模结果为0,则进行局部清屏操作。如此一来,我不仅可以实现时间以秒为单位的刷新,还能保证主游戏画面不遭受频闪的影响。当然,问题也是客观存在的,计算机中的整形储存同样依赖于小数,在具体操作过程中,理论上,取模的时间越短刷新性能越好,但更短的刷新时间会导致频闪,在综合实验后,我确定了以10毫秒为周期的刷新频率,让时间在最终得以顺畅显示。

     void opennull(int map[R][C], int row, int col)//判断当前点击是不是空白
     {
     if (map[row][col] == 0);//空白后遍历数组,遇到空白则循环打开,遇到数字则打开数字后停下
     {
         for (int i =row - 1 ; i <= row + 1; i++)
         {
             for (int j = col - 1; j <= col + 1; j++)
             {
                 if ((i >= 0&&i < R&&j >= 0&&j < C) && map[i][j] == 20)
                 {
                     map[i][j] -= 20;
                     opennull(map, i, j);
                 }
                 if ((i >= 0 && i < R && j >= 0 && j < C) && map[i][j] > 19 && map[i][j] <= 28)
                 {
                     map[i][j] -= 20;
     ​
                 }
                 
             }
         }
     }
     }

  2. 游戏启动界面的频闪问题:最初,我的游戏界面也遭受了频闪问题。通过借鉴计时器中带有的双缓冲绘图技术,我成功解决了画面的闪动问题,获得了较好的输出效果。

  3. Easyx的文本输出问题:在函数中进行数学运算和赋值时,所有数据主要以整型方式介入计算,但在easyx中,图形界面上的所能输出的只有图片和字符串。因此输出类型决定了是否能输出正确的数值。这个问题的解决依赖了sprintf函数来进行转换。

  4. 连续点击过程的实现:点开空白格子之后的连击操作是对我逻辑的一次考验。如果使用一般的循环结构,每一次程序的执行都是独立的,无法完成一生二三这样子的递推关系,因此这里的运用特点是同一个函数的嵌套使用。在一个函数中调用它本身,形成了一个逻辑判断,从而实现了连续点击的操作。

四、收获与思考

自9月24号开题至今已经过去了整整一个月的时间。回想开题之初,这个任务对我来说似乎是一个天方夜谭。对于一个从来没有接触过C++编程的人来说,从0开始的学习必然是充满挑战的。最初的理论学习花费了我两周时间,也让我对编程语言有了一些基本的认知。当我开始编写程序之时,我也发现了基本理论与应用程序之间的差别,编程过程中的重点在于对任务的解构,将具体的任务转化为抽象的逻辑,然后再用基础知识将逻辑完整的展现出来。在以地图为核心的基础逻辑上,我在网络上进行了一些资料的收集,了解了地图绘制的大致逻辑和贴图的基本方法,并以此构建出了基本的游戏框架。其他功能的实现逻辑则大多数需要靠我自己实现。在实现其他任务的过程中,我也遭遇过数不胜数的困难,调试程序的过程对精神的折磨也是巨大的,程序顺利运行的次数远远少于一次次报错、一次次显示出令人费解的画面的次数。每一次对程序的调试其实也是对自我逻辑的检验。在这样的过程中,正确合理的逻辑思维才能被逐渐培养出来。这一个月的时间对于学习C++来说并不算长,我也只觉得自己对C++初通门径,可能练得更好的是打字速度和中英文的切换吧。但我觉得这一个月的学习对我来说提供了一个帮助自己踏入大学自学节奏门槛的可能性,遑论结果几何,我都为这一个月以来的收获感到满足。正是一步步地挑战与攀登,我才能遇见更好的自己。

五、后记

这是在很短时间内初步学习C++后留下的一点感受,我在编程方面的学习也只是走出了万里长征的第一步,希望各位大佬能给予我编程学习方面的建议或指导,希望能与大家多多交流,共同进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值