WinformGDI+入門級實例——掃雷游戲(附源碼)


寫在前面:

本文將作為一個入門級的、結合源碼的文章,旨在為剛剛接觸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版掃雷做的模仿,筆者能力有限,不足之處請大家多多指點。

附件:

附件下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM