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,如果讀者也製作了貪喫蛇,不防給個鏈接,讓我們娛樂娛樂!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章