C#游戏编程:《控制台小游戏系列》之《六、贪吃蛇实例》

一、游戏分析

  1976年,Gremlin平台推出一款经典街机游戏Blockade,则为贪吃蛇的原型,这款简单的小游戏很受欢迎,给80后这代人带来不可磨灭的记忆。作为未来程序员的我们,玩自己设计出的贪吃蛇比玩现有的更加有趣,我们可以添加我们所想到的各种特征,即使游戏多么另类都不觉得奇怪,我的游戏我做主!
  贪吃蛇的设计并不复杂,是很多游戏编程爱好者入门的首选项目之一,老衲也一样。整观这个游戏,屏蔽掉其他花俏的特征,让我们把焦点放到这个游戏的两个主要对象上:蛇和食物。玩过这款小游戏的人(难道有人没玩过的?)都知道,为了让自己的小蛇疯狂生长,就必须东西南北寻找食物来吃,前提是不要碰上障碍物,否则蛇就挂了。蛇由玩家通过键盘方向键来控制爬行方向,而食物在蛇视野范围内随机生成,当蛇吃掉食物之后,蛇长一节,食物再次随机生成,当然,食物不会落到蛇身上,否则蛇也只好自断了结了。
  通过上述,我们不难发现玩家的主要操作是控制蛇的爬行,对于蛇对象来说,它的特征就是会爬能爬,所以如何设计蛇的爬行并且如何反馈到画面上就是我们现在需要考虑的地方。蛇的爬行方法多种多样,但在这之前,我们还必须考虑蛇的表示方法,如何用数据结构表达一条蛇?
  ■或许可以想到用线性表来存储蛇的各个节点,当蛇移动时遍厉每个节点,更新为对应爬行方向之后的新值,这样每次蛇移动一步,则动全身:所有节点都要访问一次,可以看出最后的效率一定不会太高;
  ■或许可以想到用线性表来存储蛇的各个节点,当蛇移动时根据爬行方向把新的座标节点添加到线性表中,然后把相对的“蛇尾”节点删除,最后遍历每个节点,调整各个节点在线形表的位置,防止线形表前后出现空缺的问题。这个方法还是避免不了要动全身,效率也不高。
  ■或许可以想到用链表来存储蛇的各个节点,当蛇移动时只需要操作链头链尾指针,其他节点无须逐一访问,效率相对前面两种来说提升不少。
  然而在C#中,我们没必要考虑这么多,List<>就能很好的解决这个问题了,用List<>表达一条蛇,当蛇爬行的时候插入一个新节点,而删除相对尾端的尾节点,从而实现蛇的爬行效果;当蛇吃到食物时,只管往里添新节点即可,无须删除旧节点操作,这样蛇的身体就会生长一节。对于如何渲染蛇到画面上也是同样的思路(或许这是因为我们的蛇每个节点都是相同样式的情况),我们没有必要遍历每个节点然后把它们逐个渲染到画面上,而是采取“画头擦尾”的方式,只画改变的节点,不变的节点不需要考虑,从而使游戏性能大大提升,即使蛇的节点几千几万个,最终需要考虑的只有前后两个节点,蛇爬行起来腰不累了,吃嘛嘛香!以下用图来表达这种思想:


