贪吃蛇
本文是按照老师给的类图思路,自己完成的贪吃蛇项目,记录一下,可能会有冗余代码,期盼朋友们的指正批评。
一.需求分析(绘制类图)
二.代码实现
2.1 游戏类
初始化窗口,控制游戏场景的切换
注意此处的游戏帧更新接口,为了练习里氏替换原则,也使用它作为场景类的容器
public enum E_Scenario // 枚举场景
{
Start,//游戏开始
End,//游戏结束
Game,//游戏中
}
public class Game
{
public static int width = 100;//窗口宽度
public static int hight = 30;//窗口高度
public IUpdate update;//当前所在场景
public Game()//初始化控制台
{
Console.CursorVisible = false;
Console.SetWindowSize(width, hight);
Console.SetBufferSize(width, hight);
}
public void Context()//游戏主循环
{
this.update = new Start_Scenario();
while (true)
//主循环在于控制页面之间,而不是页面内容,在本循环中接收转页面指令进行页面跳转
{
if (this.update is Start_Scenario)//若是开始界面
{
Console.Clear();//再进入新页面前清空
this.update = this.Exchange((this.update as Start_Scenario).Update());
GC.Collect();//页面之间的切换比较少,每一次又重新分配空间,因此使用垃圾回收,提高内存利用率
}
else if(this.update is End_Scenario)//若是结束界面
{
Console.Clear();//同上
this.update = this.Exchange((this.update as End_Scenario).Update());
GC.Collect();
}
else if(this.update is Game_Scenario)//若是游戏界面
{
Console.Clear();
this.update = this.Exchange((this.update as Game_Scenario).Update());
GC.Collect();
}
else
{
Console.SetCursorPosition(75, 50);
Console.WriteLine("选定场景为空,错误!");
}
}
}
public IUpdate Exchange(E_Scenario scenario)//场景切换
{
switch (scenario)
{
case E_Scenario.Start:
return new Start_Scenario();
case E_Scenario.End:
return new End_Scenario();
case E_Scenario.Game:
return new Game_Scenario();
}
return new End_Scenario();
}
}
public interface IUpdate//游戏帧更新接口
{
E_Scenario Update();
}
2.2 开始与结束场景类
用W,S控制选项选择,用J来确定选项选择,获取用户选择后,返回对应信息(下一个场景是什么)给游戏类
//开始结束场景基类
public class Basic_Scenario : IUpdate//开始或结束场景类
{
protected string[] Title = { "贪吃蛇", "贪吃蛇" };
protected string[] Choose1 = { "开始游戏", "回到开始画面" };
protected string[] Choose2 = { "结束游戏", "结束游戏" };
public E_Scenario Update()//重写更新接口
{
return E_Scenario.Start;
}
}
//开始场景类
public class Start_Scenario : Basic_Scenario
{
public E_Scenario Update()
//开始场景的帧刷新
{
char K = 'W';//初始时默认为选择开始游戏
int keep = 1;//用于按下J时判断选择哪一个选项
while (true)
{
Console.Clear();
//默认绘制不变部分
Console.SetCursorPosition((Game.width - this.Title[(int)E_Scenario.Start].Length) / 2 + 1, Game.hight / 2);
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(this.Title[(int)E_Scenario.Start]);
switch (K)
//根据输入字符来绘制可变部分
//当按下w则使得开始游戏选项变红
//当按下s则使得结束游戏选项变红 -- 注意结束游戏不是进入结束界面
//当按下j则选择当前选项
{
case 'w':
case 'W':
Console.ForegroundColor = ConsoleColor.Red;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.Start].Length) / 2, Game.hight / 2 + 2);
Console.WriteLine(this.Choose1[(int)E_Scenario.Start]);
Console.ForegroundColor = ConsoleColor.White;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.Start].Length) / 2, Game.hight / 2 + 4);
Console.WriteLine(this.Choose2[(int)E_Scenario.Start]);
keep = 1;
break;
case 's':
case 'S':
Console.ForegroundColor = ConsoleColor.White;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.Start].Length) / 2, Game.hight / 2 + 2);
Console.WriteLine(this.Choose1[(int)E_Scenario.Start]);
Console.ForegroundColor = ConsoleColor.Red;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.Start].Length) / 2, Game.hight / 2 + 4);
Console.WriteLine(this.Choose2[(int)E_Scenario.Start]);
keep = 2;
break;
case 'j':
case 'J':
Console.Clear();
switch (keep)
{
case 1:
return E_Scenario.Game; //让主循环进入游戏界面
case 2:
Environment.Exit(0);
break;
;
}
break;
}
K = Console.ReadKey(true).KeyChar;//等待输入字符
}
}
}
//结束场景类
public class End_Scenario : Basic_Scenario
{
public E_Scenario Update()
//结束场景的帧刷新
{
char K = 'W';//初始时默认为选择重新开始游戏
int keep = 1;//用于按下J时判断选择哪一个选项
while (true)
{
Console.Clear();
//默认绘制不变部分
Console.SetCursorPosition((Game.width - this.Title[(int)E_Scenario.End].Length) / 2 + 1, Game.hight / 2);
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(this.Title[(int)E_Scenario.End]);
switch (K)
//根据输入字符来绘制可变部分
//当按下w则使得重新开始游戏选项变红
//当按下s则使得结束游戏选项变红 -- 注意结束游戏不是进入结束界面
//当按下j则选择当前选项
{
case 'w':
case 'W':
Console.ForegroundColor = ConsoleColor.Red;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.End].Length) / 2 - 1, Game.hight / 2 + 2);
Console.WriteLine(this.Choose1[(int)E_Scenario.End]);
Console.ForegroundColor = ConsoleColor.White;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.End].Length) / 2 + 1, Game.hight / 2 + 4);
Console.WriteLine(this.Choose2[(int)E_Scenario.End]);
keep = 1;
break;
case 's':
case 'S':
Console.ForegroundColor = ConsoleColor.White;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.End].Length) / 2 -1, Game.hight / 2 + 2);
Console.WriteLine(this.Choose1[(int)E_Scenario.End]);
Console.ForegroundColor = ConsoleColor.Red;
Console.SetCursorPosition((Game.width - this.Choose1[(int)E_Scenario.End].Length) / 2 + 1, Game.hight / 2 + 4);
Console.WriteLine(this.Choose2[(int)E_Scenario.End]);
keep = 2;
break;
case 'j':
case 'J':
Console.Clear();
switch (keep)
{
case 1:
return E_Scenario.Start; //让主循环进入开始界面
case 2:
Environment.Exit(0);//结束游戏
break;
;
}
break;
}
K = Console.ReadKey(true).KeyChar;//等待输入字符
}
}
}
2.3 游戏场景类
最主要的类,游戏内容相关的类与对象都是被在本类中使用
#region 游戏场景
//游戏场景类
public class Game_Scenario : IUpdate//游戏场景类
{
private static Timer time;
Game_Object snake = new Snake();//蛇对象
Maps map = new Maps();//地图对象 -- 因为地图不是继承Game_Object
Game_Object food = new Food();//食物对象
public E_Scenario Update()//重写更新接口
{
map.Draw();//墙壁是不变的,不用刷新
(food as Food).Draw();
while (true)
{
(snake as Snake).Move(food as Food);
if((snake as Snake).Die(map))
{
break;
}
(snake as Snake).Draw();
for (int i = 0; i < 888888888; i++) //可以编写计时器来控制准确时间,但是还没学过事件,不知道怎么写
{
}
}
return E_Scenario.End;
}
}
//地图类
public class Maps
{ //仅需要在此处增加成员就可以增加
public Game_Object wall_top = new Map(0,0,Game.width/2-1,true);
public Game_Object wall_left = new Map(0, 0, Game.hight, false);
public Game_Object wall_bottom = new Map(0, Game.hight - 1, Game.width / 2 - 1, true);
public Game_Object wall_right = new Map(Game.width - 2,0, Game.hight, false);
public void Draw()
{
(this.wall_top as Map).Draw();
(this.wall_left as Map).Draw();
(this.wall_bottom as Map).Draw();
(this.wall_right as Map).Draw();
}
}
//食物类 -- 需要告知外界坐标 ,需要在一段设定时间后或者被吃掉后更改位置
public class Food : Game_Object
{
private Random random = new Random();
private string pattern = "★";
private int x;
private int y;
public Food()
{
this.location = new Location[1];
}
public void Draw()
{
this.Clean();
this.location[0].x = random.Next(2, Game.width - 3);
if (this.location[0].x %2 != 0)//保证食物不刷新在奇数坐标上
{
this.location[0].x += 1;
}
this.location[0].y = random.Next(1, Game.hight - 1);
Console.ForegroundColor = ConsoleColor.Green;
Console.SetCursorPosition(this.location[0].x, this.location[0].y);
Console.Write(pattern);
x = this.location[0].x;
y = this.location[0].y;
}
public void Clean()
{
Console.SetCursorPosition(x, y);
Console.Write(" ");
}
}
public enum E_Direction
{
K_top,
K_left,
K_bottom,
K_right,
}
//蛇类
//蛇头负责转向逻辑,蛇身只需要不断赋值前一元素的位置就可以了,但是蛇头应该和蛇身不一样图标,并且蛇头可以进行重合判定
public class Snake : Game_Object
{
private Game_Object Snake_body = new SnakeBody(E_SnakePart.SnakeBody);//声明一个蛇身
private Game_Object Snake_head = new SnakeBody(E_SnakePart.SnakeHead);//声明一个蛇头
private int[] x_origion = [12, 10, 8,6];//初始的蛇头[0]+蛇身[1/2/3]x坐标
private int[] y_origion = [8, 8, 8,8];//初始的蛇头[0]+蛇身y[1/2/3]坐标
private E_Direction Key = E_Direction.K_right;//记录下一帧的蛇头运动方向
public Snake()
{
Snake_body.location = new Location[3];
Snake_head.location = new Location[1];
#region 用两个数组存储完整的蛇体位置信息,再按如下传值,可以保持位置的连贯性,也可以分别处理头身逻辑
Snake_head.location[0].x = x_origion[0];
Snake_head.location[0].y = y_origion[0];
for (int i = 0; i<Snake_body.location.Length; i++)
{
Snake_body.location[i].x = x_origion[i + 1];
Snake_body.location[i].y = y_origion[i + 1];
}
#endregion
}
public void Draw()//蛇绘制
{
(Snake_body as SnakeBody).Draw();
(Snake_head as SnakeBody).Draw();
}
public void Move(Food food)//蛇移动
{
//先记录下本帧的蛇对象的位置
//根据运动方向得到下一帧蛇头的位置
//后续跟着蛇头新位置而挪动
#region 1、暂存当前位置
x_origion[0] = Snake_head.location[0].x;
y_origion[0] = Snake_head.location[0].y;
for (int i = 0; i < Snake_body.location.Length; i++)
{
x_origion[i + 1] = Snake_body.location[i].x;
y_origion[i + 1] = Snake_body.location[i].y;
}
#endregion
#region 2、是否吃到食物?是否转向?
if (Eat(food))
{
(food as Food).Clean();
(food as Food).Draw();
Grow();
}
Turn();
#endregion
#region 3、清除之前的图像,并得到新的位置
(Snake_head as SnakeBody).Clean();
(Snake_body as SnakeBody).Clean();
switch (Key)//得到新的蛇头位置
{
case E_Direction.K_top:
Snake_head.location[0].x = x_origion[0];
Snake_head.location[0].y = y_origion[0] - 1;
break;
case E_Direction.K_bottom:
Snake_head.location[0].x = x_origion[0];
Snake_head.location[0].y = y_origion[0] + 1;
break;
case E_Direction.K_left:
Snake_head.location[0].x = x_origion[0] - 2;
Snake_head.location[0].y = y_origion[0];
break;
case E_Direction.K_right:
Snake_head.location[0].x = x_origion[0] + 2;
Snake_head.location[0].y = y_origion[0];
break;
}
for(int i = Snake_body.location.Length - 1; i >= 0; i--)
{
Snake_body.location[i].x = x_origion[i];
Snake_body.location[i].y = y_origion[i];
}
#endregion
}
public void Turn() //蛇转向
{
if (Console.KeyAvailable)
{
switch (Console.ReadKey(true).KeyChar)
{
case 'w':
case 'W':
Key = Key != E_Direction.K_bottom ? E_Direction.K_top : E_Direction.K_bottom;
break;
case 's':
case 'S':
Key = Key != E_Direction.K_top ? E_Direction.K_bottom : E_Direction.K_top;
break;
case 'd':
case 'D':
Key = Key != E_Direction.K_left ? E_Direction.K_right : E_Direction.K_left;
break;
case 'a':
case 'A':
Key = Key != E_Direction.K_right ? E_Direction.K_left : E_Direction.K_right;
break;
}
}
}
public void Grow() //蛇长大,在尾部增长,且尾部形状不变
{
int new_length = x_origion.Length + 1;
int[] newx = new int[new_length];
int[] newy = new int[new_length];
for (int i = 0; i < new_length-1; i++)
{
newx[i] = x_origion[i];
newy[i] = y_origion[i];
}
//保证新增尾部形状不变
newx[^1] = x_origion[^1] == x_origion[^2] ? x_origion[^1] : x_origion[^1] * 2 - x_origion[^2];
newy[^1] = y_origion[^1] == y_origion[^2] ? y_origion[^1] : y_origion[^1] * 2 - y_origion[^2];
x_origion = new int[new_length];
y_origion = new int[new_length];
for(int i = 0; i < new_length; i++)
{
x_origion[i] = newx[i];
y_origion[i] = newy[i];
}
Snake_body.location = new Location[new_length - 1];//蛇身也要增长
}
public bool Eat(Food food) //当蛇头和食物重合,蛇吃食物
{
if (food.location[0] == Snake_head.location[0])
{
return true;
}
else return false;
}
public bool Die(Maps map)//当蛇头与墙壁重合,蛇死亡
{
for(int i = 0; i < map.wall_left.location.Length; i++)
{
if(Snake_head.location[0] == map.wall_left.location[i] || Snake_head.location[0] == map.wall_right.location[i])
{
return true;
}
}
for (int i = 0; i < map.wall_top.location.Length; i++)
{
if (Snake_head.location[0] == map.wall_top.location[i] || Snake_head.location[0] == map.wall_bottom.location[i])
{
return true;
}
}
for(int i = 0; i < Snake_body.location.Length; i++)
{
if (Snake_head.location[0] == Snake_body.location[i])
{
return true;
}
}
return false;
}
}
public class SnakeBody : Game_Object//蛇身类
{
public E_SnakePart bodytype;
public SnakeBody(E_SnakePart A)
{
this.bodytype = A;
}
public void Draw()//分别绘制蛇身和蛇头
{
Console.ForegroundColor = ConsoleColor.Yellow;
switch (bodytype)
{
case E_SnakePart.SnakeBody:
for (int i = 0; i < this.location.Length; i++)
{
Console.SetCursorPosition(this.location[i].x, this.location[i].y);
Console.Write('●') ;
}
break;
case E_SnakePart.SnakeHead:
Console.SetCursorPosition(this.location[0].x, this.location[0].y);
Console.Write('◎');
break;
}
}
public void Clean()//蛇身清除
{
for (int i = 0; i < this.location.Length; i++)
{
Console.SetCursorPosition(this.location[i].x, this.location[i].y);
Console.Write(" ");
}
}
}
public enum E_SnakePart
{
SnakeHead,//蛇头
SnakeBody,//蛇身
}
//地图墙壁类
public class Map : Game_Object
//希望实现在本模块控制单一墙壁的位置和朝向
{
private string pattern = "■";
public Map(int Start_x,int Start_y,int length, bool direction)
//Start_x为墙壁开始x坐标,Start_y为墙壁开始y坐标,length为墙壁长度,direction是墙壁的朝向(true为横,false为竖)
{
this.location = new Location[length];
for (int i = 0; i < length; i++)
{
this.location[i].x = direction ? Start_x + i*2 : Start_x;//为横时,x坐标逐渐增2,y坐标不变
this.location[i].y = direction ? Start_y : Start_y + i;//为竖时,x坐标不变,y坐标逐渐增1
}
}
public void Draw()
{
Console.ForegroundColor = ConsoleColor.Red;
for(int i = 0; i < this.location.Length; i++)
{
Console.SetCursorPosition(this.location[i].x, this.location[i].y);
Console.Write(pattern);
}
}
}
//绘制接口
public interface IDrawing
{
void Draw();
}
//游戏对象基类 -- 游戏过程中相互独立的对象
public class Game_Object : IDrawing
{
//考虑到在功能拓展中可能会出现无规律,仍然记录所有元素的坐标
public Location[] location;
public void Draw()
{
}
}
//边界位置结构体
public struct Location
{
public int x;
public int y;
public static bool operator ==(Location A,Location B)
{
if(A.x == B.x && A.y == B.y)
{
return true;
}
return false;
}
public static bool operator !=(Location A, Location B)
{
if (A.x == B.x && A.y == B.y)
{
return false;
}
return true;
}
}
#endregion