本文將做爲一個入門級的、結合源碼的文章,旨在爲剛剛接觸GDI+編程或對相關知識感興趣的讀者作一個入門講解。遊戲尚且未完善,但基本功能都有,完整源碼在文章結尾的附件中。編程
掃雷的遊戲界面讓我從一開始就想到了二維數組,事實上用二維數組來定義遊戲數據確實是最符合人類思惟的方式。(Square類會在後面解釋)數組
1 //遊戲數據 2 private readonly Square[,] _gameData;
有了這個開頭,接下來就是填充二維數組的數據了,對於數據,我最初的想法是用int或枚舉,固然,這是可行的,但涉及一個問題就是高耦合,全部操做將都在高層執行,難以維護。dom
因而咱們用一個Square類表示一個小方塊區。spa
1 /// <summary> 2 /// 表示遊戲中一個方塊區 3 /// </summary> 4 public sealed class Square
...
以枚舉表示方塊區的狀態:code
1 /// <summary> 2 /// 方塊區狀態 3 /// </summary> 4 public enum SquareStatus 5 { 6 /// <summary> 7 /// 閒置 8 /// </summary> 9 Idle, 10 /// <summary> 11 /// 已打開 12 /// </summary> 13 Opened, 14 /// <summary> 15 /// 已標記 16 /// </summary> 17 Marked, 18 /// <summary> 19 /// 已質疑 20 /// </summary> 21 Queried, 22 /// <summary> 23 /// 遊戲結束 24 /// </summary> 25 GameOver, 26 /// <summary> 27 /// 標記失誤(僅在遊戲結束時用於繪製) 28 /// </summary> 29 MarkMissed 30 }
用Game類來表示一局遊戲,其中包含遊戲數據、遊戲等級、雷區數、佈雷方法等。 orm
1 /// <summary> 2 /// 表示一局遊戲 3 /// </summary> 4 public sealed class Game : IDisposable 5 ...
遊戲不大,涉及的難點也就很少,但對於剛接觸GDI+的讀者,一些地方仍是比較麻煩的。blog
掃雷遊戲有一個附加規則,就是第一次單擊不論如何都不會踩到雷區,因爲這個規則的存在,咱們不能將佈雷操做作在第一次單擊以前。因此咱們在遊戲開局時假設全部方塊區都沒有雷。遞歸
1 /// <summary> 2 /// 開始遊戲 3 /// </summary> 4 public void Start() 5 { 6 //假設全部方塊區均非雷區 7 for (int i = 0; i < _gameData.GetLength(0); i++) 8 for (int j = 0; j < _gameData.GetLength(1); j++) 9 _gameData[i, j] = new Square(new Point(i, j), false, 0); 10 }
隨後,在開局後第一次單擊時佈雷。遊戲
1 /// <summary> 2 /// 佈雷 3 /// </summary> 4 /// <param name="startPt">首次單擊點</param> 5 private void Mine(Point startPt) 6 { 7 Size area = new Size(_gameData.GetLength(0), _gameData.GetLength(1)); 8 List<Point> excluded = new List<Point> { startPt }; 9 10 //隨機建立雷區 11 for (int i = 0; i < _minesCount; i++) 12 { 13 Point pt = GetRandomPoint(area, excluded); 14 _gameData[pt.X, pt.Y] = new Square(pt, true, 0); 15 excluded.Add(pt); 16 } 17 18 //建立非雷區 19 for (int i = 0; i < _gameData.GetLength(0); i++) 20 for (int j = 0; j < _gameData.GetLength(1); j++) 21 if (!_gameData[i, j].Mined)//非雷區 22 { 23 int minesAround = EnumSquaresAround(new Point(i, j)).Cast<Square>().Count(square => square.Mined);//周圍雷數 24 25 _gameData[i, j] = new Square(new Point(i, j), false, minesAround); 26 } 27 28 _gameStarted = true; 29 }
先建立雷區,再建立非雷區,以便咱們在建立非雷區時能夠計算出非雷區周圍的雷數,枚舉周圍方塊的方法咱們用yield建立一個枚舉器。ip
1 /// <summary> 2 /// 枚舉周圍全部方塊區 3 /// </summary> 4 /// <param name="squarePt">原方塊區</param> 5 /// <returns>枚舉數</returns> 6 private IEnumerable EnumSquaresAround(Point squarePt) 7 { 8 int i = squarePt.X, j = squarePt.Y; 9 10 //周圍全部方塊區 11 for (int x = i - 1; x <= i + 1; ++x)//橫向 12 { 13 if (x < 0 || x >= _gameData.GetLength(0))//越界 14 continue; 15 16 for (int y = j - 1; y <= j + 1; ++y)//縱向 17 { 18 if (y < 0 || y >= _gameData.GetLength(1))//越界 19 continue; 20 21 if (x == squarePt.X && y == squarePt.Y)//排除自身 22 continue; 23 24 yield return _gameData[x, y]; 25 } 26 } 27 }
1 //若是是空白區,則遞歸相鄰的全部空白區 2 if (_gameData[logicalPt.X, logicalPt.Y].MinesAround == 0) 3 AutoOpenAround(logicalPt);
1 /// <summary> 2 /// 自動打開周圍非雷區方塊(遞歸) 3 /// </summary> 4 /// <param name="squarePt">原方塊邏輯座標</param> 5 private void AutoOpenAround(Point squarePt) 6 { 7 //遍歷周圍方塊 8 foreach (Square square in EnumSquaresAround(squarePt)) 9 { 10 if (square.Mined || square.Status == Square.SquareStatus.Marked || square.Status == Square.SquareStatus.Opened) 11 continue; 12 13 square.LeftClick();//打開 14 //周圍無雷區 15 if (square.MinesAround == 0) 16 AutoOpenAround(square.Location);//遞歸打開 17 } 18 }
從二維數組的結構來看,咱們須要遍歷整個二維數組,而後把每一個Square繪製到winform上,但這會形成強烈的閃爍效果。由於是實時繪圖,繪製的每一步都會實時顯示在窗口上,因此咱們看到的效果就是一個方塊區一個方塊區的出如今窗口上。
爲了克服這種不友好的閃爍,雙緩衝出現了,思路就是建立一個緩衝區(一般是一個內存中的位圖),先將全部方塊區繪製到這張位圖上,繪製完成後,將位圖貼到窗體上,最終效果將再也不出現閃爍的狀況。
1 //窗口圖面 2 private readonly Graphics _wndGraphics; 3 //緩衝區 4 private readonly Bitmap _buffer; 5 //緩衝區圖面 6 private readonly Graphics _bufferGraphics;
1 /// <summary> 2 /// 繪製一幀 3 /// </summary> 4 public void Draw() 5 { 6 for (int i = 0; i < _gameData.GetLength(0); i++) 7 for (int j = 0; j < _gameData.GetLength(1); j++) 8 _gameData[i, j].Draw(_bufferGraphics); 9 10 _wndGraphics.DrawImage(_buffer, new Point(_gameFieldOffset.Width, _gameFieldOffset.Height)); 11 }
至此,全部難點基本攻破,完整代碼你們參考附件,代碼基於Windows XP版掃雷作的模仿,筆者能力有限,不足之處請你們多多指點。