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等语言实现,都不是很那难。一旦程序的流程清晰了,编码的过程自然也会浮现出来啦。
最后有需要源码的朋友们,可以留邮箱,也可以耐心等我整理好了放到一个公开的链接上,一起相互学习。