最近一直在忙着單位核心開發組件的版本更新,前天加了一個通宵,昨天晚上卻睡不着,腦子里面突然不知怎的一直在想貪吃蛇的實現方法。以往也有類似的情況,白天一直想不通的問題,晚上做夢有時會想到更好的版本,於是抽出時間按照夢里想到的方法測試編寫一下,沒想到從打開VisualStudio到完成初稿測試,只用了4個小時。不敢獨享,又加上好久沒有寫文章了,於是將我的是實現方法寫出來供大家一起討論,高手也請多多指教。
完成實現截圖:

1、實現方法設計
貪吃蛇的主要三個元素是棋盤地圖、蛇身、獎勵豆,蛇身只能在棋盤地圖內進行移動,吃到獎勵豆,自身長度增加一格,蛇身碰到自己的身體則Game Over。
實現的難點在於如何判斷蛇身如何移動,什么時候吃了獎勵豆,蛇身咬住自己了等,這些其實都是在判斷格式。如果我們將每一個格子都定義為實體,然后棋盤地圖和蛇身以及獎勵包含多個格子實體,那么就可以直接使用LinQ來簡單實現查找和對比了。
按照上述的想法,我們定義如下實體對象:
格子ModelElement:包含橫坐標、縱坐標、格子是否包含獎勵屬性
地圖ModelMap:包含地圖的行數、列數、格子實體集合、格子大小、格子默認顏色
蛇身ModelSnake:包含移動方向、移動速度、格子實體集合、蛇身顏色
具體代碼如下:
using System.Collections.Generic; using System.Drawing; namespace imStudio.Game.EsurientSnake.Models { /// <summary> /// 格子實體 /// </summary> public class ModelElement { /// <summary> /// 橫坐標 /// </summary> public int Abscissa { get; set; } /// <summary> /// 縱坐標 /// </summary> public int Ordinate { get; set; } /// <summary> /// 獎勵屬性 /// </summary> public bool Bonus { get; set; } /// <summary> /// 初始化格子 /// </summary> public ModelElement() { Abscissa = 0; Ordinate = 0; Bonus = false; } } /// <summary> /// 蛇身實體 /// </summary> public class ModelSnake { /// <summary> /// 移動速度 /// </summary> public int Speed { get; set; } /// <summary> /// 蛇身顏色 /// </summary> public Color Color { get; set; } /// <summary> /// 運動方向 /// </summary> public ModelEnum.Direction Direction { get; set; } /// <summary> /// 蛇身格子實體結合 /// </summary> public List<ModelElement> Body { get; set; } } /// <summary> /// 地圖格子大小屬性實體 /// </summary> public class ModelBox { /// <summary> /// 格子寬度 /// </summary> public int Width { get; set; } /// <summary> /// 格子高度 /// </summary> public int Height { get; set; } } /// <summary> /// 地圖視圖 /// </summary> public class ModelMap { /// <summary> /// 行數 /// </summary> public int Row { get; set; } /// <summary> /// 列數 /// </summary> public int Column { get; set; } /// <summary> /// 棋盤紋路顏色 /// </summary> public Color Line { get; set; } /// <summary> /// 棋盤格子顏色 /// </summary> public Color Color { get; set; } /// <summary> /// 棋盤格子大小 /// </summary> public ModelBox Box { get; set; } /// <summary> /// 棋盤格子實體集合 /// </summary> public List<ModelElement> Body { get; set; } } }
有了上述的實體之后下面就可以開始正式編寫程序了。具體實現步驟如下:
- 描繪棋盤地圖
- 初始化蛇身
- 蛇身移動、蛇身增加、死亡判斷
- 隨機獎勵
2、描繪棋盤地圖
總體實現方法:按照預先定義的棋盤地圖實體初始化地圖,然后使用C#的System.Drawing中CreateGraphics方法來進行描繪地圖。
按照定義的棋盤格子初始化棋盤地圖實體:
/// <summary> /// 創建地圖 /// </summary> /// <param name="rowCount"></param> /// <param name="colCount"></param> /// <param name="boxWidth"></param> /// <param name="boxHeight"></param> /// <param name="line"></param> /// <param name="box"></param> /// <returns></returns> public static ModelMap GenMap(int rowCount, int colCount, int boxWidth, int boxHeight, Color line, Color box) { var map = new ModelMap { Row = rowCount, Column = colCount, Box = new ModelBox { Width = boxWidth, Height = boxHeight }, Line = line, Color = box, Body = new List<ModelElement>() }; #region 初始化地圖實體 for (int ri = 0; ri < rowCount; ri++) { for (int ci = 0; ci < colCount; ci++) { map.Body.Add(new ModelElement { Abscissa = ri, Ordinate = ci }); } } #endregion return map; }
有了實體之后,就開始進行描邊和填充格子:
/// <summary> /// 地圖描邊 /// </summary> /// <param name="panel"></param> /// <param name="map"></param> public static void DrawMap(Panel panel, ModelMap map) { #region 勾畫地圖 var g = panel.CreateGraphics(); #region 畫橫線 for (int ri = 0; ri <= map.Row; ri++) { g.DrawLine(new Pen(Color.Black), 0, ri * map.Box.Height, map.Column * map.Box.Width, ri * map.Box.Height); } #endregion #region 畫豎線 for (int ci = 0; ci <= map.Column; ci++) { g.DrawLine(new Pen(Color.Black), ci * map.Box.Width, 0, ci * map.Box.Height, map.Row * map.Box.Width); } #endregion #region 勾畫方塊 foreach (var b in map.Body) { DrawMapBox(panel, map.Color, b.Abscissa, b.Ordinate, map.Box.Width, map.Box.Height); } #endregion #endregion } /// <summary> /// 格子顏色填充 /// </summary> /// <param name="panel"></param> /// <param name="color"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="width"></param> /// <param name="height"></param> public static void DrawMapBox(Panel panel, Color color, int x, int y, int width, int height) { var g = panel.CreateGraphics(); g.FillRectangle(new HatchBrush(HatchStyle.Percent90, color), x * width + 1, y * height + 1,width - 1, height - 1); }
完成效果截圖:

3、初始化蛇身
總體實現方法:實現方法類似地圖描述,只是需要計算一下蛇身大小。
/// <summary> /// 在地圖上初始化蛇身 /// </summary> /// <param name="map"></param> /// <param name="snake"></param> /// <returns></returns> public static ModelSnake GenSnakeOnMap(ModelMap map,ModelSnake snake) { var sk = snake; var centerX = map.Row/2; var centerY = map.Column/2; sk.Body = new List<ModelElement> { new ModelElement { Abscissa = centerX, Ordinate = centerY }, new ModelElement { Abscissa = centerX, Ordinate = centerY - 1 } }; return sk; } /// <summary> /// 蛇身描繪至地圖 /// </summary> /// <param name="panel"></param> /// <param name="map"></param> /// <param name="snake"></param> /// <returns></returns> public static ModelSnake DrawSnakeOnMap(Panel panel, ModelMap map,ModelSnake snake) { snake = GenSnakeOnMap(map, snake); foreach (var b in snake.Body) { DrawMapBox(panel, snake.Color, b.Abscissa, b.Ordinate, map.Box.Width, map.Box.Height); } return snake; }
完成實現截圖:

4、蛇身移動、增加蛇身、死亡判斷
總體實現方法:設定一個Timer實現每隔多少時間蛇身移動一次。每移動一次,蛇身尾部消失,頭部按照運動方向增加一格。這個時候就體現到使用實體的好處了,只需要對蛇身格子實體集合進行一次Remove和一次Insert就可以了。而蛇身是否咬到自己,判斷起來就更加簡單了,直接使用LinQ判斷即將Insert的實體是否已經在蛇身實體結合中存在即可。實現代碼如下:
/// <summary> /// 蛇身在移動圖上移動 /// </summary> /// <param name="panel"></param> /// <param name="map"></param> /// <param name="snake"></param> /// <param name="direction"></param> /// <param name="enableBack"></param> /// <param name="dead"></param> /// <returns></returns> public static ModelSnake MoveSnakeOnMap(Panel panel, ModelMap map, ModelSnake snake, ModelEnum.Direction direction, bool enableBack, out bool dead) { dead = false; if (!enableBack) { if (direction.Equals(ModelEnum.Direction.Up) && snake.Direction.Equals(ModelEnum.Direction.Down)) direction = snake.Direction; else if (direction.Equals(ModelEnum.Direction.Down) && snake.Direction.Equals(ModelEnum.Direction.Up)) direction = snake.Direction; else if (direction.Equals(ModelEnum.Direction.Left) && snake.Direction.Equals(ModelEnum.Direction.Right)) direction = snake.Direction; else if (direction.Equals(ModelEnum.Direction.Right) && snake.Direction.Equals(ModelEnum.Direction.Left)) direction = snake.Direction; } else { if (direction.Equals(ModelEnum.Direction.Up) && snake.Direction.Equals(ModelEnum.Direction.Down)) snake.Body.Reverse(); else if (direction.Equals(ModelEnum.Direction.Down) && snake.Direction.Equals(ModelEnum.Direction.Up)) snake.Body.Reverse(); else if (direction.Equals(ModelEnum.Direction.Left) && snake.Direction.Equals(ModelEnum.Direction.Right)) snake.Body.Reverse(); else if (direction.Equals(ModelEnum.Direction.Right) && snake.Direction.Equals(ModelEnum.Direction.Left)) snake.Body.Reverse(); } var head = new ModelElement { Abscissa = snake.Body[0].Abscissa, Ordinate = snake.Body[0].Ordinate }; switch (direction) { case ModelEnum.Direction.Left: head.Abscissa--; break; case ModelEnum.Direction.Right: head.Abscissa++; break; case ModelEnum.Direction.Up: head.Ordinate--; break; case ModelEnum.Direction.Down: head.Ordinate++; break; } if (head.Abscissa < 0) head.Abscissa = map.Column - 1; else if (head.Abscissa == map.Column) head.Abscissa = 0; if (head.Ordinate < 0) head.Ordinate = map.Row - 1; else if (head.Ordinate == map.Row) head.Ordinate = 0; var d = snake.Body.SingleOrDefault(t => t.Abscissa == head.Abscissa && t.Ordinate == head.Ordinate); if (d != null) { dead = true; } var tail = snake.Body[snake.Body.Count - 1]; var m = map.Body.SingleOrDefault(t => t.Bonus && t.Abscissa == tail.Abscissa && t.Ordinate == tail.Ordinate); if (m == null) { DrawMapBox(panel, map.Color, tail.Abscissa, tail.Ordinate, map.Box.Width, map.Box.Height); snake.Body.Remove(tail); } else { DrawMapBox(panel, snake.Color, head.Abscissa, head.Ordinate, map.Box.Width, map.Box.Height); m.Bonus = false; } DrawMapBox(panel, snake.Color, head.Abscissa, head.Ordinate, map.Box.Width, map.Box.Height); snake.Body.Insert(0, head); snake.Direction = direction; return snake; }
5、隨機獎勵
總體實現方法:在蛇身移動的Insert之前,判斷一下即將移動到的地圖格子是否包含獎勵就可以了。如果包含格子,多增加一個蛇身格子就可以了。第四部分代碼中已經包含這部分代碼了。
到此核心的代碼就已經完成了,剩下的就是將這些方法組合起來了。在這里就不多寫了,附上源代碼,額外增加了獎勵永存和倒車功能,開玩~~~
備注:暫未實現鍵盤監測,只能通過界面上的箭頭按鈕進行控制。

源代碼下載地址:https://github.com/songhaipeng/imStudio.Game
