文档章节

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

穆翔
 穆翔
发布于 2016/09/27 10:05
字数 1250
阅读 1.7K
收藏 48

整体思路:

扫雷的游戏界面让我从一开始就想到了二维数组,事实上用二维数组来定义游戏数据确实是最符合人类思维的方式。(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[i, j] = 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[pt.X, pt.Y] = 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[i, j].Mined)//非雷区
            {
                int minesAround = EnumSquaresAround(new Point(i, j)).Cast<Square>().Count(square => square.Mined);//周围雷数

                _gameData[i, j] = 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[x, y];
        }
    }
}

逻辑难点2:当单击区周围无雷区(空白)时,自动批量打开周围所有非雷区

//如果是空白区,则递归相邻的所有空白区
if (_gameData[logicalPt.X, logicalPt.Y].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[i, j].Draw(_bufferGraphics);

    _wndGraphics.DrawImage(_buffer, new Point(_gameFieldOffset.Width, _gameFieldOffset.Height));
}

总结:

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

源码:

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

© 著作权归作者所有

穆翔
粉丝 6
博文 1
码字总数 1250
作品 0
丰台
私信 提问
加载中

评论(6)

穆翔
穆翔 博主

引用来自“蓝水晶飞机”的评论

5年前用vb6 做了个相似的,现在都忘了当初怎么实现的哈哈……
😂 闲着没事写着玩的
穆翔
穆翔 博主

引用来自“JenkinZhou”的评论

不错,学习了!
😄 谢谢阅读
红薯官方
红薯官方
5年前用vb6 做了个相似的,现在都忘了当初怎么实现的哈哈……
JenkinZhou
JenkinZhou
不错,学习了!
穆翔
穆翔 博主

引用来自“红薯”的评论

源码应该放到码云平台上 git.oschina.net ,方便别人在线查看
好的~
红薯
红薯
源码应该放到码云平台上 git.oschina.net ,方便别人在线查看
移动开发之微信小程序——资料集合

本文转载自:知乎 有需要下载的客官可可以点击知乎去下载相关资料 一:官方地址集合: 1:官方工具:https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html?t=1476434678461 2:...

DJY1992
2016/12/21
0
0
Weex 开发小游戏是件很 high 的事儿

前言(废话) 自上一篇 Weex 体验文章《网易严选 App 感受 Weex 开发》发布以来,朋友们的反馈还是不错的,github 也意外得到了400+的 star,18%(有实践精神)的朋友们选择了 fork 下来试一...

木羽zwwill
2017/10/21
0
0
微信应用号开发必备技能都在这里了啦!

“微信应用号”就像平地里炸响的一声春雷,在互联网圈内炸开了锅,小代码小程序即将成为主流,H5迎来了自己的第二春。废话少说,关于微信应用号开发技能,你都掌握了吗? 没掌握的看这里,干...

咖啡
2016/09/23
0
0
Unity 3D游戏开发 pdf高清下载

在朋友推荐下,当时入门unity3d的时候阅读的书籍。这本书作为入门书籍去阅读很不错,构建了整个世界观和基础控件的使用、基础知识的解释。整个书籍不厚,但是内容还是很值得推荐。 在unity3d...

流浪的猫爱编程
2018/10/28
0
0
React-Native学习资料

我(web+android开发经验)学习React Native过程中接触的知识点和学习的线路图。 React Native第1天——环境配置及知识体系(http://my.oschina.net/addcn/blog/647290) 掌握环境配置及运行h...

addcn
2016/04/06
298
0

没有更多内容

加载失败,请刷新页面

加载更多

Kettle自定义jar包供javascript使用

我们都知道 Kettle 是用 Java 语言开发,并且可以在 JavaScript 里面直接调用 java 类方法。所以有些时候,我们可以自定义一些方法,来供 JavaScript 使用。 本篇文章有参考自:https://www...

CREATE_17
昨天
102
0
处理CSV文件中的逗号

我正在寻找有关如何处理正在创建的csv文件的建议,然后由我们的客户上传,并且该值可能带有逗号(例如公司名称)。 我们正在研究的一些想法是:带引号的标识符(值“,”值“,”等)或使用|...

javail
昨天
79
0
如何克隆一个Date对象?

将Date变量分配给另一个变量会将引用复制到同一实例。 这意味着更改一个将更改另一个。 如何实际克隆或复制Date实例? #1楼 简化版: Date.prototype.clone = function () { return new ...

技术盛宴
昨天
73
0
计算一个数的数位之和

计算一个数的数位之和 例如:128 :1+2+8 = 11 public int numSum(int num) { int sum = 0; do { sum += num % 10; } while ((num = num / 10) > 0); return sum;......

SongAlone
昨天
124
0
为什么图片反复压缩后普遍会变绿,而不是其他颜色?

作者:Lion Yang 链接:https://www.zhihu.com/question/29355920/answer/119088684 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 业余版概要:安卓的...

shzwork
昨天
81
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部