C#面向对象实践项目--贪吃蛇
目录
一、项目整体架构与核心逻辑
二、关键类的功能与关系
1. 游戏核心管理类:Game
2. 场景接口与基类
3. 具体场景类
4. 游戏元素类
5. 基础结构体与接口
三.类图
四、核心流程解析
五、项目可优化部分
一、项目整体架构与核心逻辑
该项目运用场景管理模式和接口驱动设计,将游戏划分为开始、进行中、结束这三个场景,借助接口实现不同模块间的交互,同时通过继承来复用代码。其核心逻辑如下:
- 场景切换机制:游戏以场景为单位进行管理,各个场景需实现
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 和 EndSceneGameObject
└─ 实现 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
来存储位置,提高检测效率。