整體思路:
掃雷的遊戲界面讓我從一開始就想到了二維數組,事實上用二維數組來定義遊戲數據確實是最符合人類思維的方式。(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