一、遊戲分析
1976年,Gremlin平臺推出一款經典街機遊戲Blockade,則爲貪喫蛇的原型,這款簡單的小遊戲很受歡迎,給80後這代人帶來不可磨滅的記憶。作爲未來程序員的我們,玩自己設計出的貪喫蛇比玩現有的更加有趣,我們可以添加我們所想到的各種特徵,即使遊戲多麼另類都不覺得奇怪,我的遊戲我做主!
貪喫蛇的設計並不複雜,是很多遊戲編程愛好者入門的首選項目之一,老衲也一樣。整觀這個遊戲,屏蔽掉其他花俏的特徵,讓我們把焦點放到這個遊戲的兩個主要對象上:蛇和食物。玩過這款小遊戲的人(難道有人沒玩過的?)都知道,爲了讓自己的小蛇瘋狂生長,就必須東西南北尋找食物來喫,前提是不要碰上障礙物,否則蛇就掛了。蛇由玩家通過鍵盤方向鍵來控制爬行方向,而食物在蛇視野範圍內隨機生成,當蛇喫掉食物之後,蛇長一節,食物再次隨機生成,當然,食物不會落到蛇身上,否則蛇也只好自斷了結了。
通過上述,我們不難發現玩家的主要操作是控制蛇的爬行,對於蛇對象來說,它的特徵就是會爬能爬,所以如何設計蛇的爬行並且如何反饋到畫面上就是我們現在需要考慮的地方。蛇的爬行方法多種多樣,但在這之前,我們還必須考慮蛇的表示方法,如何用數據結構表達一條蛇?
■或許可以想到用線性表來存儲蛇的各個節點,當蛇移動時遍厲每個節點,更新爲對應爬行方向之後的新值,這樣每次蛇移動一步,則動全身:所有節點都要訪問一次,可以看出最後的效率一定不會太高;
■或許可以想到用線性表來存儲蛇的各個節點,當蛇移動時根據爬行方向把新的座標節點添加到線性表中,然後把相對的“蛇尾”節點刪除,最後遍歷每個節點,調整各個節點在線形表的位置,防止線形表前後出現空缺的問題。這個方法還是避免不了要動全身,效率也不高。
■或許可以想到用鏈表來存儲蛇的各個節點,當蛇移動時只需要操作鏈頭鏈尾指針,其他節點無須逐一訪問,效率相對前面兩種來說提升不少。
然而在C#中,我們沒必要考慮這麼多,List<>就能很好的解決這個問題了,用List<>表達一條蛇,當蛇爬行的時候插入一個新節點,而刪除相對尾端的尾節點,從而實現蛇的爬行效果;當蛇喫到食物時,只管往裏添新節點即可,無須刪除舊節點操作,這樣蛇的身體就會生長一節。對於如何渲染蛇到畫面上也是同樣的思路(或許這是因爲我們的蛇每個節點都是相同樣式的情況),我們沒有必要遍歷每個節點然後把它們逐個渲染到畫面上,而是採取“畫頭擦尾”的方式,只畫改變的節點,不變的節點不需要考慮,從而使遊戲性能大大提升,即使蛇的節點幾千幾萬個,最終需要考慮的只有前後兩個節點,蛇爬行起來腰不累了,喫嘛嘛香!以下用圖來表達這種思想:
根據上面的分析,我們很清晰的知道我們這個遊戲主要的對象是什麼了,萬事開頭難,分析清楚以後我們就很容易實現我們所需要的東西了,首先來看蛇類的實現。
///Snake類實現
-
using System;
-
using System.Collections.Generic;
-
using CEngine;
-
using CGraphics;
-
-
namespace Snake
-
{
-
-
-
-
internal class Snake
-
{
-
#region 字段
-
-
-
-
-
private List<CPoint> m_body;
-
-
-
-
private CDirection m_dir;
-
-
-
-
private CPoint m_head;
-
-
-
-
private CPoint m_tail;
-
-
#endregion
-
-
#region 構造函數
-
-
-
-
-
-
-
public Snake(Int32 len, CDirection dir)
-
{
-
this.m_dir = dir;
-
-
this.m_body = new List<CPoint>();
-
-
for (Int32 i = 0; i <= len; i++)
-
{
-
m_body.Add(new CPoint(i+1, 2));
-
}
-
-
if (m_body.Count > 0)
-
{
-
m_head = m_body[m_body.Count - 1];
-
m_tail = m_body[0];
-
}
-
}
-
-
#endregion
-
-
#region 方法
-
-
-
-
-
-
public void setDirection(CDirection dir)
-
{
-
this.m_dir = dir;
-
}
-
-
-
-
-
-
public CDirection getDirection()
-
{
-
return this.m_dir;
-
}
-
-
-
-
-
-
public CPoint getHead()
-
{
-
return m_body[m_body.Count - 1];
-
}
-
-
-
-
-
-
private CPoint getTail()
-
{
-
return m_body[0];
-
}
-
-
-
-
-
-
-
public void addBodyNode(CPoint point, bool food)
-
{
-
-
this.m_body.Add(point);
-
-
if (!food)
-
{
-
if (this.m_body.Count > 0)
-
{
-
this.m_body.Remove(getTail());
-
}
-
}
-
}
-
-
-
-
-
-
-
public Boolean isCollision(CPoint point)
-
{
-
Boolean flag = false;
-
foreach (CPoint p in m_body)
-
{
-
flag = false;
-
if (p == point)
-
{
-
flag = true;
-
break;
-
}
-
}
-
return flag;
-
}
-
-
-
-
-
-
public Boolean isSeftCollision()
-
{
-
Boolean flag = false;
-
for (Int32 i = 0; i <m_body.Count-1; i++)
-
{
-
flag = false;
-
if (m_body[i] == getHead())
-
{
-
flag = true;
-
break;
-
}
-
}
-
return flag;
-
}
-
-
-
-
-
-
public bool move()
-
{
-
CPoint head = getHead();
-
switch (m_dir)
-
{
-
case CDirection.Left:
-
if (head.getX() == 1)
-
return false;
-
head.setX(head.getX() - 1);
-
break;
-
case CDirection.Right:
-
if (head.getX() == 28)
-
return false;
-
head.setX(head.getX() + 1);
-
break;
-
case CDirection.Up:
-
if (head.getY() == 1)
-
return false;
-
head.setY(head.getY() - 1);
-
break;
-
case CDirection.Down:
-
if (head.getY() == 23)
-
return false;
-
head.setY(head.getY() + 1);
-
break;
-
default:
-
break;
-
}
-
addBodyNode(head, false);
-
return true;
-
}
-
-
-
-
-
-
public void draw(CDraw draw)
-
{
-
draw.setDrawSymbol(CSymbol.RING_SOLID);
-
draw.fillRect(getHead().getX(), getHead().getY(), 1, 1, ConsoleColor.Yellow);
-
draw.setDrawSymbol(CSymbol.DEFAULT);
-
draw.fillRect(getTail().getX(), getTail().getY(), 1, 1, ConsoleColor.Black);
-
}
-
-
#endregion
-
}
-
}
蛇方向是一個枚舉值,具體實現爲下:
///CDirection枚舉實現
-
using System;
-
-
namespace CEngine
-
{
-
[Flags]
-
public enum CDirection
-
{
-
Left = 0x01,
-
Right = 0x02,
-
Up = 0x04,
-
Down = 0x08,
-
None = 0
-
}
-
}
蛇類主要處理的是蛇的爬行、蛇是否喫到喫到食物、蛇是否自殘和蛇的繪製等,此蛇類相對比較簡單,就不必過多討論了,接下來是食物類,鑑於此遊戲DEMO是測試這個遊戲框架的特徵,所以以簡爲主,就不必討論不同食物間的特徵等問題,這只是一個遊戲DEMO,不是一個完整的遊戲,知道這一點是必須的。
///Food類實現
-
using System;
-
using CEngine;
-
using CGraphics;
-
-
namespace Snake
-
{
-
-
-
-
internal class Food
-
{
-
-
-
-
private CPoint m_position;
-
-
-
-
-
public Food()
-
{
-
-
}
-
-
public Food(CPoint point)
-
{
-
this.m_position = point;
-
}
-
-
-
-
-
-
public CPoint getPosition()
-
{
-
return this.m_position;
-
}
-
-
-
-
-
-
public void setPosition(CPoint point)
-
{
-
this.m_position = point;
-
}
-
-
-
-
-
-
-
public void setPosition(Int32 x, Int32 y)
-
{
-
this.m_position = new CPoint(x,y);
-
}
-
-
-
-
-
-
public void draw(CDraw draw)
-
{
-
draw.setDrawSymbol(CSymbol.RING_SOLID);
-
draw.drawRect(m_position.getX(), m_position.getY(), 1,1,ConsoleColor.Green);
-
}
-
}
-
}
接下來是處理遊戲邏輯,也就是實現這個遊戲的控制類,這個類繼承我們的遊戲框架類,擁有框架提供的各種功能,從而對遊戲實行控制和渲染,實現如下:
///SnakeGame類實現:
-
using System;
-
using CEngine;
-
using CGraphics;
-
-
namespace Snake
-
{
-
-
-
-
public sealed class SnakeGame : CGame
-
{
-
-
-
-
public enum GameState
-
{
-
-
-
-
Init,
-
-
-
-
Start,
-
-
-
-
Pause,
-
-
-
-
End
-
}
-
-
-
-
-
private Snake g_snake;
-
-
-
-
private Food g_food;
-
-
-
-
private Random g_random;
-
-
-
-
private Int32 g_score;
-
-
-
-
private Int32 g_lifes;
-
-
-
-
private GameState g_state;
-
-
-
#region 遊戲運行函數
-
-
-
-
-
protected override void gameInit()
-
{
-
base.setTitle("控制檯遊戲之——簡易貪喫蛇v1.0");
-
base.setCursorVisible(false);
-
base.setUpdateRate(50);
-
-
this.g_random = new Random();
-
this.g_snake = new Snake(3, CDirection.Right);
-
this.g_food = new Food();
-
-
this.g_lifes = 3;
-
this.g_state = GameState.Init;
-
-
this.drawInitUI();
-
}
-
-
-
-
-
-
protected override void onRedraw(CPaintEventArgs e)
-
{
-
base.onRedraw(e);
-
-
CDraw draw = e.getDraw();
-
-
g_food.draw(draw);
-
-
draw.drawText("得分:" + g_score.ToString(), 63, 2, ConsoleColor.Green);
-
draw.drawText("生命:" + g_lifes.ToString(), 63, 4, ConsoleColor.Red);
-
}
-
-
-
-
-
-
protected override void gameDraw(CGraphics.CDraw draw)
-
{
-
if (g_state == GameState.Start)
-
{
-
g_snake.draw(draw);
-
draw.drawText("FPS:" + getFPS(), 63, 6, ConsoleColor.Blue);
-
}
-
}
-
-
-
-
-
protected override void gameLoop()
-
{
-
-
if (g_state == GameState.Start)
-
{
-
-
if (g_snake.move() && !g_snake.isSeftCollision())
-
{
-
-
if (g_snake.getHead() == g_food.getPosition())
-
{
-
-
this.g_score += 10;
-
-
g_snake.addBodyNode(g_food.getPosition(), true);
-
-
createFood();
-
}
-
}
-
else
-
{
-
-
this.g_lifes--;
-
-
-
if (this.g_score > 20)
-
{
-
this.g_score -= 20;
-
}
-
-
-
base.delay(1000);
-
-
-
this.g_snake = new Snake(3, CDirection.Right);
-
-
base.update(new CRect(1, 1, 28, 23));
-
-
-
if (this.g_lifes == 0)
-
{
-
this.g_state = GameState.End;
-
-
this.setGameOver(true);
-
}
-
}
-
}
-
}
-
-
-
-
-
protected override void gameExit()
-
{
-
drawEndUI();
-
-
g_snake = null;
-
g_food = null;
-
}
-
-
-
-
-
-
protected override void gameKeyDown(CKeyboardEventArgs e)
-
{
-
if (g_snake != null)
-
{
-
if (e.getKey() == CKeys.Left)
-
{
-
if (g_snake.getDirection() != CDirection.Right)
-
{
-
g_snake.setDirection(CDirection.Left);
-
}
-
}
-
else if (e.getKey() == CKeys.Right)
-
{
-
if (g_snake.getDirection() != CDirection.Left)
-
{
-
g_snake.setDirection(CDirection.Right);
-
}
-
}
-
else if (e.getKey() == CKeys.Up)
-
{
-
if (g_snake.getDirection() != CDirection.Down)
-
{
-
g_snake.setDirection(CDirection.Up);
-
}
-
}
-
else if (e.getKey() == CKeys.Down)
-
{
-
if (g_snake.getDirection() != CDirection.Up)
-
{
-
g_snake.setDirection(CDirection.Down);
-
}
-
}
-
else if (e.getKey() == CKeys.Space)
-
{
-
if (g_state == GameState.Init)
-
{
-
g_state = GameState.Start;
-
-
drawStartUI();
-
}
-
else if (g_state == GameState.Pause)
-
{
-
g_state = GameState.Start;
-
}
-
else if (g_state == GameState.Start)
-
{
-
g_state = GameState.Pause;
-
}
-
}
-
else if (e.getKey() == CKeys.Escape)
-
{
-
setGameOver(true);
-
}
-
}
-
}
-
-
#endregion
-
-
-
-
-
private void createFood()
-
{
-
CPoint point = new CPoint(g_random.Next(1, 29), g_random.Next(1, 24));
-
-
while (g_snake.isCollision(point))
-
{
-
point.setX(g_random.Next(1, 29));
-
point.setY(g_random.Next(1, 24));
-
}
-
-
g_food.setPosition(point);
-
-
base.update(new CRect(point.getX(), point.getY(), 1, 1));
-
}
-
-
#region 繪製界面
-
-
-
private void drawInitUI()
-
{
-
CDraw draw = base.getDraw();
-
draw.clear(ConsoleColor.Black);
-
draw.setDrawSymbol(CSymbol.RECT_EMPTY);
-
draw.drawRect(6, 4, 29, 12, ConsoleColor.DarkBlue);
-
draw.fillRect(7, 5, 27, 10, ConsoleColor.Blue);
-
draw.drawText("<<C#控制檯遊戲系列>>", 16, 5, ConsoleColor.White);
-
draw.drawText("簡 易 貪 喫 蛇", 34, 9, ConsoleColor.Yellow);
-
draw.drawText("Copyright. D-Zone Studio", 42, 14, ConsoleColor.Gray);
-
draw.drawText("Space to Play", 35, 20, ConsoleColor.White);
-
}
-
-
-
private void drawStartUI()
-
{
-
CDraw draw = base.getDraw();
-
-
draw.clear(ConsoleColor.Black);
-
draw.setDrawSymbol(CSymbol.RECT_EMPTY);
-
draw.drawRect(0, 0, 30, 25, ConsoleColor.White);
-
draw.setDrawSymbol(CSymbol.RHOMB_SOLID);
-
draw.drawRect(30, 0, 10, 25, ConsoleColor.DarkYellow);
-
draw.drawText("操作:鍵盤操作,方向鍵控制貪喫蛇爬行方向,空格鍵控制開始遊戲和暫停遊戲,ESC鍵退出遊戲。", new CRect(32, 14, 6, 10), ConsoleColor.DarkGreen);
-
this.createFood();
-
}
-
-
-
private void drawEndUI()
-
{
-
CDraw draw = base.getDraw();
-
-
draw.clear(ConsoleColor.Black);
-
draw.drawText("Game over, ", 30, 10, ConsoleColor.White);
-
draw.drawText("score:" + g_score.ToString(), 42, 10, ConsoleColor.White);
-
Console.ReadLine();
-
}
-
-
#endregion
-
}
-
}
與以往編寫小遊戲的經驗比較,我們發現沒有必要每次都要編寫代碼驅動和維護遊戲的運行,在這裏,遊戲框架類已爲我們封裝這些特徵,我們製作遊戲時只需要考慮遊戲如何玩法等設計上的問題,而不需要考慮其他細節問題,一定程度上提高我們的開發效率。儘管這個小遊戲是一個DEMO版本,但它也具備一定的完整性,比如擁有了遊戲必須的幾個界面,然而它還不是完整的,沒有關卡的設計,沒有信息的保存,也沒有參數的配置,但對於說明遊戲框架如何使用已經足夠了。
接下來讓我們欣賞一下我們的勞動成果:
終於完成了這個DEMO,如果讀者也製作了貪喫蛇,不防給個鏈接,讓我們娛樂娛樂!