1)、引言
學習編程,我個人覺得最好的辦法就是根據自己的水平不斷的給自己設定一個小目標。而這個小目標就是一個有意思的項目,通過完成這個項目,對自己的成果(也包括失敗的)進行分析總結,從中提煉出對應的技術並分享出來,不斷的往復,如此,為的就是讓我們永遠保持編寫程序的興趣和熱情,完了,還提高我們的技術。而本文就是總結自己的一個小目標(基於控制台實現的貪吃蛇游戲而寫的總結)
2)、技術實現
大家小時候一定玩過貪吃蛇的游戲。貪吃蛇游戲的控制過程其實也不復雜。簡單的可以概括為以下4個部分。
1.1 、組成蛇的小塊以及食物(Block)
在本程序中,食物以及蛇的組成都用一個對象表示,因為它們的作用都是一樣,僅僅只需要一個坐標對。以及提供一個靜態方法,即可以通過在游戲的地圖內生隨機產生一個坐標對並返回Block對象。
1.2、管理蛇的部分(Snake)
程序的主體主要“蛇”這個對象的屬性以及方法的設計。現在我們想想在游戲中這個對象需要有哪些屬性以及行為,首先“蛇”有長度以及“蛇”的組成,這就是蛇的屬性,可以由一個相鄰的Block類型的集合snakeList來表示;其次,蛇能移動(上下左右),能吃食物,並且不能碰到邊框以及頭部不能觸碰自己的身體,這就可以抽象為蛇的三個行為,Move(),IsEatFood (),IsOver(),分別為移動,吃食物,檢測自身是否滿足游戲的規則。
1、Move()
//蛇移動的關鍵代碼如下。
public void Move(Direction dir) { Block head = this.snakeLIst[0];//獲取蛇頭 this.snakeLIst.RemoveAt(this.snakeLIst .Count -1);//移除末尾項 Block newBlock=null; switch (dir)//獲取蛇當前運行的方向,然后把根據蛇頭的位置計算出新的蛇頭的位置。相當於把蛇尾的坐標進行計算插入到蛇頭。 { case Direction.Top : newBlock = new Block(head.Row-1, head.Col); break; case Direction .Bottom : newBlock = new Block(head.Row+1, head.Col); break; case Direction .Left : newBlock = new Block(head.Row, head.Col-1); break; case Direction .Right: newBlock = new Block(head.Row , head.Col+1); break; } this.snakeLIst.Insert(0, newBlock);//將新的位置插入到蛇頭 }
蛇移動的程序的動態過程如下圖:(以向左走為例)
2、IsEatFood ()
//代碼如下
/// <summary> /// 判斷蛇是否達到食物的位置,如果到達這eat /// </summary> /// <param name="b">食物對象</param> /// <returns>返回bool,好讓調用方知道是否需要產生新的食物</returns> public bool IsEatFood(Block b) { Block head = this.snakeLIst[0];//獲取蛇頭 if (head.IsEqual(b))//是食物的位置一致 { this.snakeLIst.Add(b);//添加一個block到蛇的集合中,並這下一次move中移動到蛇頭,保證有序。 return true ; } return false ; }
3、IsOver()
//代碼如下
public bool IsOver() { Block head = this.snakeLIst[0]; if (head.Row == 0 || head.Col == 0 || head.Row == 25 || head.Col == 80)//是否遇到邊界 { return true; } for (int i = 1; i < this.snakeLIst.Count; i++)//是否遇到自身 { if (head.IsEqual(this.snakeLIst[i])) { return true ; } } return false; }
1.3、管理游戲界面的類(MapManager)
我們都知道游戲都是畫面不斷變化的一個過程,所以我們必須在極短的時間內去更新游戲的畫面,所以就離不開定時器。然后就可以實時的去繪制游戲的當前的狀態,那么一系列的狀態連起來就一個動態的游戲畫面。
- 1、我把整個游戲的地圖存儲在一個二位數組里面,如果其值為0則代表為空的,如果為1,則輸出“#”,因為這和在窗體中繪制游戲畫面不一樣,控制台只能從左至右,從上至下的輸出。
- 2、首先繪制邊界,實際上就是把二維數組(地圖)中的某一些值賦值為1.
- 3、建立“對象”並初始化,遍歷組成蛇的Block集合,來獲取”蛇“在二維數組(地圖)的位置,並把對應的位置賦值為1.
- 4、初始化食物,也即是block對象,在二維數組(地圖)中找到他的位置,和第3步一樣將對應的位置賦值為1。
- 5、上面的4步已經把“蛇”、食物以及游戲的邊界繪制出來,接下來就是蛇的運動了。蛇的運動就是每次調用“蛇”的Move()行為,然后再蛇運行之前都要判斷下面兩個條件,第一,當前蛇頭的位置是不是在食物的位置,如果在則吃掉食物,生成新的食物,調用的方法為IsEatFood();第二,“蛇”是否滿足規則,如果不滿足則游戲結束,調用IsOver()。
- 6、當上面的步驟都完成之后,即二維數組(地圖)都賦值好了,然后再從上之下,從左至右依次輸出(其實就是一長串字符串)。最后一步才是真正的在界面輸出,其中前面的步驟都是游戲畫面的緩存。關於計時器是使
其中System.Threading中的Timer類,具體的用戶可以查看其它資料,也可以看后面程序中是如何使用Timer類的
1.4、與用戶交互的設計
程序將用戶交互的接口放在了MapManager,主要功能為,啟動計時器去管理游戲的繪制與運行,然后另外就是處理用戶的輸入去改變游戲運行狀態。
其中,需要注意的是,由於是基於控制台的實現,用戶的輸入肯定是不能按enter之后然后程序才能接受,而是實時的接受用戶的輸入且還不能將用戶的輸入顯示到控制中,還好c#提供了Console.Readkey(true),可以滿足程序的要求。
1.5、下面是程序運行的流程圖
注意:
其中需要注意的是,最后輸出賦值好的二維數組(地圖)時候,不是遍歷二維數組(地圖)遍歷一項就輸出一項,而是用StringBuilding對象去添加,直到遍歷完了,一次性輸出StringBuilding對象,達到雙緩存的效果,使得控制台繪制不會閃爍。
3)程序編碼的實現
本程序是基於c#控制台實現的,開發工具為2013
1、Block類
public class Block { private int x; private int y; public Block() { } public Block(int x, int y) { this.x = x; this.y = y; } public int Row { get { return this.x; } set { this.x = value; } } public int Col { get { return this.y; } set { this.y = value; } } public bool IsEqual(Block b) { if (this.x == b.Row&& this.y == b.Col) { return true; } return false; } public static Block ProvideFood() { Random r=new Random (); Block b = new Block(r.Next(1, 25), r.Next(1, 80)); return b; } }
2、Snake類
public class Snake { List<Block> snakeLIst = new List<Block>();//存儲蛇的結構 public List<Block> SnakeLIst { get { return snakeLIst; } } public Snake() { InitSnake(); } private void InitSnake() { int rowStart = 2; int colStart=5; int lenth = 20+colStart ; Block b; for (int i = colStart; i < lenth; i++) { b = new Block(rowStart ,i); this.snakeLIst.Insert(0, b); } } /// <summary> /// 判斷蛇是否達到食物的位置,如果到達這eat /// </summary> /// <param name="b">食物對象</param> /// <returns>返回bool,好讓調用方知道是否需要產生新的食物</returns> public bool IsEatFood(Block b) { Block head = this.snakeLIst[0];//獲取蛇頭 if (head.IsEqual(b))//是食物的位置一致 { this.snakeLIst.Add(b);//添加一個block到蛇的集合中,並這下一次move中移動到蛇頭,保證有序。 return true ; } return false ; } public void Move(Direction dir) { Block head = this.snakeLIst[0];//獲取蛇頭 this.snakeLIst.RemoveAt(this.snakeLIst .Count -1);//移除末尾項 Block newBlock=null; switch (dir)//獲取蛇當前運行的方向,然后把根據蛇頭的位置計算出新的蛇頭的位置。相當於把蛇尾的坐標進行計算插入到蛇頭。 { case Direction.Top : newBlock = new Block(head.Row-1, head.Col); break; case Direction .Bottom : newBlock = new Block(head.Row+1, head.Col); break; case Direction .Left : newBlock = new Block(head.Row, head.Col-1); break; case Direction .Right: newBlock = new Block(head.Row , head.Col+1); break; } this.snakeLIst.Insert(0, newBlock);//將新的位置插入到蛇頭 } public bool IsOver() { Block head = this.snakeLIst[0]; if (head.Row == 0 || head.Col == 0 || head.Row == 25 || head.Col == 80)//是否遇到邊界 { return true; } for (int i = 1; i < this.snakeLIst.Count; i++)//是否遇到自身 { if (head.IsEqual(this.snakeLIst[i])) { return true ; } } return false; } }
3、MapManager類(包括與用戶的交互)
public class MapManager { const int row = 25; const int col = 80; Snake snake;//蛇 Block b;//食物 Timer t;//定時器 int[,] gameMap = new int[row, col];//地圖 int count=0; StringBuilder mapBuffer;//緩存區 bool isNormal = true; //初始化地圖+繪制邊界 private void InitMap() { for (int i = 0; i < row; i++) { if (i == row - 1 || i == 0) { for (int j = 0; j < col; j++) { this.gameMap[i, j] = 1; } } else { for (int j = 0; j < col; j++) { if (j == col - 1 || j == 0) { this.gameMap[i, j] = 1; } else { this.gameMap[i, j] = 0; } } } } } //繪制蛇 private void InitSnake() { foreach (var s in snake.SnakeLIst) { gameMap[s.Row, s.Col] = 1; } } //繪制食物 private void InitFood() { gameMap[this.b.Row, this.b.Col] = 1; } //輸出控制台(游戲畫面) private void DrawMap() { mapBuffer.Clear(); Console.Clear(); for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { if (gameMap[i, j] == 1) { mapBuffer.Append("*"); } else { mapBuffer.Append(" "); } } mapBuffer.Append("\n"); } Console.WriteLine("\n-----------------------------當前得分{0}------------------------\n", count); Console .Write(mapBuffer .ToString ());//從緩存區中輸出整個游戲畫面 } //游戲運行管理 private void GameRun(object o) { InitMap(); InitSnake(); InitFood(); if (snake.IsEatFood(b)) { b = Block.ProvideFood();//產生新的食物 count++;//得分 } snake.Move(GlobalVar.dir); if (snake.IsOver()) { GameOver(); } DrawMap(); } private void GameOver() { Console.Clear(); Console.WriteLine("Game Over"); isNormal = false; t.Dispose(); } private void GameInit() { Console.WriteLine("按[w,s.a.d]作為上下左右,按[q]退出游戲!!!"); Console.WriteLine("按任何鍵進入游戲"); Console.ReadKey(true); } //程序開始,該方法包括啟動定時器,以及與用戶的交互 public void Start() { GameInit(); snake = new Snake(); b = Block.ProvideFood(); mapBuffer = new StringBuilder(); //GameRun(null); t = new Timer(GameRun, null, 200, 100); char c; while (isNormal) { c=Console .ReadKey(true ).KeyChar ; switch (c) { case 's': if (GlobalVar.dir != Direction.Top) { GlobalVar.dir = Direction.Bottom; } break; case 'w': if (GlobalVar.dir != Direction.Bottom) { GlobalVar.dir = Direction.Top; } break; case 'a': if (GlobalVar.dir != Direction.Right) { GlobalVar.dir = Direction.Left; } break; case 'd': if (GlobalVar.dir != Direction.Left) { GlobalVar.dir = Direction.Right; } break; case 'q': GameOver(); break; } } Console.ReadLine(); } }
4、方向枚舉(Direction)
public enum Direction { Left,Right,Top,Bottom }
5、main(程序入口)
static void Main(string[] args) { MapManager mm = new MapManager(); mm.Start(); }
6、程序效果
本來想插入視頻,但是不可以直接上傳,就截幾個圖吧。
4)結論
其實程序的最重要的部分是設計思路而不是編碼,就這個程序也可以使用c、python等語言實現,都不是很那難。一旦程序的流程清晰了,編碼的過程自然也會浮現出來啦。