利用C#实现贪吃蛇
说明
本文根据B站up主唐老狮的课程所学所记
UML
关联: 如类A会有一个类B成员作为它的成员变量
直接关联: 如母鸡类中有一个行为是下蛋,它和气候直接关联
聚合: 如地图类聚合围墙类,鸟群类聚合大雁类
依赖关系: 如动物类依赖于空气类和水类
复合: 如公司类包含各种部门类,部门类和公司类的关系就是复合关系
面向对象七大原则
总体实现目标
高内聚、低耦合,使程序模块的可重用性、移植性增强
高内聚低耦合
从类角度来看:减少类内部对其他类的调用
从功能模块来看:减少模块之间的交互复杂度
单一职责原则(SRP,Single Responsibility Principle)
类被修改的几率很大,因此应该专注于单一的功能。如果把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能。
例如:假设程序、策划、美术三个工种是三个类,他们应该各司其职,在程序世界中只应该做自己应该做的事情。
开闭原则(OCP,Open-Closed Principle)
对拓展开放,对修改关闭
拓展开放:模块的行为可以被拓展从而满足新的需求
修改关闭:不允许修改模块的源代码(或者尽量使修改最小化)
例如:继承就是最典型的开闭原则的体现,可以通过添加新的子类和重写父类的方法来实现。
里氏替换原则(LSP,Liskov Substitution Principle)
任何父类出现的地方,子类都可以替代
例如:用父类容器装载子类对象,因为子类对象包含了父类的所有内容。
依赖倒转原则(DIP,Dependence Inversion Principle)
要依赖于抽象,不要依赖于具体的实现。
例如:玩家对象抽象出开枪这一行为
迪米特原则(LoP of Demeter)
又称最少知识原则,一个对象应当对其他对象尽可能少的了解,不要和陌生人说话
例如:一个对象中的成员,要尽可能少的直接和其他类建立关系,目的是降低耦合性。
接口分离原则(ISP,Interface Segregation Principle)
不应该强迫别人依赖他们不需要使用的方法,一个接口不需要提供太多的行为,一个接口应该尽量只提供一个对外的功能,让别人去选择需要实现什么样的行为,而不是把所有的行为都封装到一个接口当中。
例如:飞行接口、走路接口、跑步接口等等虽然都是移动的行为,但是我们应该把他们分为一个一个单独的接口,让别人去选择使用。
合成复用原则(CRP,Composite Reuse Principle)
尽量使用对象组合,而不是继承来达到复用的目的,继承关系是强耦合,组合关系是低耦合。
例如:脸应该是眼镜、鼻子、嘴巴、耳朵的组合,而不是依次的继承。角色和装备也应该是组合,而不是继承。
注意:不能盲目的使用合成复用原则,要在遵循迪米特原则的前提下。
UML类图
Game对象和场景更新接口
1、创建“贪吃蛇”项目,创建“c1”文件
2、在“c1”文件下创建“Game”类
3、在“c1”文件下创建“ISceneUpdate”接口
4、对“ISceneUpdate”添加Update()方法
5、添加“Game”类所需成员变量
//窗口长宽
public int w = 80;
public int h = 20;
//当前选中场景
public ISceneUpdate nowScene;
6、构造函数实现
public Game()
{
Console.CursorVisible = false;//鼠标是否隐藏
Console.SetWindowSize(w, h);//设置窗口大小
Console.SetBufferSize(w, h);//设置缓冲区大小
}
7、实现游戏主循环:添加start函数
//实现游戏主循环:负责游戏场景逻辑的更新
public void start()
{
while(true)
{
//如果当前场景不为空就更新
if(nowScene != null)
{
nowScene.Update();
}
}
}
8、场景切换
①添加E_SceneType枚举
//场景类型枚举
enum E_SceneType
{
Begin,
Game,
End
}
②添加ChangeScene函数
//场景切换
public void ChangeScene(E_SceneType type)
{
//切场景之前应该把上一个场景的绘制内容擦掉
Console.Clear();
switch(type)
{
case E_SceneType.Begin:
break;
case E_SceneType.Game:
break;
case E_SceneType.End:
break;
}
}
修改文件展示:
Game.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 贪吃蛇.c1
{
//场景类型枚举
enum E_SceneType
{
Begin,
Game,
End
}
class Game
{
//窗口长宽
public int w = 80;
public int h = 20;
//当前选中场景
public ISceneUpdate nowScene;
public Game()
{
Console.CursorVisible = false;//鼠标是否隐藏
Console.SetWindowSize(w, h);//设置窗口大小
Console.SetBufferSize(w, h);//设置缓冲区大小
}
//实现游戏主循环:负责游戏场景逻辑的更新
public void start()
{
while(true)
{
//如果当前场景不为空就更新
if(nowScene != null)
{
nowScene.Update();
}
}
}
//场景切换
public void ChangeScene(E_SceneType type)
{
//切场景之前应该把上一个场景的绘制内容擦掉
Console.Clear();
switch(type)
{
case E_SceneType.Begin:
break;
case E_SceneType.Game:
break;
case E_SceneType.End:
break;
}
}
}
}
ISceneUpdate.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 贪吃蛇.c1
{
interface ISceneUpdate
{
void Update();
}
}
实现多场景切换
1、创建“c2”文件,添加游戏场景类GameScene,为了继承ISceneUpdate,则需要引用命名空间
using 贪吃蛇.c1;
继承接口ISceneUpdate
namespace 贪吃蛇.c2
{
class GameScene : ISceneUpdate
{
public void Update()
{
Console.SetCursorPosition(0, 0);
Console.Write("游戏场景");
}
}
}
3、添加开始和结束场景基类BeginOrEndBaseScene,并继承ISceneUpdate
using 贪吃蛇.c1;
namespace 贪吃蛇.c2
{
class BeginOrEndBaseScene : ISceneUpdate
{
public int nowSelIndex = 0;
public string strTitle;
public string strOne;//表示
public void Update()
{
Console.SetCursorPosition(0, 0);
Console.Write("开始或结束场景");
}
}
}
4、修改Game类中的ChangeScene方法
public void ChangeScene(E_SceneType type)
{
//切场景之前应该把上一个场景的绘制内容擦掉
Console.Clear();
switch(type)
{
case E_SceneType.Begin:
nowScene = new BeginOrEndBaseScene();//当前场景为开始或者结束
break;
case E_SceneType.Game:
nowScene = new GameScene();//当前场景为游戏
break;
case E_SceneType.End:
nowScene = new BeginOrEndBaseScene();
break;
}
}
在Game类中的构造函数调用ChangeScene方法
ChangeScene(E_SceneType.Begin);
主函数实现如下
运行测试如下
5、开始和结束基类逻辑实现
①首先将Game中的w、h变量改为const常量,方便通过Game直接调用
②将BeginOrEndBaseScene改为抽象类,添加抽象方法,并将该类中的成员变量设置为protected。
public abstract void EnterJDoSomething();//按下j键的逻辑
③实现BeginOrEndBaseScene中的Update方法
public void Update()
{
//将控制台的前景色设为白色
Console.ForegroundColor = ConsoleColor.White;
//显示标题
Console.SetCursorPosition(Game.w / 2 - strTitle.Length, 5);//设置光标在窗口宽度的一半减去字体长度的位置,第五行
Console.Write(strTitle);//打印标题
//显示第一个选项
Console.SetCursorPosition(Game.w / 2 - strOne.Length, 8);
Console.ForegroundColor = nowSelIndex == 0 ? ConsoleColor.Red : ConsoleColor.White;//根据索引不同调整选中选项颜色
Console.Write(strOne);
//显示第二个选项
Console.SetCursorPosition(Game.w / 2 - 4, 10);
Console.ForegroundColor = nowSelIndex == 1 ? ConsoleColor.Red : ConsoleColor.White;
Console.Write("游戏结束");
//检测输入
switch (Console.ReadKey(true).Key)
{
case ConsoleKey.W://按下w键
--nowSelIndex;
if(nowSelIndex < 0)
{
nowSelIndex = 0;
}
break;
case ConsoleKey.S://按下s键
++nowSelIndex;
if (nowSelIndex > 0)
nowSelIndex = 1;
break;
case ConsoleKey.J://按下j键
EnterJDoSomething();
break;
}
}
④将Game类中改变场景ChangeScene方法实例化BeginOrEndBaseScene类注释掉,因为该类已经是一个抽象类,不能被实例化。
6、实现开始场景BeginScene类
①将BeginScene类继承BeginOrEndBaseScene,并实现构造函数将strTitle和strOne初始化
class BeginScene : BeginOrEndBaseScene
{
public BeginScene()
{
strTitle = "贪吃蛇";
strOne = "开始游戏";
}
public override void EnterJDoSomething()
{
}
}
②为了能在此类中调用ChangeScene函数,特将Game类中的ChangeScene函数设为static,由于静态方法中不能调用成员变量,则将nowScene也设为静态变量。
③实现EnterJDoSomething方法
public override void EnterJDoSomething()
{
if(nowSelIndex == 0)
{
Game.ChangeScene(E_SceneType.Game);
}
else
{
Environment.Exit(0);
}
}
④将Game类中改变场景开始被注释掉的代码改为
运行代码,此时按下w或者s键盘可上下切换,且在开始游戏选项中按下j键可进入游戏场景。
7、实现结束场景EndScene类
class EndScene : BeginOrEndBaseScene
{
public EndScene()
{
strTitle = "游戏结束";
strOne = "回到开始界面";
}
public override void EnterJDoSomething()
{
if (nowSelIndex == 0)
{
Game.ChangeScene(E_SceneType.Begin);
}
else
{
Environment.Exit(0);
}
}
}
运行代码,初始出现在结束场景。
游戏对象基类的实现
1、创建c3文件夹,创建IDraw接口,添加Draw方法
2、创建一个类,改名为Position,作为位置结构体
struct Position
{
public int x;
public int y;
public Position(int x, int y)
{
this.x = x;
this.y = y;
}
public static bool operator ==(Position p1,Position p2)
{
if(p1.x == p2.x && p1.y == p2.y)
{
return true;
}
return false;
}
public static bool operator !=(Position p1, Position p2)
{
if (p1.x == p2.x && p1.y == p2.y)
{
return false;
}
return true;
}
}
3、创建GameObject类
abstract class GameObject : IDraw
{
public Position pos;
public abstract void Draw();
}
继承游戏对象基类的对象
1、实现地图墙壁类Wall
①创建c4文件夹,添加类Wall
class Wall : GameObject
{
public Wall(int x,int y)
{
pos = new Position(x,y);
}
public override void Draw()
{
Console.SetCursorPosition(pos.x, pos.y);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("🤡");
}
}
2、实现食物类Food
class Food : GameObject
{
public Food(int x,int y)
{
pos = new Position(x,y);
}
public override void Draw()
{
Console.SetCursorPosition(pos.x, pos.y);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("🐘");
}
}
3、实现蛇身子类和蛇身枚举
enum E_SnakeBody_Type
{
Head,
Body
}
class SnakeBody : GameObject
{
private E_SnakeBody_Type type;
public SnakeBody(E_SnakeBody_Type type,int x,int y)
{
this.type = type;
this.pos = new Position(x,y);
}
public override void Draw()
{
Console.SetCursorPosition(pos.x,pos.y);
Console.ForegroundColor = type == E_SnakeBody_Type.Head ? ConsoleColor.Green : ConsoleColor.Red;
Console.WriteLine(type == E_SnakeBody_Type.Head ? "😍" : "🔞");
}
}
地图对象
1、创建c5文件夹,添加地图类Map
class Map : IDraw
{
private Wall[] walls;
public Map()
{
walls = new Wall[Game.w + (Game.h - 3) * 2];
int index = 0;
for (int i = 0; i < Game.w; i += 2)
{
walls[index] = new Wall(i, 0);
++index;
}
for (int i = 0; i < Game.w; i += 2)
{
walls[index] = new Wall(i, Game.h - 2);
++index;
}
for (int i = 1; i < Game.h - 2; i++)
{
walls[index] = new Wall(0, i);
++index;
}
for (int i = 1; i < Game.h - 2; i++)
{
walls[index] = new Wall(Game.w - 2, i);
++index;
}
}
public void Draw()
{
for (int i = 0; i < walls.Length; i++)
{
walls[i].Draw();
}
}
2、修改游戏场景类GameScene
class GameScene : ISceneUpdate
{
private Map map;
public GameScene()
{
map = new Map();
}
public void Update()
{
map.Draw();
}
}
蛇对象
1、创建c6文件夹,添加蛇类Snake
class Snake : IDraw
{
SnakeBody[] bodys;
int nowNum;
public Snake(int x,int y)
{
bodys = new SnakeBody[200];
bodys[0] = new SnakeBody(E_SnakeBody_Type.Head,x,y);
nowNum = 1;
}
public void Draw()
{
for(int i = 0;i < nowNum;i++)
{
bodys[i].Draw();
}
}
}
2、修改游戏场景类GameScene
class GameScene : ISceneUpdate
{
private Map map;
Snake snake;
public GameScene()
{
map = new Map();
snake = new Snake(40,10);
}
public void Update()
{
map.Draw();
snake.Draw();
}
}
蛇移动
1、修改蛇类Snake
①添加方向枚举
②初始化枚举变量
③添加Move方法
enum E_MoveDir
{
up,
down, left, right
}
class Snake : IDraw
{
SnakeBody[] bodys;
int nowNum;
E_MoveDir dir;
public Snake(int x,int y)
{
bodys = new SnakeBody[200];
bodys[0] = new SnakeBody(E_SnakeBody_Type.Head,x,y);
nowNum = 1;
dir = E_MoveDir.up;
}
public void Move()
{
SnakeBody lastBody = bodys[nowNum - 1];
Console.SetCursorPosition(lastBody.pos.x,lastBody.pos.y);
Console.WriteLine(" ");
switch (dir)
{
case E_MoveDir.up:
--bodys[0].pos.y;
break;
case E_MoveDir.down:
++bodys[0].pos.y;
break;
case E_MoveDir.left:
bodys[0].pos.x -= 2;
break;
case E_MoveDir.right:
bodys[0].pos.x += 2;
break;
}
}
public void Draw()
{
for(int i = 0;i < nowNum;i++)
{
bodys[i].Draw();
}
}
}
2、修改游戏场景代码GameScene
class GameScene : ISceneUpdate
{
private Map map;
Snake snake;
int updateIndex;
public GameScene()
{
map = new Map();
snake = new Snake(40,10);
}
public void Update()
{
if(updateIndex % 99999999 == 0)
{
map.Draw();
snake.Move();
snake.Draw();
updateIndex = 0;
}
updateIndex++;
}
}
蛇转向
1、在Snake类中添加ChangeDir方法
public void ChangeDir(E_MoveDir dir)
{
//不转向的情况
if (this.dir == dir ||
nowNum > 1 && (this.dir == E_MoveDir.left && dir == E_MoveDir.right
|| this.dir == E_MoveDir.right && dir == E_MoveDir.left
|| this.dir == E_MoveDir.up && dir == E_MoveDir.down
|| this.dir == E_MoveDir.down && dir == E_MoveDir.up))
{
return;
}
//否则转向
this.dir = dir;
}
2、在游戏场景类GameScene中修改Update
public void Update()
{
if(updateIndex % 22222 == 0)
{
map.Draw();
snake.Move();
snake.Draw();
updateIndex = 0;
}
updateIndex++;
if(Console.KeyAvailable)//按键保持激活的时候
{
switch(Console.ReadKey(true).Key)
{
case ConsoleKey.W:
snake.ChangeDir(E_MoveDir.up);
break;
case ConsoleKey.S:
snake.ChangeDir(E_MoveDir.down);
break;
case ConsoleKey.A:
snake.ChangeDir(E_MoveDir.left);
break;
case ConsoleKey.D:
snake.ChangeDir(E_MoveDir.right);
break;
}
}
}
撞墙撞身体
1、Snake类中添加CheckEnd函数
public bool CheckEnd(Map map)
{
for(int i = 0;i < map.walls.Length;i++)
{
if (bodys[0].pos == map.walls[i].pos)//判断头与墙的位置是否相等
{
return true;
}
}
for(int i = 1;i < nowNum;i++)
{
if (bodys[0].pos == bodys[i].pos) return true;//判断头与身体的位置是否相等
}
return false;
}
2、将Map中walls数组改为public
3、GameScene中的Update添加如下代码
吃食物
1、在Snake类中添加CheckSamePos函数
public bool CheckSamePos(Position p)
{
for (int i = 0; i < nowNum; i++)
{
if (bodys[i].pos == p)//判断传入参数是否和蛇位置相等
return true;
}
return false;
}
2、在Food类中添加RandomPos函数
public void RandomPos(Snake snake)//用于随机位置生成食物
{
Random r = new Random();
int x = r.Next(2, (Game.w / 2 - 1) * 2);
int y = r.Next(1, Game.h - 4);
pos = new Position(x, y);
if(snake.CheckSamePos(pos))
{
RandomPos(snake);
}
}
3、更新Food构造函数
public Food(Snake snake)
{
RandomPos(snake);
}
4、在Snake类中添加CheckEatPos函数
public void CheckEatFood(Food food)
{
if (bodys[0].pos == food.pos)
food.RandomPos(this);
}
5、在GameScene中的Update方法中加入如下代码
长身体
1、在Snake类中添加AddBody方法
public void AddBody()
{
SnakeBody frontBody = bodys[nowNum - 1];
bodys[nowNum] = new SnakeBody(E_SnakeBody_Type.Body, frontBody.pos.x,frontBody.pos.y);
++nowNum;
}
2、更新Snake中的CheckEatFood方法
3、更新Snake中的Move方法
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!