C#编写制作《扫雷》游戏
先看看《扫雷》 的思维导图,再看动画演示。
部分功能的动画演示,下面有更全的。
本文为《C#扫雷游戏开发实战》系列文章第三
1、C#,扫雷游戏(Minesweeper)之壹——作弊手段大曝光
2、C#,扫雷游戏(Minesweeper)之贰——世界排名的规则知识
3、C#,扫雷游戏(Minesweeper)之叁——扫雷软件的需求分析与软件设计
4、C#,扫雷游戏(Minesweeper)之肆——扫雷软件的核心代码
5、C#,扫雷游戏(Minesweeper)之伍——扫雷软件的改进与实践
这是一篇关于《扫雷》游戏的近技术文章。
因为家人年纪大、视力差、保守与恋旧、不喜欢(通过微软商店才能下载安装的)新版本的微软《扫雷》,也无法适应竞技版本的《扫雷》。牢子 平时狂傲惯了,便被下了战书,要求编一个特制的《扫雷》游戏,只好硬着头皮边学边干。
期间遇到了一些小小的问题,发布出来与大家分享与交流。
吾乃一介超级懒虫,接到任务后,想来这么简单的事,一定有人干得不错了。先bing再bai后goo,看看哪位大咖的代码能用,直接改改就得以交差了。
当然,如果这么简单,就无需写本文了。搜索后,惊奇而失望地发现:
很久很久以前,曾经少量玩过《扫雷》,规则稍微看了一下,瞎按一通,好像从来未过关。反正不提神,不如啃数学书有意思,就再没碰过。今天才发现《扫雷》游戏居然已经火了30年了,依然经久不衰,真是一个奇迹!
特意写了个爬虫,满宇宙地爬。爬到的《扫雷》代码,java/C/C++/HTML5+js/php/python/C#/...,很多很多,粗粗地都看了一遍,可惜都太烂了,完全不叫编程。
竞技型《扫雷》软件不好写;微软的《MineSweeper》就因为“被”作弊手段太多而被踢出了竞技赛场;
任何功能要做到极致,均非易事。记得,曾经为了写两个数的加法,累计起来达到5000多行代码,想起来那是癫而不是狂。何苦来斋?
《扫雷》软件具体来看都有什么需求呢?与家人、邻居与热衷于游戏的邻居孩子聊聊之后,稀稀拉拉记录下来,五花八门。包括但不限于下面这些:
(01)严格遵守微软制定的《扫雷》规则;
(02)设计风格要严格遵守经典版本(指的是 Windows 3.1);
(03)第一挖,不要触雷哈;这个新版本才有哈。
(04)能随意放大、缩小窗口;满足视力差的需求;
(05)时间精确到毫秒;邻居孩子的竞技练习需求;
(06)记录所有玩的过程并用图表显示,查看进步?退步?
(07)纯绿色软件,一个exe文件解决所有问题;发给好朋友省事;
(08)有点音效,比如爆炸,但不要太多,别太吵;
(09)字体好看一点;这个可以有!
(10)如果干得凑合,以后还要六边形的、异型的挖雷游戏;去去去!
(11)邻居是挖雷的好手,提出来说不能“被”作弊!天哪,这个难哪。
(13)不吉利,跳过;
(14)。。。
1、《扫雷》的规则
网上爬了大量的《扫雷》的规则,想抄一份。可惜没有一个说透的,只好自己写:
游戏主旨:游戏区分布着大量的格子,其中的一些格子随机埋设了地雷,玩家(使用鼠标左键)点击格子扫描(挖),查看是否有地雷,成功扫(挖)开“除了有地雷之外的所有格子”即为胜利。
扫描地雷:玩家(使用鼠标左键)点击格子挖开:a如果没有地雷,则格子变成平地。b如果遇到地雷,则直接爆炸牺牲(GAME OVER)。c如果不是地雷,而周边有地雷,显示数字。d挖开格子的同时,系统可以自动挖开周边连续的、可能很多的空格子(很爽!)。
地雷信息:挖开的格子,如果有数字显示,则该数字即是周边3x3区域8个(边上的少)格子内的地雷数。其他异型《扫雷》,原理一样,统计与显示的数字不同。
地雷标记:依据格子及已知地雷的分布,认定有地雷的格子,可以使用鼠标右键单击,a做个标志(插上小旗子)。b再次点击转为怀疑。c再次点击取消。
表示怀疑:如果某个未挖开的格子,怀疑其可能是地雷,a可以鼠标右键双击(或两次鼠标右键单击)做出问号(?)标记。b再次点击可以取消。
自动挖开:如果“某个格子显示的数字 = 已经标记的周边地雷数”,鼠标左右键双击(或左键双击)可以自动打开周边相关的格子。自动挖开是算法问题。
游戏主界面分为三个区域。
2.1 菜单区
本文的《扫雷》有三列菜单。第一列包括:开始新游戏;初级;中级;高级;记录与退出等等;第二列是趣味扫雷,有各种地图为底图的异型《扫雷》;第三列是帮助与关于。
Visual Studio + C# 搭界面的效率、开发的效率都无与伦比,省下很多时间可专注于算法与数据,并用于其他部分的编程。真佩服30年前的程序员,调细节是多么辛苦!
2.2 信息区
信息区左侧为显示未挖开的地雷数(不一定是真的哈!玩家认为的);中间是表情显示;右侧是时间显示。表情显示区可以点击,等于重开游戏。
2.3 游戏区
游戏区一般都是矩形的。
如果格子(地砖,地板,地块,以下称格子Grid)是异型的,比如六边形的,那么主界面可以是其他形状;不过,即使是方形(或长方形)格子,游戏区也可以是任意形状的哈。比如,东东市地图、米国地图、袋鼠国地图等等。开始用湾湾岛地图的,后来想都是一家人呢,再不听话也是兄弟姐妹。
3、需求总结
需求不少,用编程语言总结一下,主要是:
游戏区的设计:满足矩形与非矩形的部署;
布雷:满足各难度系数(按密度系数换算)的地雷布设;
规则:鼠标事件的处理,满足规则要求;
显示:格子各种状态;周边地雷的统计;地雷数、时间LED显示;表情;
自动挖雷:无向图的深度优先遍历算法(DFS for Graph);
防作弊手段:含扫雷过程细节的记录与加密存储;伪视频;
游戏历史记录、统计与图示。
1、菜单区
菜单区没什么可说的。
2、信息区
需要但没用的资源。
2.1字体问题
2.1.1 字体显示问题
信息区的字体,比较特殊,属于LED字体;而且有背景字? RGB(128,0,0)。
(1)简单方法:用 Label 控件,设置? LED字体,显示地雷 与 时间。
(2)原始方法:使用事先保存的 字图片(0--9),显示于 PictureBox 控件;需要分别计算每个数字的位置等等,大小不可(不方便)调整;
(3)较好办法:使用 LED 字体;并绘制 DrawString于 PictureBox 控件;支持暗红色背景字及大小调整(照样清晰);
2.1.2 字体文件问题
LED字体文件一般都随着软件发布,这不符合 纯绿色软件 的要求。
前面说了,要求一个 exe 文件解决所有问题,不要另外的文件。当然也不能用远程下载的方式。
一般可以用资源的方式。本文的做法更极端,甚至不用资源,避免被再次利用!
实现的方法:将二进制的 TTF 文件,转为base64字符串,再转为 .cs 代码文件,嵌入工程。运行的时候,再反转为 byte 数组,通过? AddMemoryFont 方法直接加载内存字体,极快又省事。
坑(Mud puddle):最早想省事,直接加载内存字体 AddMemoryFont,用于 Label 显示。但在用于 Label 控件的时候就不稳定了,按要求设置 Label 的 UseCompatibleTextRendering 为 true,也经常报内存修改的错误!后来改为 PictureBox 显示,就没有问题了。
2.2时间显示
2.2.1 时间格式问题
参与竞技的孩子们要求显示到毫秒。因而时间显示是秒+毫秒的格式。其中秒不能超过1000,需要取余数,以免显示超过界限。
软件允许玩家选择时间显示格式。
2.2.2 时钟周期问题
这是微软 Minesweeper 早期的一个 bug。MInesweeper是按计时器 Timer ,而 Timer 都是在一个周期(Interval)后在进入程序节点。正确的话,需要扣除 Timer Interval 的第一个时钟周期。
本文不用这个办法,直接记录第一次点击的时间。
2.3地雷显示
左端的地雷显示区,显然不能显示真实的剩余地雷数。有一些代码直接显示真实的剩余地雷数,这不是作弊吗?应该显示“玩家认为”的剩余的地雷数,也就是实际地雷总数,扣除已经标记的地雷数(可能标错哈)。
2.4表情显示
表情显示区有两个功能:
(1)反馈玩家的操作;
(2)点击重新开始新游戏;
表情有四个:
笑脸;一般场合;
哭脸;挖错了;失败;
惊讶,张嘴;左右双击自动挖雷;
得意,带墨镜,成功!
表情显示可以用两种方式。一是用预先存好的图片;二是直接画。
因为要适应随意放大缩小的需求,本文选择第二种。
画法:
笑脸:两个小圆+一段圆弧;
哭脸:两个交叉线+一段圆弧;
惊讶:两个稍微大一点的圆+一个椭圆;
得意:两个稍微大的椭圆+一段圆弧+三条直线(眼睛架);
3、格子
格子,也称为地砖、地块、地,组成了游戏界面。
3.1 格子的形状
格子的形状一般都是正方形的。
异型的《扫雷》支持六边形、三角形及其他可无限重复拼接的结合形状。
玩过之后发现,正方形 与 六边形 比较有意思一些。
3.2 格子的状态
格子的状态与显示效果有:
一般的扫雷代码都使用事先保存的图片,这不适合可以任意放大缩小的需求。
本文的做法是按不同的状态,设计不同的绘制方法。包括格子区绘制、台阶绘制、旗帜绘制、地雷绘制、错误(叉)绘制及爆炸(流血)格子的绘制等等。
3.3 格子的数字
3.3.1 字体问题
格子的数字可以用两种方式显示。
一是用实现保存的图片。二是用TrueTypeFont(TTF)字体。
因为要满足可以随意放大缩小的需求,用图片显然不行了。
尝试了很多种字体,请家人一一过目,最终发现选择 Arial Black 比较好。
字体的大小 fontHeight = GridHeight * 0.5 看起来比较舒服。经典版本的字体稍微有些大了。
3.3.2 数据问题
格子里面的数字是统计周围格子地雷数得到。
一般的代码通过统计二维数字中当前格子周边 3x3区块的地雷数得到。这个只能用于矩形的游戏区与正方形的格子。
本文的代码中,游戏区保存的是格子之间的,因而可以支持任意形状的格子与任意分布的游戏区。
计算地雷的高级算法:
(One-step DFS Algorithm for un-direction graph)。
单步深度优先遍历?没听说过?
当然。别当真啊,逗着玩呢,只需要循环即可得到,不需要什么算法!
4、鼠标事件
4.1 鼠标事件分析
如果用键盘《扫雷》,一定会失去兴趣与乐趣。
搜索历史文件,发现《扫雷》可能大大促进了小老鼠的改良。
鼠标事件及其逻辑看下图比较清楚,不再赘述。
4.2 鼠标事件的问题
鼠标事件的处理有一些细节,也是一些坑。
4.2.1 双击的时间问题
先问一个问题:鼠标键两次点击之间“间隔多少时间”算“双击”?
解决之道:通过 Windows API 获取系统设置的鼠标双击时间间隔。
仅仅用这个时间是不够的,需要按玩家的操作过程之统计数据进行适当的智能修正。
高龄玩家点击速度慢,间隔时间要长一点。
竞技选手手速快,间隔时间要短很多。
4.2.2 左右键双击的问题
Visual Studio C# 开发库只提供了 MouseDown 与 MouseUp 的事件,怎么判别“左右键双击”?
这里要用到 堆栈Stack(列表List也可以),因为还有更全面的考虑,使用堆栈是比较理想的。实现方法:MouseDown时,Push压栈;MouseUp 时,Pop出栈,因而只有在 LeftStack 与 RightStack 之 Count 均 >0 的情况,才是左右键双击。
总体上需要考虑与跟踪 MouseDown,MouseUp,MouseLeave,MouseMove 的所有情况。
5、游戏区的要点
5.1 级别设置
扫雷的级别设置,有两个关键数字:网格大小 和 密度系数。
5.1.1 网格大小
网格的大小是指格子的数量。
竞技扫雷的网格是8x8,9x9,16x16,16x30,24x30。
一般的《Minesweeper》软件也支持用户自定义。个人觉得用户自定义没有什么意义。
异型《扫雷》的网格大小评价方式不同。
5.1.2 密度系数
扫雷的级别设置关键参数是密度系数 Density。
密度系数 = 地雷数 / 格子数 %
一般的设置是:
8x8,8个雷,密度系数:12.50%;
9x9,10个雷,密度系数:12.35%;
16x16,40个雷,密度系数:15.63%;
16x30,99个雷,密度系数:20.62%;
24x30,164个雷,密度系数:22.78%。
密度系数越高,难度越高。
密度系数高,意味着地雷多,空隙少!
空隙少,是最致命的难度。
5.2 绘制方法
游戏区的格子,可以设计为独立的Text, Button, Panel, PictureBox 等等。
但测试后,发现效率最高、效果最好的应该是整体的 PictureBox。
5.3 闪烁问题
设置
this.DoubleBuffered = true;
即可,省事。
6 音效问题
音效的要求是:
效果要好,不能断断续续的;
纯绿色软件,不能从外部加载音效文件;
本文代码只保留了爆炸的音效。
一开始试了 MP3 的文件及播放器,发现有时滞。
遂将 MP3 转为 WAV 。
为了符合 纯绿色软件 的要求,不从文件加载。先将 WAV 文件,压缩,byte[] 转为 base64 字符串,拆分到 cs 文件。并入 project,后续再解压,base64,并反转 byte 成为 stream ,进入时被一次加载。后面播放的时候,只需要 play 即可。
7 数据统计
数据统计及图表是低档的东西,上不了台面,不提了。
写这部分代码最鼓噪乏味。
数据结构的设计最能体现程序员的层次,网上所有的《扫雷》游戏代码之数据设计都一塌糊涂,完全不是编程的思路与感觉。
1、格子数据
格子需要保存的数据有:
2、游戏区数据
游戏区存储的核心数据是格子数据。
网络代码几乎无一例外都是按 二维数组 Bricks[,] 设计的。
而,划重点了!格子之间的关系是“图Graph”,因而游戏区的数据必须是按图数据设计的。
3、游戏区数据
(1)游戏区Id;
(2)形状编码;默认为矩形,可以支持六边形、三角形等等;
(3)行数Row,列数Column编:适用于矩形游戏区的数据;
(4)形状参数Size;
(5)格子之间的关系数据:邻接矩阵 Adjacency;
(6)地雷总数;
评判标准:未用邻接矩阵的代码基本上是30年前的水平。
自动挖开算法的深度优先遍历(DFS)算法需要基于邻接矩阵的数据。
4、游戏数据
4.1 普通数据(历史数据)
需要保存玩家游戏的各种数据。
记录成绩;鼓励进步;参与分享;不亦乐乎?
数据项,包括但不限于:
(1)鼠标点击X坐标
(2)鼠标点击Y坐标
(3)操作的格子Id
(4)操作的格子(行)位置
(5)操作的格子(列)位置
(6)时间戳Ticks
(7)操作编码
4.2 细节数据(防作弊数据)
保存玩家扫雷过程的细节数据,主要用于区分是“人”或是机器人(程序)扫雷。
数据项,包括但不限于:
(1)行数
(2)列数
(3)地雷总数
(4)密度系数
(5)总耗时(毫秒)
(6)左键单击数
(7)全部点击数
(8)本地开始时间戳(Ticks)
(9)结束时间戳(Ticks)
(10)布雷矩阵信息(01)
(11)挖出地雷数
(12)步数(移动次数)
后续的处理包括(但不限于):
玩家鼠标轨迹数据分析;
玩家操作记录分析;
玩家行为分析;
目的就是一个:看看到底是“人”在玩,还是“机”在玩!
花了几天时间,家人已经玩的不亦乐乎了。
邻居孩子正在与他认为非常简单的六边形斗争呢,似乎不太顺利。
本来想这么简单的事,几句话就可以说清楚了,稀稀拉拉居然写了这么多文字,关键是榨干了也没几句话是有用的。既然写了,就发出来吧。请大家不要嫌弃。
谢谢您耐心的阅读!