WinformGDI+入门级实例——扫雷游戏(附源码)
整体思路:
扫雷的游戏界面让我从一开始就想到了二维数组,事实上用二维数组来定义游戏数据确实是最符合人类思维的方式。(Square类会在后面解释)
//游戏数据
private readonly Square[,] _gameData;有了这个开头,接下来就是填充二维数组的数据了,对于数据,我最初的想法是用int或枚举,当然,这是可行的,但涉及一个问题就是高耦合,所有操作将都在高层执行,难以维护。
于是我们用一个Square类表示一个小方块区。
/// <summary>
/// 表示游戏中一个方块区
/// </summary>
public sealed class Square
...以枚举表示方块区的状态:
/// <summary>
/// 方块区状态
/// </summary>
public enum SquareStatus
{
/// <summary>
/// 闲置
/// </summary>
Idle,
/// <summary>
/// 已打开
/// </summary>
Opened,
/// <summary>
/// 已标记
/// </summary>
Marked,
/// <summary>
/// 已质疑
/// </summary>
Queried,
/// <summary>
/// 游戏结束
/// </summary>
GameOver,
/// <summary>
/// 标记失误(仅在游戏结束时用于绘制)
/// </summary>
MarkMissed
}用Game类来表示一局游戏,其中包含游戏数据、游戏等级、雷区数、布雷方法等。
/// <summary>
/// 游戏对象
/// </summary>
public sealed class Game
...难点攻破:
游戏不大,涉及的难点也就不多,但对于刚接触GDI+的读者,一些地方还是比较麻烦的。
逻辑难点1:布雷
扫雷游戏有一个附加规则,就是第一次单击不论如何都不会踩到雷区,由于这个规则的存在,我们不能将布雷操作做在第一次单击之前。所以我们在游戏开局时假设所有方块区都没有雷。
/// <summary>
/// 开始游戏
/// </summary>
public void Start()
{
//假设所有方块区均非雷区
for (int i = 0; i < _gameData.GetLength(0); i++)
for (int j = 0; j < _gameData.GetLength(1); j++)
_gameData = new Square(new Point(i, j), false, 0);
}随后,在开局后第一次单击时布雷。
/// <summary>
/// 布雷
/// </summary>
/// <param name="startPt">首次单击点</param>
private void Mine(Point startPt)
{
Size area = new Size(_gameData.GetLength(0), _gameData.GetLength(1));
List<Point> excluded = new List<Point> { startPt };
//随机创建雷区
for (int i = 0; i < _minesCount; i++)
{
Point pt = GetRandomPoint(area, excluded);
_gameData = new Square(pt, true, 0);
excluded.Add(pt);
}
//创建非雷区
for (int i = 0; i < _gameData.GetLength(0); i++)
for (int j = 0; j < _gameData.GetLength(1); j++)
if (!_gameData.Mined)//非雷区
{
int minesAround = EnumSquaresAround(new Point(i, j)).Cast<Square>().Count(square => square.Mined);//周围雷数
_gameData = new Square(new Point(i, j), false, minesAround);
}
_gameStarted = true;
}先创建雷区,再创建非雷区,以便我们在创建非雷区时可以计算出非雷区周围的雷数,枚举周围方块的方法我们用yield创建一个枚举器。
/// <summary>
/// 枚举周围所有方块区
/// </summary>
/// <param name="squarePt">原方块区</param>
/// <returns>枚举数</returns>
private IEnumerable EnumSquaresAround(Point squarePt)
{
int i = squarePt.X, j = squarePt.Y;
//周围所有方块区
for (int x = i - 1; x <= i + 1; ++x)//横向
{
if (x < 0 || x >= _gameData.GetLength(0))//越界
continue;
for (int y = j - 1; y <= j + 1; ++y)//纵向
{
if (y < 0 || y >= _gameData.GetLength(1))//越界
continue;
if (x == squarePt.X && y == squarePt.Y)//排除自身
continue;
yield return _gameData;
}
}
}逻辑难点2:当单击区周围无雷区(空白)时,自动批量打开周围所有非雷区
//如果是空白区,则递归相邻的所有空白区
if (_gameData.MinesAround == 0)
AutoOpenAround(logicalPt); /// <summary>
/// 自动打开周围非雷区方块(递归)
/// </summary>
/// <param name="squarePt">原方块逻辑坐标</param>
private void AutoOpenAround(Point squarePt)
{
//遍历周围方块
foreach (Square square in EnumSquaresAround(squarePt))
{
if (square.Mined || square.Status == Square.SquareStatus.Marked || square.Status == Square.SquareStatus.Opened)
continue;
square.LeftClick();//打开
//周围无雷区
if (square.MinesAround == 0)
AutoOpenAround(square.Location);//递归打开
}
}绘图难点1:双缓冲以克服闪烁
从二维数组的结构来看,我们需要遍历整个二维数组,然后把每个Square绘制到winform上,但这会造成强烈的闪烁效果。因为是实时绘图,绘制的每一步都会实时显示在窗口上,所以我们看到的效果就是一个方块区一个方块区的出现在窗口上。
为了克服这种不友好的闪烁,双缓冲出现了,思路就是创建一个缓冲区(通常是一个内存中的位图),先将所有方块区绘制到这张位图上,绘制完成后,将位图贴到窗体上,最终效果将不再出现闪烁的情况。
//窗口图面
private readonly Graphics _wndGraphics;
//缓冲区
private readonly Bitmap _buffer;
//缓冲区图面
private readonly Graphics _bufferGraphics; /// <summary>
/// 绘制一帧
/// </summary>
public void Draw()
{
for (int i = 0; i < _gameData.GetLength(0); i++)
for (int j = 0; j < _gameData.GetLength(1); j++)
_gameData.Draw(_bufferGraphics);
_wndGraphics.DrawImage(_buffer, new Point(_gameFieldOffset.Width, _gameFieldOffset.Height));
}总结:
至此,所有难点基本攻破,完整代码大家参考附件,代码基于Windows XP版扫雷做的模仿,笔者能力有限,不足之处请大家多多指点。
源码:
http://git.oschina.net/muxiangovo/Mine
文档来源:开源中国社区https://my.oschina.net/muxiang/blog/752040
页:
[1]