掃雷的遊戲界面讓我從一開始就想到了二維數組,事實上用二維數組來定義遊戲數據確實是最符合人類思惟的方式。(Square類會在後面解釋)git
//遊戲數據 private readonly Square[,] _gameData;
有了這個開頭,接下來就是填充二維數組的數據了,對於數據,我最初的想法是用int或枚舉,固然,這是可行的,但涉及一個問題就是高耦合,全部操做將都在高層執行,難以維護。數組
因而咱們用一個Square類表示一個小方塊區。dom
/// <summary> /// 表示遊戲中一個方塊區 /// </summary> public sealed class Square ...
以枚舉表示方塊區的狀態:spa
/// <summary> /// 方塊區狀態 /// </summary> public enum SquareStatus { /// <summary> /// 閒置 /// </summary> Idle, /// <summary> /// 已打開 /// </summary> Opened, /// <summary> /// 已標記 /// </summary> Marked, /// <summary> /// 已質疑 /// </summary> Queried, /// <summary> /// 遊戲結束 /// </summary> GameOver, /// <summary> /// 標記失誤(僅在遊戲結束時用於繪製) /// </summary> MarkMissed }
用Game類來表示一局遊戲,其中包含遊戲數據、遊戲等級、雷區數、佈雷方法等。.net
/// <summary> /// 遊戲對象 /// </summary> public sealed class Game ...
遊戲不大,涉及的難點也就很少,但對於剛接觸GDI+的讀者,一些地方仍是比較麻煩的。code
掃雷遊戲有一個附加規則,就是第一次單擊不論如何都不會踩到雷區,因爲這個規則的存在,咱們不能將佈雷操做作在第一次單擊以前。因此咱們在遊戲開局時假設全部方塊區都沒有雷。orm
/// <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]; } } }
//若是是空白區,則遞歸相鄰的全部空白區 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);//遞歸打開 } }
從二維數組的結構來看,咱們須要遍歷整個二維數組,而後把每一個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