本文将作为一个入门级的、结合源码的文章,旨在为刚刚接触 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...
游戏不大,涉及的难点也就不多,但对于刚接触 GDI + 的读者,一些地方还是比较麻烦的。
扫雷游戏有一个附加规则,就是第一次单击不论如何都不会踩到雷区,由于这个规则的存在,我们不能将布雷操作做在第一次单击之前。所以我们在游戏开局时假设所有方块区都没有雷。
- 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 excluded = new List {
- 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().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
- }
- 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
- }
从二维数组的结构来看,我们需要遍历整个二维数组,然后把每个 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 版扫雷做的模仿,笔者能力有限,不足之处请大家多多指点。
来源: http://www.cnblogs.com/CoffeeMX/p/5864974.html