目录
一、项目整体架构与核心逻辑
该项目运用场景管理模式和接口驱动设计,将游戏划分为开始、进行中、结束这三个场景,借助接口实现不同模块间的交互,同时通过继承来复用代码。其核心逻辑如下:
- 场景切换机制:游戏以场景为单位进行管理,各个场景需实现
ISceneUpdate
接口的Update
方法,以此完成场景内的逻辑更新与画面渲染。 - 对象绘制体系:游戏中的物体(例如墙壁、蛇身、食物)实现
IDraw
接口的Draw
方法,从而实现统一的绘制操作。 - 输入处理方式:开始场景和结束场景共用一套输入逻辑(利用
W/S
键选择选项,J
键确认),这部分逻辑在基类中实现。
项目源代码:
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
enum E_SceneType
{
Begin,
Game,
End,
}
class Game
{
//要想得到控制台的数值,就要把Game类中的成员用const修饰为常量,方便其他类使用
public const int w = 80;
public const int h = 30;
public static ISceneUpdate nowScene;
public Game ()
{
//隐藏光标
Console.CursorVisible = false;
//设置游戏窗口大小
Console.SetWindowSize(w, h);
//设置缓冲区
Console.SetBufferSize(w, h);
ChangeScene(E_SceneType.Begin);
}
public void StartGame()
{
while(true)
{
if(nowScene !=null)
{
nowScene.Update();
}
}
}
public static void ChangeScene(E_SceneType type)
{
//切换场景之前先清理之前场景内容
Console.Clear();
switch (type)
{
case E_SceneType.Begin:
nowScene = new BeginScene();
break;
case E_SceneType.Game:
nowScene = new GameScene();
break;
case E_SceneType.End:
nowScene = new EndScene();
break;
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
interface ISceneUpdate
{
void Update();
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
abstract class BeginOrEndBaseScene:ISceneUpdate
{
protected int selIndex = 0;
protected string strTilte;
protected string strOne;
public abstract void EnterJDoing();
public void Update()
{
//开始和结束场景的游戏逻辑
//选择当前的选项,然后监听键盘输入wsj
//后续在控制台输出的文本都会显示为白色,直至再次对ForegroundColor属性进行修改
Console.ForegroundColor = ConsoleColor.White;
//显示标题
Console.SetCursorPosition(Game.w / 2 - strTilte.Length, 5);
Console.Write(strTilte);
//显示下方的选项
Console.SetCursorPosition(Game.w / 2 - strOne.Length, 8);
Console.ForegroundColor = selIndex == 0 ? ConsoleColor.Red : ConsoleColor.White;
Console.Write(strOne);
Console.SetCursorPosition(Game.w / 2 - 4, 10);
Console.ForegroundColor = selIndex == 1 ? ConsoleColor.Red : ConsoleColor.White;
Console.Write("结束游戏");
//检测输入
switch (Console.ReadKey(true).Key)
{
case ConsoleKey.W:
--selIndex;
if (selIndex < 0)
selIndex = 0;
break;
case ConsoleKey.S:
++selIndex;
if (selIndex > 1)
selIndex = 1;
break;
case ConsoleKey.J:
EnterJDoing();
break;
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
class BeginScene : BeginOrEndBaseScene
{
public BeginScene ()
{
strTilte = "贪吃蛇";
strOne = "开始游戏";
}
public override void EnterJDoing()
{
if(selIndex ==0)
{
Game.ChangeScene(E_SceneType.Game);
}
else
{
Environment.Exit(0);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
class EndScene : BeginOrEndBaseScene
{
public EndScene()
{
strTilte = "游戏结束";
strOne = "回到开始界面";
}
public override void EnterJDoing()
{
if(selIndex ==0)
{
Game.ChangeScene(E_SceneType.Begin);
}
else
{
Environment.Exit(0);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
class GameScene : ISceneUpdate
{
Map map;
Snake snake;
Food food;
int upDateInded = 0;
public GameScene ()
{
map = new Map();
snake = new Snake(40, 10);
food = new Food(snake);
}
public void Update()
{
if(upDateInded %10000==0)
{
map.Draw();
food.Draw();
snake.Move();
snake.Draw();
//检测是否死亡
if(snake .CheakEnd (map))
{
//结束逻辑
Game.ChangeScene(E_SceneType.End);
}
snake.CheakEatFood(food);
upDateInded = 0;
}
++upDateInded;
//在控制台中检测玩家输入,不被卡住的解决方案
if (Console.KeyAvailable)
{
switch (Console .ReadKey (true).Key)
{
case ConsoleKey.W:
snake.ChangDir(E_MoveDir.Up);
break;
case ConsoleKey.A:
snake.ChangDir(E_MoveDir.Left);
break;
case ConsoleKey.S:
snake.ChangDir(E_MoveDir.Down);
break;
case ConsoleKey.D:
snake.ChangDir(E_MoveDir.Right);
break;
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
interface IDraw
{
void Draw();
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
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;
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
abstract class GameObject : IDraw
{
public Position pos;
public abstract void Draw();
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
class Map : IDraw
{
public 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();
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
enum E_SnakeBodyType
{
Head,
Body,
}
class SnakeBody : GameObject
{
public E_SnakeBodyType type;
public SnakeBody (E_SnakeBodyType type ,int x,int y)
{
this.type = type;
pos = new Position(x, y);
}
public override void Draw()
{
Console.SetCursorPosition(pos.x, pos.y);
Console.ForegroundColor = type == E_SnakeBodyType.Head ? ConsoleColor.Yellow : ConsoleColor.Green;
Console.Write(type == E_SnakeBodyType.Head ? "㊣" : "0");
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
enum E_MoveDir
{
Up,
Down,
Left,
Right,
}
class Snake : IDraw
{
SnakeBody[] bodys;
E_MoveDir dir;
//来记录当前蛇的长度
int nowNum;
public Snake(int x, int y)
{
bodys = new SnakeBody[200];
bodys[0] = new SnakeBody(E_SnakeBodyType.Head, x, y);
++nowNum;
dir = E_MoveDir.Right;
}
public void Draw()
{
//画一节一节的身子
for (int i = 0; i < nowNum ; i++)
{
bodys[i].Draw();
}
}
public void Move()
{
//移动前
//擦除最后一个位置
SnakeBody lastBody = bodys[nowNum - 1];
Console.SetCursorPosition(lastBody.pos.x, lastBody.pos.y);
Console.Write(" ");
//在蛇头移动之前 从蛇尾开始 不停的让后一个的位置等于前一个的位置
for (int i = nowNum -1; i >0; i--)
{
bodys[i].pos = bodys[i - 1].pos;
}
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 ChangDir(E_MoveDir dir)
{
if(dir==this.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;
}
//只要没有return 就记录外边传入的方向,按照这个方向移动
this.dir = dir;
}
public bool CheakEnd(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;
}
//通过传入一个位置来判断是否和蛇重合
public bool CheakSamePos(Position p)
{
for (int i = 0; i < nowNum ; i++)
{
if(bodys[i].pos ==p)
{
return true;
}
}
return false;
}
public void CheakEatFood(Food food)
{
if(bodys [0].pos ==food.pos )
{
//吃到了就应该让食物的位置再随机 增加蛇身体的长度
food.RandomPos(this);
//长身体
AddBody();
}
}
private void AddBody()
{
SnakeBody frontBody = bodys[nowNum - 1];
//先长
bodys[nowNum] = new SnakeBody(E_SnakeBodyType.Body, frontBody.pos.x, frontBody.pos.y);
//再加长度
++nowNum;
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
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.Write("■");
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace 贪吃蛇
{
class Food : GameObject
{
public Food(Snake snake)
{
RandomPos(snake);
}
public override void Draw()
{
Console.SetCursorPosition(pos.x, pos.y);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("o");
}
//随机位置的行为 行为 和蛇的位置有关系 有了蛇再做考虑
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语句
if(snake .CheakSamePos (pos))
{
RandomPos(snake);
}
}
}
}
using System;
namespace 贪吃蛇
{
class Program
{
static void Main(string[] args)
{
Game game = new Game();
game.StartGame();
}
}
}
二、关键类的功能与关系
1. 游戏核心管理类:Game
- 功能概述:
- 负责初始化游戏窗口,包括隐藏光标、设置窗口大小和缓冲区等操作。
- 管理场景的切换,通过
ChangeScene
方法清除当前场景内容,并创建新的场景实例。 - 运行游戏主循环,持续调用当前场景的
Update
方法。
- 成员变量:
nowScene
:类型为ISceneUpdate
,用于引用当前场景对象。w
和h
:作为常量,定义了游戏窗口的宽度(80)和高度(30)。
- 类间关系:
- 与
ISceneUpdate
接口的实现类(如BeginScene
、GameScene
、EndScene
)相关联,通过多态的方式调用场景的更新逻辑。 - 提供静态方法
ChangeScene
,供其他类触发场景切换操作。
- 与
2. 场景接口与基类
ISceneUpdate
接口:- 定义:包含
Update()
方法,所有场景类都必须实现该方法,用于处理场景的逻辑更新和画面渲染。 - 实现类:
BeginScene
、GameScene
、EndScene
。
- 定义:包含
BeginOrEndBaseScene
基类:- 功能:为开始场景和结束场景提供通用的输入处理逻辑,例如选项选择(通过
selIndex
记录当前选中项)和键盘监听(W/S
键切换选项,J
键确认)。 - 成员变量:
selIndex
:用于记录当前选中的选项索引。strTilte
和strOne
:分别表示场景标题和第一个选项的文本内容。
- 抽象方法:
EnterJDoing()
,由子类实现具体的确认逻辑(如开始游戏、退出游戏等)。 - 子类:
BeginScene
、EndScene
。
- 功能:为开始场景和结束场景提供通用的输入处理逻辑,例如选项选择(通过
3. 具体场景类
BeginScene
(开始场景):- 功能:显示 “贪吃蛇” 标题和 “开始游戏”“结束游戏” 选项,当用户选择 “开始游戏” 时,切换到游戏场景;选择 “结束游戏” 时,退出程序。
- 实现:继承自
BeginOrEndBaseScene
,重写EnterJDoing
方法,根据选中的索引来决定执行切换场景操作还是退出程序。
EndScene
(结束场景):- 功能:显示 “游戏结束” 标题和 “回到开始界面”“结束游戏” 选项,根据用户的选择切换回开始场景或者退出程序。
- 实现:同样继承自
BeginOrEndBaseScene
,重写EnterJDoing
方法来实现相应的逻辑。
GameScene
(游戏场景):- 功能:
- 管理游戏中的核心元素,包括地图(
Map
)、蛇(Snake
)和食物(Food
)。 - 处理游戏的更新逻辑,如地图绘制、蛇的移动、食物检测、碰撞检测等。
- 监听玩家的输入(
W/A/S/D
键),用于改变蛇的移动方向。
- 管理游戏中的核心元素,包括地图(
- 成员变量:
map
:类型为Map
,代表游戏地图。snake
:类型为Snake
,代表游戏中的蛇。food
:类型为Food
,代表游戏中的食物。upDateInded
:用于控制更新频率,避免画面刷新过于频繁。
- 功能:
4. 游戏元素类
Map
(地图):- 功能:绘制游戏边界的墙壁,墙壁由多个
Wall
对象组成。 - 实现:
- 在构造函数中初始化
Wall
数组,按照游戏窗口的尺寸生成上下左右四边的墙壁。 Draw
方法遍历Wall
数组,调用每个Wall
的Draw
方法来绘制墙壁。
- 在构造函数中初始化
- 功能:绘制游戏边界的墙壁,墙壁由多个
Snake
(蛇):- 功能:
- 管理蛇的身体(由
SnakeBody
对象数组组成),处理蛇的移动、转向、增长以及碰撞检测等逻辑。 - 检测蛇是否撞到墙壁或者自己的身体,以此判断游戏是否结束。
- 检测是否吃到食物,若吃到则增加蛇的身体长度,并重新生成食物的位置。
- 管理蛇的身体(由
- 成员变量:
bodys
:类型为SnakeBody[]
,存储蛇的各个身体部分。dir
:类型为E_MoveDir
,表示蛇的当前移动方向。nowNum
:记录蛇当前的身体节数。
- 关键方法:
Move()
:按照当前方向移动蛇头,并让蛇身跟随移动,同时擦除蛇尾的位置。ChangDir()
:改变蛇的移动方向,但要避免蛇反向移动(如向左时不能立即向右)。CheakEnd()
:检测蛇是否与墙壁或自身身体发生碰撞。CheakEatFood()
:检测蛇头是否与食物的位置重合。
- 功能:
SnakeBody
(蛇身体节):- 功能:表示蛇的某一节身体,区分蛇头(
Head
)和蛇身(Body
),绘制时使用不同的颜色和符号(蛇头为黄色 “㊣”,蛇身为绿色 “0”)。 - 继承:继承自
GameObject
类,GameObject
实现了IDraw
接口,因此SnakeBody
需要实现Draw
方法。
- 功能:表示蛇的某一节身体,区分蛇头(
Food
(食物):- 功能:随机生成食物的位置,确保该位置不与蛇的身体重合,并绘制食物(青色 “o”)。
- 实现:
- 在构造函数中调用
RandomPos
方法生成初始位置。 RandomPos
方法使用随机数生成位置,并通过Snake.CheakSamePos
方法检测该位置是否与蛇的身体重合,若重合则重新生成位置。
- 在构造函数中调用
5. 基础结构体与接口
Position
结构体:- 功能:表示二维坐标(x, y),重载了
==
和!=
运算符,方便进行位置比较。
- 功能:表示二维坐标(x, y),重载了
IDraw
接口:- 定义:包含
Draw()
方法,所有需要绘制的游戏元素(如Wall
、SnakeBody
、Food
)都需实现该方法。
- 定义:包含
GameObject
抽象类:- 功能:作为所有游戏物体的基类,提供
pos
属性(位置),并要求子类实现Draw
方法。 - 子类:
Wall
、SnakeBody
、Food
。
- 功能:作为所有游戏物体的基类,提供
三.类图
Game
├─ 依赖 ISceneUpdate(多态调用 Update())
│ ├─ BeginScene(继承 BeginOrEndBaseScene)
│ ├─ GameScene(实现 ISceneUpdate)
│ └─ EndScene(继承 BeginOrEndBaseScene)
│
└─ 组合 GameScene
├─ Map(包含 Wall[])
│ └─ Wall(继承 GameObject,实现 IDraw)
├─ Snake(包含 SnakeBody[])
│ └─ SnakeBody(继承 GameObject,实现 IDraw)
└─ Food(继承 GameObject,实现 IDraw)
BeginOrEndBaseScene
├─ 实现 ISceneUpdate
└─ 派生 BeginScene 和 EndScene
GameObject
└─ 实现 IDraw
├─ Wall
├─ SnakeBody
└─ Food
四、核心流程解析
- 游戏启动:
Program.Main
方法创建Game
实例,Game
的构造函数会初始化窗口,并切换到开始场景(BeginScene
)。
- 场景更新:
Game.StartGame
进入循环,不断调用nowScene.Update()
。- 在开始场景和结束场景中,
BeginOrEndBaseScene.Update
负责处理选项显示和键盘输入,用户按下J
键时触发EnterJDoing
方法来切换场景。 - 在游戏场景中,
GameScene.Update
会执行以下操作:- 每隔一定帧数(通过
upDateInded
控制),调用map.Draw()
绘制墙壁,food.Draw()
绘制食物,snake.Move()
移动蛇,snake.Draw()
绘制蛇。 - 检测蛇是否死亡(
snake.CheakEnd
),若是则切换到结束场景。 - 检测蛇是否吃到食物(
snake.CheakEatFood
),若是则增加蛇的身体长度,并重新生成食物的位置。
- 每隔一定帧数(通过
- 蛇的移动与碰撞检测:
- 蛇的移动逻辑是,蛇头按照当前方向移动,蛇身的每一节依次跟随前一节的位置移动,蛇尾的位置会被擦除。
- 通过
Snake.CheakEnd
方法检测蛇头是否与墙壁或蛇身发生碰撞,以此判断游戏是否结束。
- 食物生成:
Food.RandomPos
方法生成随机位置,若该位置与蛇的身体重合,则递归调用自身重新生成位置,直到找到合适的位置为止。
五、项目可优化部分
- 分离职责:可以将输入处理、渲染逻辑和业务逻辑进一步分离,提高代码的可维护性。
- 增加配置项:把窗口大小、更新频率等参数提取到配置文件中,方便进行调整。
- 完善分数系统:在
GameScene
中添加分数统计功能,当蛇吃到食物时增加分数,并在界面上进行显示。 - 优化碰撞检测:对于蛇身的碰撞检测,可以使用
HashSet
来存储位置,提高检测效率。