二、游戏实现

  根据上面的分析,我们很清晰的知道我们这个游戏主要的对象是什么了,万事开头难,分析清楚以后我们就很容易实现我们所需要的东西了,首先来看蛇类的实现。
  ///Snake类实现
  1. using System;  
  2. using System.Collections.Generic;  
  3. using CEngine;  
  4. using CGraphics;  
  5.   
  6. namespace Snake  
  7. {  
  8.     /// <summary>  
  9.     /// 贪吃蛇类  
  10.     /// </summary>  
  11.     internal class Snake  
  12.     {  
  13.         #region 字段  
  14.   
  15.         /// <summary>  
  16.         /// 蛇身  
  17.         /// </summary>  
  18.         private List<CPoint> m_body;  
  19.         /// <summary>  
  20.         /// 爬行方向  
  21.         /// </summary>  
  22.         private CDirection m_dir;  
  23.         /// <summary>  
  24.         /// 蛇头  
  25.         /// </summary>  
  26.         private CPoint m_head;  
  27.         /// <summary>  
  28.         /// 蛇尾  
  29.         /// </summary>  
  30.         private CPoint m_tail;  
  31.  
  32.         #endregion  
  33.  
  34.         #region 构造函数  
  35.   
  36.         /// <summary>  
  37.         /// 构造函数  
  38.         /// </summary>  
  39.         /// <param name="len"></param>  
  40.         /// <param name="dir"></param>  
  41.         public Snake(Int32 len, CDirection dir)  
  42.         {  
  43.             this.m_dir = dir;  
  44.   
  45.             this.m_body = new List<CPoint>();  
  46.   
  47.             for (Int32 i = 0; i <= len; i++)  
  48.             {  
  49.                 m_body.Add(new CPoint(i+1, 2));  
  50.             }  
  51.   
  52.             if (m_body.Count > 0)  
  53.             {  
  54.                 m_head = m_body[m_body.Count - 1];  
  55.                 m_tail = m_body[0];  
  56.             }  
  57.         }  
  58.  
  59.         #endregion  
  60.  
  61.         #region 方法  
  62.   
  63.         /// <summary>  
  64.         /// 设置方向  
  65.         /// </summary>  
  66.         /// <param name="dir"></param>  
  67.         public void setDirection(CDirection dir)  
  68.         {  
  69.             this.m_dir = dir;  
  70.         }  
  71.   
  72.         /// <summary>  
  73.         /// 获取方向  
  74.         /// </summary>  
  75.         /// <returns></returns>  
  76.         public CDirection getDirection()  
  77.         {  
  78.             return this.m_dir;  
  79.         }  
  80.   
  81.         /// <summary>  
  82.         /// 获取蛇头  
  83.         /// </summary>  
  84.         /// <returns></returns>  
  85.         public CPoint getHead()  
  86.         {  
  87.             return m_body[m_body.Count - 1];  
  88.         }  
  89.   
  90.         /// <summary>  
  91.         /// 获取蛇尾  
  92.         /// </summary>  
  93.         /// <returns></returns>  
  94.         private CPoint getTail()  
  95.         {  
  96.             return m_body[0];  
  97.         }  
  98.   
  99.         /// <summary>  
  100.         /// 添加蛇body节点  
  101.         /// </summary>  
  102.         /// <param name="point">新节点</param>  
  103.         /// <param name="food">是否是食物节点</param>  
  104.         public void addBodyNode(CPoint point, bool food)  
  105.         {  
  106.             //添加新节点  
  107.             this.m_body.Add(point);  
  108.             //非食物节点则移除尾巴  
  109.             if (!food)  
  110.             {  
  111.                 if (this.m_body.Count > 0)  
  112.                 {  
  113.                     this.m_body.Remove(getTail());  
  114.                 }  
  115.             }  
  116.         }  
  117.   
  118.         /// <summary>  
  119.         /// 是否在某位置发生碰撞  
  120.         /// </summary>  
  121.         /// <param name="point"></param>  
  122.         /// <returns></returns>  
  123.         public Boolean isCollision(CPoint point)  
  124.         {  
  125.             Boolean flag = false;  
  126.             foreach (CPoint p in m_body)  
  127.             {  
  128.                 flag = false;  
  129.                 if (p == point)  
  130.                 {  
  131.                     flag = true;  
  132.                     break;  
  133.                 }  
  134.             }  
  135.             return flag;  
  136.         }  
  137.   
  138.         /// <summary>  
  139.         /// 是否与自身发生碰撞  
  140.         /// </summary>  
  141.         /// <returns></returns>  
  142.         public Boolean isSeftCollision()  
  143.         {  
  144.             Boolean flag = false;  
  145.             for (Int32 i = 0; i <m_body.Count-1; i++)  
  146.             {  
  147.                 flag = false;  
  148.                 if (m_body[i] == getHead())  
  149.                 {  
  150.                     flag = true;  
  151.                     break;  
  152.                 }  
  153.             }  
  154.             return flag;  
  155.         }  
  156.   
  157.         /// <summary>  
  158.         /// 蛇移动  
  159.         /// </summary>  
  160.         /// <returns></returns>  
  161.         public bool move()  
  162.         {  
  163.             CPoint head = getHead();  
  164.             switch (m_dir)  
  165.             {  
  166.                 case CDirection.Left:  
  167.                     if (head.getX() == 1)  
  168.                         return false;  
  169.                     head.setX(head.getX() - 1);  
  170.                     break;  
  171.                 case CDirection.Right:  
  172.                     if (head.getX() == 28)  
  173.                         return false;  
  174.                     head.setX(head.getX() + 1);  
  175.                     break;  
  176.                 case CDirection.Up:  
  177.                     if (head.getY() == 1)  
  178.                         return false;  
  179.                     head.setY(head.getY() - 1);  
  180.                     break;  
  181.                 case CDirection.Down:  
  182.                     if (head.getY() == 23)  
  183.                         return false;  
  184.                     head.setY(head.getY() + 1);  
  185.                     break;  
  186.                 default:  
  187.                     break;  
  188.             }  
  189.             addBodyNode(head, false);  
  190.             return true;  
  191.         }  
  192.   
  193.         /// <summary>  
  194.         /// 绘制蛇  
  195.         /// </summary>  
  196.         /// <param name="draw"></param>  
  197.         public void draw(CDraw draw)  
  198.         {  
  199.             draw.setDrawSymbol(CSymbol.RING_SOLID);  
  200.             draw.fillRect(getHead().getX(), getHead().getY(), 1, 1, ConsoleColor.Yellow);  
  201.             draw.setDrawSymbol(CSymbol.DEFAULT);  
  202.             draw.fillRect(getTail().getX(), getTail().getY(), 1, 1, ConsoleColor.Black);  
  203.         }  
  204.  
  205.         #endregion  
  206.     }  
  207. }  
  蛇方向是一个枚举值,具体实现为下:
  ///CDirection枚举实现
  1. using System;  
  2.   
  3. namespace CEngine  
  4. {  
  5.     [Flags]  
  6.     public enum CDirection  
  7.     {  
  8.         Left = 0x01,  
  9.         Right = 0x02,  
  10.         Up = 0x04,  
  11.         Down = 0x08,  
  12.         None = 0  
  13.     }  
  14. }  
  蛇类主要处理的是蛇的爬行、蛇是否吃到吃到食物、蛇是否自残和蛇的绘制等,此蛇类相对比较简单,就不必过多讨论了,接下来是食物类,鉴于此游戏DEMO是测试这个游戏框架的特征,所以以简为主,就不必讨论不同食物间的特征等问题,这只是一个游戏DEMO,不是一个完整的游戏,知道这一点是必须的。
  ///Food类实现
  1. using System;  
  2. using CEngine;  
  3. using CGraphics;  
  4.   
  5. namespace Snake  
  6. {  
  7.     /// <summary>  
  8.     /// 食物类  
  9.     /// </summary>  
  10.     internal class Food  
  11.     {  
  12.         /// <summary>  
  13.         /// 位置  
  14.         /// </summary>  
  15.         private CPoint m_position;  
  16.   
  17.         /// <summary>  
  18.         /// 构造函数  
  19.         /// </summary>  
  20.         public Food()  
  21.         {  
  22.              
  23.         }  
  24.   
  25.         public Food(CPoint point)  
  26.         {  
  27.             this.m_position = point;  
  28.         }  
  29.   
  30.         /// <summary>  
  31.         /// 获取位置  
  32.         /// </summary>  
  33.         /// <returns></returns>  
  34.         public CPoint getPosition()  
  35.         {  
  36.             return this.m_position;  
  37.         }  
  38.   
  39.         /// <summary>  
  40.         /// 设置位置  
  41.         /// </summary>  
  42.         /// <param name="point"></param>  
  43.         public void setPosition(CPoint point)  
  44.         {  
  45.             this.m_position = point;  
  46.         }  
  47.   
  48.         /// <summary>  
  49.         /// 设置位置  
  50.         /// </summary>  
  51.         /// <param name="x"></param>  
  52.         /// <param name="y"></param>  
  53.         public void setPosition(Int32 x, Int32 y)  
  54.         {  
  55.             this.m_position = new CPoint(x,y);  
  56.         }  
  57.   
  58.         /// <summary>  
  59.         /// 绘制食物  
  60.         /// </summary>  
  61.         /// <param name="draw"></param>  
  62.         public void draw(CDraw draw)  
  63.         {  
  64.             draw.setDrawSymbol(CSymbol.RING_SOLID);  
  65.             draw.drawRect(m_position.getX(), m_position.getY(), 1,1,ConsoleColor.Green);  
  66.         }  
  67.     }  
  68. }  
  接下来是处理游戏逻辑,也就是实现这个游戏的控制类,这个类继承我们的游戏框架类,拥有框架提供的各种功能,从而对游戏实行控制和渲染,实现如下:
  ///SnakeGame类实现:
  1. using System;  
  2. using CEngine;  
  3. using CGraphics;  
  4.   
  5. namespace Snake  
  6. {  
  7.     /// <summary>  
  8.     /// 贪吃蛇游戏类  
  9.     /// </summary>  
  10.     public sealed class SnakeGame : CGame  
  11.     {  
  12.         /// <summary>  
  13.         /// 游戏状态  
  14.         /// </summary>  
  15.         public enum GameState  
  16.         {  
  17.             /// <summary>  
  18.             /// 初始化  
  19.             /// </summary>  
  20.             Init,  
  21.             /// <summary>  
  22.             /// 开始游戏  
  23.             /// </summary>  
  24.             Start,  
  25.             /// <summary>  
  26.             /// 暂停游戏  
  27.             /// </summary>  
  28.             Pause,  
  29.             /// <summary>  
  30.             /// 结束游戏  
  31.             /// </summary>  
  32.             End  
  33.         }  
  34.   
  35.         /// <summary>  
  36.         /// 贪吃蛇  
  37.         /// </summary>  
  38.         private Snake g_snake;  
  39.         /// <summary>  
  40.         /// 食物  
  41.         /// </summary>  
  42.         private Food g_food;  
  43.         /// <summary>  
  44.         /// 随机数  
  45.         /// </summary>  
  46.         private Random g_random;  
  47.         /// <summary>  
  48.         /// 分数  
  49.         /// </summary>  
  50.         private Int32 g_score;  
  51.         /// <summary>  
  52.         /// 生命  
  53.         /// </summary>  
  54.         private Int32 g_lifes;  
  55.         /// <summary>  
  56.         /// 状态  
  57.         /// </summary>  
  58.         private GameState g_state;  
  59.  
  60.  
  61.         #region 游戏运行函数  
  62.   
  63.         /// <summary>  
  64.         /// 游戏初始化  
  65.         /// </summary>  
  66.         protected override void gameInit()  
  67.         {  
  68.             base.setTitle("控制台游戏之——简易贪吃蛇v1.0");  
  69.             base.setCursorVisible(false);  
  70.             base.setUpdateRate(50);  
  71.   
  72.             this.g_random = new Random();  
  73.             this.g_snake = new Snake(3, CDirection.Right);  
  74.             this.g_food = new Food();  
  75.   
  76.             this.g_lifes = 3;  
  77.             this.g_state = GameState.Init;  
  78.   
  79.             this.drawInitUI();  
  80.         }  
  81.   
  82.         /// <summary>  
  83.         /// 游戏重绘时响应  
  84.         /// </summary>  
  85.         /// <param name="e"></param>  
  86.         protected override void onRedraw(CPaintEventArgs e)  
  87.         {  
  88.             base.onRedraw(e);  
  89.   
  90.             CDraw draw = e.getDraw();  
  91.             //绘制食物  
  92.             g_food.draw(draw);  
  93.             //绘制数据  
  94.             draw.drawText("得分:" + g_score.ToString(), 63, 2, ConsoleColor.Green);  
  95.             draw.drawText("生命:" + g_lifes.ToString(), 63, 4, ConsoleColor.Red);  
  96.         }  
  97.   
  98.         /// <summary>  
  99.         /// 游戏渲染  
  100.         /// </summary>  
  101.         /// <param name="draw"></param>  
  102.         protected override void gameDraw(CGraphics.CDraw draw)  
  103.         {  
  104.             if (g_state == GameState.Start)  
  105.             {  
  106.                 g_snake.draw(draw);  
  107.                 draw.drawText("FPS:" + getFPS(), 63, 6, ConsoleColor.Blue);  
  108.             }  
  109.         }  
  110.   
  111.         /// <summary>  
  112.         /// 游戏逻辑  
  113.         /// </summary>  
  114.         protected override void gameLoop()  
  115.         {  
  116.             //游戏开始状态  
  117.             if (g_state == GameState.Start)  
  118.             {  
  119.                 //如果蛇能爬行或者没有自残则爬行  
  120.                 if (g_snake.move() && !g_snake.isSeftCollision())  
  121.                 {  
  122.                     //吃到食物  
  123.                     if (g_snake.getHead() == g_food.getPosition())  
  124.                     {  
  125.                         //加10分  
  126.                         this.g_score += 10;  
  127.                         //蛇长大  
  128.                         g_snake.addBodyNode(g_food.getPosition(), true);  
  129.                         //创建新食物  
  130.                         createFood();  
  131.                     }  
  132.                 }  
  133.                 else  
  134.                 {  
  135.                     //蛇死亡,减一条生命  
  136.                     this.g_lifes--;  
  137.   
  138.                     //扣分算法  
  139.                     if (this.g_score > 20)  
  140.                     {  
  141.                         this.g_score -= 20;  
  142.                     }  
  143.   
  144.                     //延时一秒钟  
  145.                     base.delay(1000);  
  146.   
  147.                     //蛇回到原始状态  
  148.                     this.g_snake = new Snake(3, CDirection.Right);  
  149.                     //更新导致重绘区域  
  150.                     base.update(new CRect(1, 1, 28, 23));  
  151.   
  152.                     //游戏结束  
  153.                     if (this.g_lifes == 0)  
  154.                     {  
  155.                         this.g_state = GameState.End;  
  156.   
  157.                         this.setGameOver(true);  
  158.                     }  
  159.                 }  
  160.             }  
  161.         }  
  162.   
  163.         /// <summary>  
  164.         /// 游戏结束  
  165.         /// </summary>  
  166.         protected override void gameExit()  
  167.         {  
  168.             drawEndUI();  
  169.   
  170.             g_snake = null;  
  171.             g_food = null;  
  172.         }  
  173.   
  174.         /// <summary>  
  175.         /// 键盘事件  
  176.         /// </summary>  
  177.         /// <param name="e"></param>  
  178.         protected override void gameKeyDown(CKeyboardEventArgs e)  
  179.         {  
  180.             if (g_snake != null)  
  181.             {  
  182.                 if (e.getKey() == CKeys.Left)  
  183.                 {  
  184.                     if (g_snake.getDirection() != CDirection.Right)  
  185.                     {  
  186.                         g_snake.setDirection(CDirection.Left);  
  187.                     }  
  188.                 }  
  189.                 else if (e.getKey() == CKeys.Right)  
  190.                 {  
  191.                     if (g_snake.getDirection() != CDirection.Left)  
  192.                     {  
  193.                         g_snake.setDirection(CDirection.Right);  
  194.                     }  
  195.                 }  
  196.                 else if (e.getKey() == CKeys.Up)  
  197.                 {  
  198.                     if (g_snake.getDirection() != CDirection.Down)  
  199.                     {  
  200.                         g_snake.setDirection(CDirection.Up);  
  201.                     }  
  202.                 }  
  203.                 else if (e.getKey() == CKeys.Down)  
  204.                 {  
  205.                     if (g_snake.getDirection() != CDirection.Up)  
  206.                     {  
  207.                         g_snake.setDirection(CDirection.Down);  
  208.                     }  
  209.                 }  
  210.                 else if (e.getKey() == CKeys.Space)  
  211.                 {  
  212.                     if (g_state == GameState.Init)  
  213.                     {  
  214.                         g_state = GameState.Start;  
  215.   
  216.                         drawStartUI();  
  217.                     }  
  218.                     else if (g_state == GameState.Pause)  
  219.                     {  
  220.                         g_state = GameState.Start;  
  221.                     }  
  222.                     else if (g_state == GameState.Start)  
  223.                     {  
  224.                         g_state = GameState.Pause;  
  225.                     }  
  226.                 }  
  227.                 else if (e.getKey() == CKeys.Escape)  
  228.                 {  
  229.                     setGameOver(true);  
  230.                 }  
  231.             }  
  232.         }  
  233.  
  234.         #endregion  
  235.   
  236.         /// <summary>  
  237.         /// 创建食物  
  238.         /// </summary>  
  239.         private void createFood()  
  240.         {  
  241.             CPoint point = new CPoint(g_random.Next(1, 29), g_random.Next(1, 24));  
  242.             //防止食物出现在蛇身  
  243.             while (g_snake.isCollision(point))  
  244.             {  
  245.                 point.setX(g_random.Next(1, 29));  
  246.                 point.setY(g_random.Next(1, 24));  
  247.             }  
  248.   
  249.             g_food.setPosition(point);  
  250.             //调用更新函数导致控制台重绘  
  251.             base.update(new CRect(point.getX(), point.getY(), 1, 1));  
  252.         }  
  253.  
  254.         #region 绘制界面  
  255.           
  256.         //绘制游戏初始界面  
  257.         private void drawInitUI()  
  258.         {  
  259.             CDraw draw = base.getDraw();  
  260.             draw.clear(ConsoleColor.Black);  
  261.             draw.setDrawSymbol(CSymbol.RECT_EMPTY);  
  262.             draw.drawRect(6, 4, 29, 12, ConsoleColor.DarkBlue);  
  263.             draw.fillRect(7, 5, 27, 10, ConsoleColor.Blue);  
  264.             draw.drawText("<<C#控制台游戏系列>>", 16, 5, ConsoleColor.White);  
  265.             draw.drawText("简 易 贪 吃 蛇", 34, 9, ConsoleColor.Yellow);  
  266.             draw.drawText("Copyright. D-Zone Studio", 42, 14, ConsoleColor.Gray);  
  267.             draw.drawText("Space to Play", 35, 20, ConsoleColor.White);  
  268.         }  
  269.           
  270.         //绘制游戏开始界面  
  271.         private void drawStartUI()  
  272.         {  
  273.             CDraw draw = base.getDraw();  
  274.             //绘制界面  
  275.             draw.clear(ConsoleColor.Black);  
  276.             draw.setDrawSymbol(CSymbol.RECT_EMPTY);  
  277.             draw.drawRect(0, 0, 30, 25, ConsoleColor.White);  
  278.             draw.setDrawSymbol(CSymbol.RHOMB_SOLID);  
  279.             draw.drawRect(30, 0, 10, 25, ConsoleColor.DarkYellow);  
  280.             draw.drawText("操作:键盘操作,方向键控制贪吃蛇爬行方向,空格键控制开始游戏和暂停游戏,ESC键退出游戏。"new CRect(32, 14, 6, 10), ConsoleColor.DarkGreen);  
  281.             this.createFood();  
  282.         }  
  283.   
  284.         //绘制游戏结束界面  
  285.         private void drawEndUI()  
  286.         {  
  287.             CDraw draw = base.getDraw();  
  288.             //绘制界面  
  289.             draw.clear(ConsoleColor.Black);  
  290.             draw.drawText("Game over, ", 30, 10, ConsoleColor.White);  
  291.             draw.drawText("score:" + g_score.ToString(), 42, 10, ConsoleColor.White);  
  292.             Console.ReadLine();  
  293.         }  
  294.  
  295.         #endregion  
  296.     }  
  297. }  
  与以往编写小游戏的经验比较,我们发现没有必要每次都要编写代码驱动和维护游戏的运行,在这里,游戏框架类已为我们封装这些特征,我们制作游戏时只需要考虑游戏如何玩法等设计上的问题,而不需要考虑其他细节问题,一定程度上提高我们的开发效率。尽管这个小游戏是一个DEMO版本,但它也具备一定的完整性,比如拥有了游戏必须的几个界面,然而它还不是完整的,没有关卡的设计,没有信息的保存,也没有参数的配置,但对于说明游戏框架如何使用已经足够了。
  接下来让我们欣赏一下我们的劳动成果:



三、结语

  终于完成了这个DEMO,如果读者也制作了贪吃蛇,不防给个链接,让我们娱乐娱乐!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章