寫在前面:
本文將作為一個入門級的、結合源碼的文章,旨在為剛剛接觸GDI+編程或對相關知識感興趣的讀者做一個入門講解。游戲尚且未完善,但基本功能都有,完整源碼在文章結尾的附件中。
整體思路:
掃雷的游戲界面讓我從一開始就想到了二維數組,事實上用二維數組來定義游戲數據確實是最符合人類思維的方式。(Square類會在后面解釋)
1 //游戲數據 2 private readonly Square[,] _gameData;
有了這個開頭,接下來就是填充二維數組的數據了,對於數據,我最初的想法是用int或枚舉,當然,這是可行的,但涉及一個問題就是高耦合,所有操作將都在高層執行,難以維護。
於是我們用一個Square類表示一個小方塊區。
1 /// <summary> 2 /// 表示游戲中一個方塊區 3 /// </summary> 4 public sealed class Square
...
以枚舉表示方塊區的狀態:
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類來表示一局游戲,其中包含游戲數據、游戲等級、雷區數、布雷方法等。
1 /// <summary> 2 /// 表示一局游戲 3 /// </summary> 4 public sealed class Game : IDisposable 5 ...
難點攻破:
游戲不大,涉及的難點也就不多,但對於剛接觸GDI+的讀者,一些地方還是比較麻煩的。
邏輯難點1:布雷
掃雷游戲有一個附加規則,就是第一次單擊不論如何都不會踩到雷區,由於這個規則的存在,我們不能將布雷操作做在第一次單擊之前。所以我們在游戲開局時假設所有方塊區都沒有雷。
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創建一個枚舉器。
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 }
邏輯難點2:當單擊區周圍無雷區(空白)時,自動批量打開周圍所有非雷區
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 }
繪圖難點1:雙緩沖以克服閃爍
從二維數組的結構來看,我們需要遍歷整個二維數組,然后把每個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版掃雷做的模仿,筆者能力有限,不足之處請大家多多指點。