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

寫在前面:

本文將做爲一個入門級的、結合源碼的文章,旨在爲剛剛接觸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:佈雷

掃雷遊戲有一個附加規則,就是第一次單擊不論如何都不會踩到雷區,因爲這個規則的存在,咱們不能將佈雷操做作在第一次單擊以前。因此咱們在遊戲開局時假設全部方塊區都沒有雷。遞歸

 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 }

邏輯難點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版掃雷作的模仿,筆者能力有限,不足之處請你們多多指點。

附件:

附件下載

相關文章
相關標籤/搜索