C#面向对象实践项目--贪吃蛇

发布于:2025-06-04 ⋅ 阅读:(28) ⋅ 点赞:(0)

目录

一、项目整体架构与核心逻辑

二、关键类的功能与关系

1. 游戏核心管理类:Game

2. 场景接口与基类

3. 具体场景类

4. 游戏元素类

5. 基础结构体与接口

三.类图

四、核心流程解析

五、项目可优化部分


一、项目整体架构与核心逻辑

该项目运用场景管理模式接口驱动设计,将游戏划分为开始、进行中、结束这三个场景,借助接口实现不同模块间的交互,同时通过继承来复用代码。其核心逻辑如下:

  1. 场景切换机制:游戏以场景为单位进行管理,各个场景需实现ISceneUpdate接口的Update方法,以此完成场景内的逻辑更新与画面渲染。
  2. 对象绘制体系:游戏中的物体(例如墙壁、蛇身、食物)实现IDraw接口的Draw方法,从而实现统一的绘制操作。
  3. 输入处理方式:开始场景和结束场景共用一套输入逻辑(利用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,用于引用当前场景对象。
    • wh:作为常量,定义了游戏窗口的宽度(80)和高度(30)。
  • 类间关系
    • ISceneUpdate接口的实现类(如BeginSceneGameSceneEndScene)相关联,通过多态的方式调用场景的更新逻辑。
    • 提供静态方法ChangeScene,供其他类触发场景切换操作。
2. 场景接口与基类
  • ISceneUpdate接口
    • 定义:包含Update()方法,所有场景类都必须实现该方法,用于处理场景的逻辑更新和画面渲染。
    • 实现类BeginSceneGameSceneEndScene
  • BeginOrEndBaseScene基类
    • 功能:为开始场景和结束场景提供通用的输入处理逻辑,例如选项选择(通过selIndex记录当前选中项)和键盘监听(W/S键切换选项,J键确认)。
    • 成员变量
      • selIndex:用于记录当前选中的选项索引。
      • strTiltestrOne:分别表示场景标题和第一个选项的文本内容。
    • 抽象方法EnterJDoing(),由子类实现具体的确认逻辑(如开始游戏、退出游戏等)。
    • 子类BeginSceneEndScene
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数组,调用每个WallDraw方法来绘制墙壁。
  • 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),重载了==!=运算符,方便进行位置比较。
  • IDraw接口
    • 定义:包含Draw()方法,所有需要绘制的游戏元素(如WallSnakeBodyFood)都需实现该方法。
  • GameObject抽象类
    • 功能:作为所有游戏物体的基类,提供pos属性(位置),并要求子类实现Draw方法。
    • 子类WallSnakeBodyFood

三.类图

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

四、核心流程解析

  1. 游戏启动
    • Program.Main方法创建Game实例,Game的构造函数会初始化窗口,并切换到开始场景(BeginScene)。
  2. 场景更新
    • Game.StartGame进入循环,不断调用nowScene.Update()
    • 在开始场景和结束场景中,BeginOrEndBaseScene.Update负责处理选项显示和键盘输入,用户按下J键时触发EnterJDoing方法来切换场景。
    • 在游戏场景中,GameScene.Update会执行以下操作:
      • 每隔一定帧数(通过upDateInded控制),调用map.Draw()绘制墙壁,food.Draw()绘制食物,snake.Move()移动蛇,snake.Draw()绘制蛇。
      • 检测蛇是否死亡(snake.CheakEnd),若是则切换到结束场景。
      • 检测蛇是否吃到食物(snake.CheakEatFood),若是则增加蛇的身体长度,并重新生成食物的位置。
  3. 蛇的移动与碰撞检测
    • 蛇的移动逻辑是,蛇头按照当前方向移动,蛇身的每一节依次跟随前一节的位置移动,蛇尾的位置会被擦除。
    • 通过Snake.CheakEnd方法检测蛇头是否与墙壁或蛇身发生碰撞,以此判断游戏是否结束。
  4. 食物生成
    • Food.RandomPos方法生成随机位置,若该位置与蛇的身体重合,则递归调用自身重新生成位置,直到找到合适的位置为止。

五、项目可优化部分

  1. 分离职责:可以将输入处理、渲染逻辑和业务逻辑进一步分离,提高代码的可维护性。
  2. 增加配置项:把窗口大小、更新频率等参数提取到配置文件中,方便进行调整。
  3. 完善分数系统:在GameScene中添加分数统计功能,当蛇吃到食物时增加分数,并在界面上进行显示。
  4. 优化碰撞检测:对于蛇身的碰撞检测,可以使用HashSet来存储位置,提高检测效率。


网站公告

今日签到

点亮在社区的每一天
去签到