一、游戏分析
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,如果读者也制作了贪吃蛇,不防给个链接,让我们娱乐娱乐!