• WinformGDI+入门级实例——扫雷游戏(附源码)


    写在前面:

    本文将作为一个入门级的、结合源码的文章,旨在为刚刚接触GDI+编程或对相关知识感兴趣的读者做一个入门讲解。游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中。

    整体思路:

    扫雷的游戏界面让我从一开始就想到了二维数组,事实上用二维数组来定义游戏数据确实是最符合人类思维的方式。(Square类会在后面解释)

    1 //游戏数据
    2 private readonly Square[,] _gameData;

    有了这个开头,接下来就是填充二维数组的数据了,对于数据,我最初的想法是用int或枚举,当然,这是可行的,但涉及一个问题就是高耦合,所有操作将都在高层执行,难以维护。

    于是我们用一个Square类表示一个小方块区。

    1 /// <summary>
    2 /// 表示游戏中一个方块区
    3 /// </summary>
    4 public sealed class Square
    ...

    以枚举表示方块区的状态:

     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 : IDisposable
    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     //随机创建雷区
    11     for (int i = 0; i < _minesCount; i++)
    12     {
    13         Point pt = GetRandomPoint(area, excluded);
    14         _gameData[pt.X, pt.Y] = new Square(pt, true, 0);
    15         excluded.Add(pt);
    16     }
    17 
    18     //创建非雷区
    19     for (int i = 0; i < _gameData.GetLength(0); i++)
    20         for (int j = 0; j < _gameData.GetLength(1); j++)
    21             if (!_gameData[i, j].Mined)//非雷区
    22             {
    23                 int minesAround = EnumSquaresAround(new Point(i, j)).Cast<Square>().Count(square => square.Mined);//周围雷数
    24 
    25                 _gameData[i, j] = new Square(new Point(i, j), false, minesAround);
    26             }
    27 
    28     _gameStarted = true;
    29 }

    先创建雷区,再创建非雷区,以便我们在创建非雷区时可以计算出非雷区周围的雷数,枚举周围方块的方法我们用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     //周围所有方块区
    11     for (int x = i - 1; x <= i + 1; ++x)//横向
    12     {
    13         if (x < 0 || x >= _gameData.GetLength(0))//越界
    14             continue;
    15 
    16         for (int y = j - 1; y <= j + 1; ++y)//纵向
    17         {
    18             if (y < 0 || y >= _gameData.GetLength(1))//越界
    19                 continue;
    20 
    21             if (x == squarePt.X && y == squarePt.Y)//排除自身
    22                 continue;
    23 
    24             yield return _gameData[x, y];
    25         }
    26     }
    27 }

    逻辑难点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 
    13         square.LeftClick();//打开
    14         //周围无雷区
    15         if (square.MinesAround == 0)
    16             AutoOpenAround(square.Location);//递归打开
    17     }
    18 }

    绘图难点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 void Draw()
     5 {
     6     for (int i = 0; i < _gameData.GetLength(0); i++)
     7         for (int j = 0; j < _gameData.GetLength(1); j++)
     8             _gameData[i, j].Draw(_bufferGraphics);
     9 
    10     _wndGraphics.DrawImage(_buffer, new Point(_gameFieldOffset.Width, _gameFieldOffset.Height));
    11 }

    总结:

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

    附件:

    附件下载

    Never give up!
  • 相关阅读:
    如何在一个控件上同时实现单触和多触事件
    看看iOS 5.0 beta 6都有哪些变化吧
    转,net实现下载
    转检测到潜在危险
    如何分析已有项目如何能够得到最快最大的提升???
    转Unity 入门
    近段总结
    转,net几个热点问题
    dll动态链接库
    转使用NUnit在.Net编程中进行单元测试
  • 原文地址:https://www.cnblogs.com/CoffeeMX/p/5864974.html
Copyright © 2020-2023  润新知