请选择 进入手机版 | 继续访问电脑版

技术控

    今日:4| 主题:54678
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] 每日一博 | WinformGDI+入门级实例——扫雷游戏

[复制链接]
女神的忄 发表于 2016-10-1 09:15:11
142 0

立即注册CoLaBug.com会员,免费获得投稿人的专业资料,享用更多功能,玩转个人品牌!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

每日一博 | WinformGDI+入门级实例——扫雷游戏

每日一博 | WinformGDI+入门级实例——扫雷游戏-1-技术控-private,public,sealed,游戏,闲置

  整体思路:

  扫雷的游戏界面让我从一开始就想到了二维数组,事实上用二维数组来定义游戏数据确实是最符合人类思维的方式。(Square类会在后面解释)
  1. //游戏数据
  2. private readonly Square[,] _gameData;
复制代码
有了这个开头,接下来就是填充二维数组的数据了,对于数据,我最初的想法是用int或枚举,当然,这是可行的,但涉及一个问题就是高耦合,所有操作将都在高层执行,难以维护。
  于是我们用一个Square类表示一个小方块区。
  1. /// <summary>
  2. /// 表示游戏中一个方块区
  3. /// </summary>
  4. public sealed class Square
  5. ...
复制代码
以枚举表示方块区的状态:
  1. /// <summary>
  2. /// 方块区状态
  3. /// </summary>
  4. public enum SquareStatus
  5. {
  6.     /// <summary>
  7.     /// 闲置
  8.     /// </summary>
  9.     Idle,
  10.     /// <summary>
  11.     /// 已打开
  12.     /// </summary>
  13.     Opened,
  14.     /// <summary>
  15.     /// 已标记
  16.     /// </summary>
  17.     Marked,
  18.     /// <summary>
  19.     /// 已质疑
  20.     /// </summary>
  21.     Queried,
  22.     /// <summary>
  23.     /// 游戏结束
  24.     /// </summary>
  25.     GameOver,
  26.     /// <summary>
  27.     /// 标记失误(仅在游戏结束时用于绘制)
  28.     /// </summary>
  29.     MarkMissed
  30. }
复制代码
用Game类来表示一局游戏,其中包含游戏数据、游戏等级、雷区数、布雷方法等。
  1. /// <summary>
  2. /// 游戏对象
  3. /// </summary>
  4. public sealed class Game
  5. ...
复制代码
难点攻破:

  游戏不大,涉及的难点也就不多,但对于刚接触GDI+的读者,一些地方还是比较麻烦的。
  逻辑难点1:布雷

  扫雷游戏有一个附加规则,就是第一次单击不论如何都不会踩到雷区,由于这个规则的存在,我们不能将布雷操作做在第一次单击之前。所以我们在游戏开局时假设所有方块区都没有雷。
  1. /// <summary>
  2. /// 开始游戏
  3. /// </summary>
  4. public void Start()
  5. {
  6.     //假设所有方块区均非雷区
  7.     for (int i = 0; i < _gameData.GetLength(0); i++)
  8.         for (int j = 0; j < _gameData.GetLength(1); j++)
  9.             _gameData[i, j] = new Square(new Point(i, j), false, 0);
  10. }
复制代码
随后,在开局后第一次单击时布雷。
  1. /// <summary>
  2. /// 布雷
  3. /// </summary>
  4. /// <param name="startPt">首次单击点</param>
  5. private void Mine(Point startPt)
  6. {
  7.     Size area = new Size(_gameData.GetLength(0), _gameData.GetLength(1));
  8.     List<Point> excluded = new List<Point> { startPt };
  9.     //随机创建雷区
  10.     for (int i = 0; i < _minesCount; i++)
  11.     {
  12.         Point pt = GetRandomPoint(area, excluded);
  13.         _gameData[pt.X, pt.Y] = new Square(pt, true, 0);
  14.         excluded.Add(pt);
  15.     }
  16.     //创建非雷区
  17.     for (int i = 0; i < _gameData.GetLength(0); i++)
  18.         for (int j = 0; j < _gameData.GetLength(1); j++)
  19.             if (!_gameData[i, j].Mined)//非雷区
  20.             {
  21.                 int minesAround = EnumSquaresAround(new Point(i, j)).Cast<Square>().Count(square => square.Mined);//周围雷数
  22.                 _gameData[i, j] = new Square(new Point(i, j), false, minesAround);
  23.             }
  24.     _gameStarted = true;
  25. }
