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[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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章