复制代码
先创建雷区,再创建非雷区,以便我们在创建非雷区时可以计算出非雷区周围的雷数,枚举周围方块的方法我们用yield创建一个枚举器。
  1. /// <summary>
  2. /// 枚举周围所有方块区
  3. /// </summary>
  4. /// <param name="squarePt">原方块区</param>
  5. /// <returns>枚举数</returns>
  6. private IEnumerable EnumSquaresAround(Point squarePt)
  7. {
  8.     int i = squarePt.X, j = squarePt.Y;
  9.     //周围所有方块区
  10.     for (int x = i - 1; x <= i + 1; ++x)//横向
  11.     {
  12.         if (x < 0 || x >= _gameData.GetLength(0))//越界
  13.             continue;
  14.         for (int y = j - 1; y <= j + 1; ++y)//纵向
  15.         {
  16.             if (y < 0 || y >= _gameData.GetLength(1))//越界
  17.                 continue;
  18.             if (x == squarePt.X && y == squarePt.Y)//排除自身
  19.                 continue;
  20.             yield return _gameData[x, y];
  21.         }
  22.     }
  23. }
复制代码
逻辑难点2:当单击区周围无雷区(空白)时,自动批量打开周围所有非雷区

  1. //如果是空白区,则递归相邻的所有空白区
  2. if (_gameData[logicalPt.X, logicalPt.Y].MinesAround == 0)
  3.     AutoOpenAround(logicalPt);
复制代码
  1. /// <summary>
  2. /// 自动打开周围非雷区方块(递归)
  3. /// </summary>
  4. /// <param name="squarePt">原方块逻辑坐标</param>
  5. private void AutoOpenAround(Point squarePt)
  6. {
  7.     //遍历周围方块
  8.     foreach (Square square in EnumSquaresAround(squarePt))
  9.     {
  10.         if (square.Mined || square.Status == Square.SquareStatus.Marked || square.Status == Square.SquareStatus.Opened)
  11.             continue;
  12.         square.LeftClick();//打开
  13.         //周围无雷区
  14.         if (square.MinesAround == 0)
  15.             AutoOpenAround(square.Location);//递归打开
  16.     }
  17. }
复制代码
绘图难点1:双缓冲以克服闪烁

  从二维数组的结构来看,我们需要遍历整个二维数组,然后把每个Square绘制到winform上,但这会造成强烈的闪烁效果。因为是实时绘图,绘制的每一步都会实时显示在窗口上,所以我们看到的效果就是一个方块区一个方块区的出现在窗口上。
  为了克服这种不友好的闪烁,双缓冲出现了,思路就是创建一个缓冲区(通常是一个内存中的位图),先将所有方块区绘制到这张位图上,绘制完成后,将位图贴到窗体上,最终效果将不再出现闪烁的情况。
  1. //窗口图面
  2. private readonly Graphics _wndGraphics;
  3. //缓冲区
  4. private readonly Bitmap _buffer;
  5. //缓冲区图面
  6. private readonly Graphics _bufferGraphics;
复制代码
  1. /// <summary>
  2. /// 表示游戏中一个方块区
  3. /// </summary>
  4. public sealed class Square
  5. ...0
复制代码
总结:

  至此,所有难点基本攻破,完整代码大家参考附件,代码基于Windows XP版扫雷做的模仿,笔者能力有限,不足之处请大家多多指点。
  源码:

  http://git.oschina.net/muxiangovo/Mine



上一篇:vcpkg —— VC++ 打包工具
下一篇:AOP 那点事儿
*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

我要投稿

推荐阅读


回页顶回复上一篇下一篇回列表
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 )

© 2001-2017 Comsenz Inc. Design: Dean. DiscuzFans.

返回顶部 返回列表