尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#贪吃蛇实战:300行代码串联核心编程思维

C#贪吃蛇实战:300行代码串联核心编程思维 简介一份面向C#初学者的控制台游戏开发实战资源通过完整实现经典贪吃蛇帮助开发者巩固类、方法、变量、条件语句、循环、状态机等核心语法与算法逻辑。资源包共33个文件体积约70KB主体为18个.cs源码文件涵盖场景管理、蛇身移动、食物生成、地图绘制等模块另有可运行的.exe文件方便直接查看效果以及json/config等配置文件和sqlite数据文件便于理解工程结构。目前已有217人学习说明该资源具备一定参考价值。项目代码将游戏拆分为多个场景与对象条理清晰适合逐步阅读。通过研读代码并动手修改读者可以掌握控制台输入输出处理、碰撞检测、游戏主循环设计等技能也能学习如何将面向对象思想应用到小型游戏项目中是C#入门阶段难得的完整小项目练习。 如果让我给刚学C#的朋友推荐一个入门练手项目大概率会是这个贪吃蛇。别觉得它小一个控制台版的贪吃蛇实际上把C#里最有价值的那几块硬骨头——数据类型、集合、面向对象、事件委托、状态机、输入响应、界面刷新——全部串起来了。我见过不少人一上来就啃上位机、搞Socket、做VisionPro联合编程结果连一个最基础的“帧循环 外部输入变更状态”都讲不清楚。贪吃蛇恰恰能把这个思维理顺而且不到三百行代码就能跑起来成就感来得特别快。这篇文章不是给你贴一份完整源码就完事我会把整个项目的设计思路、核心原理、实操时容易踩的坑都拆开讲。不管你是刚入门想找项目练手还是准备面试想拿一个能讲透的项目又或者后面要做C#上位机开发、设备通讯这类偏工业的场景这篇都值得你认真看一遍。1. 为什么拿贪吃蛇当C#实践项目1.1 麻雀虽小五脏俱全的知识点覆盖贪吃蛇这个游戏规则一句话就能说清楚控制一条蛇吃食物每吃一个就变长撞墙或者撞到自己就结束。但恰恰是这么简单的规则背后需要的技术点几乎覆盖了C#面试和日常开发的高频区域。我把这个项目里真正被用到的知识点列一张表你就知道它的含金量了知识点在贪吃蛇里的落点实际价值数据类型int、byte、char、bool坐标、方向、游戏状态、按键值类型选择和转换的基础训练枚举enum方向枚举、游戏状态枚举避免魔法数字代码可读性关键结构体struct坐标点Point值类型使用场景List 集合存放蛇身节点泛型集合增删改查的典型场景面向对象类、职责划分Snake、Food、GameManager、Renderer单一职责原则的反复实践委托与事件吃到食物事件、游戏结束事件解耦模块之间的依赖循环与线程控制游戏主循环、Thread.Sleep调速控制台版帧循环的核心状态机Ready、Running、Paused、GameOver复杂逻辑梳理利器很多初学者把这些知识点单独拎出来都能看懂一放到真实项目里就不知道怎么组合。贪吃蛇就是那个最合适的“组合练习场”。1.2 游戏循环和上位机开发的思考方式其实是同一套这一点我想多说两句。热词里出现了一堆“C#上位机”、“扫码枪触发事件”、“Socket通讯”乍看跟贪吃蛇八竿子打不着但如果你动手写过贪吃蛇的控制逻辑再去搞设备通讯会发现脑子里的模型是一样的循环等待外部事件收到事件后更新内部状态然后刷新界面。扫码枪触发事件本质是什么就是设备输入导致系统状态变化跟你在贪吃蛇里按方向键让蛇转向机制上没有任何区别。Socket收包后更新界面也是如此收到数据、处理数据、重绘界面。先在小项目里把这个“事件循环 状态更新 界面刷新”的骨架做熟了后面上手真实项目才会觉得似曾相识而不是两眼一抹黑。2. 整体设计与思路拆解2.1 技术选型控制台版还是WinForms版做贪吃蛇第一步要定的就是形态。网上有控制台版、WinForms版、WPF版我给的结论很直接第一次做就做控制台版。表格对比一下方案优点缺点适合人群控制台版无任何UI依赖、代码聚焦核心逻辑、启动快画面简陋、刷新机制需自己处理新手入门、面试讲原理WinForms版有画面、System.Windows.Forms.Timer处理刷新引入UI线程概念、复杂度上升有一定基础、想练UI事件WPF版画面漂亮、MVVM可拓展数据绑定和渲染逻辑对新手不友好只推荐进阶者尝试控制台版最大的好处是“逼着你手动处理游戏循环”。在WinForms里你拖一个Timer控件Tick事件一写就完事了很多底层机制反而被框架藏起来了。控制台版不一样每一步都由你亲手控制什么频率刷新、什么时候读键盘、怎么重绘蛇身全部可见这才叫吃透了原理。2.2 数据结构的选型思路整个游戏里最核心的数据结构就三样方向、坐标、蛇身集合。方向用一个枚举public enum Direction { Up, Down, Left, Right }为什么不用int或者字符串因为方向只有四个取值用枚举在编译期就能排除非法值你在代码里写direction Direction.Up任何智商正常的程序员一眼就能读懂。你要是用direction 1三天后你自己都忘了1是上还是下。坐标点用一个结构体public struct Point { public int X { get; set; } public int Y { get; set; } public Point(int x, int y) { X x; Y y; } }C#里Point结构体的一个天然优势是值比较两个Point的X和Y相同它们就相等List.Contains就能直接判断某个点是否在蛇身上。你要是用类class来做默认是引用比较每次还得手动比较X、Y麻烦不少。这里用struct比用class更合适也是很多面试官会追问的点。蛇身集合用List private ListPoint snake new ListPoint();为什么不用数组因为蛇的长度是动态变化的数组定长不可变每次吃食物扩容还得你自己写逻辑纯粹给自己找麻烦。为什么不用LinkedList看起来蛇头插入蛇尾删除是链表的长处但在控制台贪吃蛇里Snake需要频繁按下标访问节点做碰撞检测List的随机访问优势更明显。这个场景里List就是综合最优解。顺带提一个细节坐标类型我用了int其实用byte也能装下因为控制台窗口宽高一般不会超过255。但实际项目中为了后续扩展不出问题int更稳妥。不是所有场景都要“最省内存”代码的可维护性永远排在无谓的优化前面。2.3 单例模式与职责划分我见过很多人写贪吃蛇所有代码全塞在Main里写了两三百行逻辑一团浆糊。这个项目虽然小但我建议一开始就用面向对象的思维来拆Snake类维护蛇身集合和移动逻辑Food类生成食物、判断是否被吃到GameManager类游戏主循环、状态管理、碰撞检测Renderer类专门负责绘制界面为什么这样拆因为有明确的单一职责Snake只管自己怎么动Food只管自己出现在哪Renderer只管画GameManager只管协调。以后你改食物颜色、改蛇的移动速度、加个暂停功能都只动对应模块不用在几百行代码里大海捞针。GameManager可以考虑用单例模式实现。因为这个游戏的状态全局只有一份不管从哪个模块访问都应该是同一个GameManager实例避免出现“这个模块改了一份状态、那个模块读的是另一份”的诡异问题。C#写单例最简洁的方式public sealed class GameManager { private static readonly LazyGameManager _instance new LazyGameManager(() new GameManager()); public static GameManager Instance _instance.Value; private GameManager() { } }LazyT是C#里实现线程安全单例最优雅的方式启动时才真正创建实例不用你手动处理lock。贪吃蛇本身是单线程的用不上并发保护但提前掌握这个写法以后做多线程项目直接用就行。3. 核心细节解析与实操要点3.1 方向控制与按键缓冲别让蛇“反向穿身”蛇的移动逻辑很好理解每次在蛇头方向插入一个新节点如果吃到食物就保留尾节点没吃到就移除尾节点。但新手最容易犯的错误是——允许180度掉头。比如蛇正在向左走你按一下右键蛇头直接朝右瞬间撞到第二节身体游戏结束。这个问题的本质是一个合法的新方向不能和当前方向相反。处理方式是在改变方向的入口统一拦截private void ChangeDirection(Direction newDirection) { if (newDirection Direction.Up direction Direction.Down) return; if (newDirection Direction.Down direction Direction.Up) return; if (newDirection Direction.Left direction Direction.Right) return; if (newDirection Direction.Right direction Direction.Left) return; direction newDirection; }还有个细节很容易忽略如果用户一帧内快速按了两个方向键比如先按上再按左但此刻蛇还在向右走这两次按键都生效的话蛇会先向右上移动再立即向左上移动看起来就像“穿过自己”。更合理的做法是只允许每帧处理一个输入或者用队列存储输入、每帧只取一个。控制台版贪吃蛇对精度要求不高我给的方案是在一次循环里只处理一次按键if (Console.KeyAvailable) { ConsoleKeyInfo key Console.ReadKey(true); // 只处理当前读取到的这一次按键 }这样能避免输入缓冲叠加导致的鬼畜转向实测下来手感最正常。注意Console.ReadKey(true)里的参数true表示不把按下的字符回显到控制台这个必须带上否则屏幕上会出现一堆残留按键字符。3.2 碰撞检测的边界与细节碰撞检测是游戏的胜负手分两种撞墙和撞自己。撞墙的检测比较简单拿到新蛇头坐标看是否越过控制台的绘制区域边界private bool IsWallCollision(Point head) { return head.X 0 || head.X Console.WindowWidth - 1 || head.Y 0 || head.Y Console.WindowHeight - 1; }这里有个细节值得注意控制台窗口的坐标原点在左上角X轴向右Y轴向下最右和最下可绘制的坐标是WindowWidth-1和WindowHeight-1。所以边界判断时要防止蛇头进入那个会触发自动换行的坐标。保留边距一方面为了视觉舒适另一方面是为了避免蛇身字符被控制台缓冲自动换行搞乱。撞自己有两种实现思路。一种是遍历蛇身所有节点逐一和蛇头比较坐标另一种是用一个HashSetPoint来存储蛇身坐标查重复杂度O(1)。控制台贪吃蛇的蛇身长度一般不会超过几百List遍历完全够用但如果你想把代码写得有点“性能意识”可以用HashSet做辅助检测。我记得C#里的hashset有Contains方法。不过要注意蛇头在移动后的瞬间旧蛇尾可能还没移除判断时应该排除蛇尾所在节点否则会造成“自撞误判”。3.3 绘制与刷新千万别用Console.Clear这是控制台贪吃蛇里最影响体验的细节。很多新手图省事每帧直接Console.Clear()然后整个重画一跑起来就发现屏幕疯狂闪烁眼睛都快瞎了。原因在于Console.Clear()清空整个缓冲区再逐字重绘这个开销很大而且会有明显的闪烁。正确的做法是定点绘制只在新蛇头位置画一个方块在旧蛇尾位置画一个空格食物变化时再单独更新食物位置。核心绘制逻辑大致是这样// 绘制新头 Console.SetCursorPosition(newHead.X, newHead.Y); Console.Write(■); // 绘制食物 Console.SetCursorPosition(food.X, food.Y); Console.Write(●); // 如果没有吃到食物擦掉旧尾 if (!isEating) { Console.SetCursorPosition(oldTail.X, oldTail.Y); Console.Write( ); }每帧只写几个字符闪烁问题自然消失。这也是控制台编程里一个很重要的思想局部刷新代替全量刷新放到上位机开发里就是UI更新的性能优化思路原理一模一样。4. 实操过程与核心环节实现4.1 搭建项目骨架与全局配置先创建一个控制台项目我用的是.NET 6以上的模板直接用Console.WriteLine做输出。项目结构建议按职责建四个文件Snake.cs、Food.cs、Renderer.cs、GameManager.cs。先定义全局配置类把边界、初始速度、初始蛇身都集中管理public static class GameConfig { public const int Width 60; // 地图宽度 public const int Height 20; // 地图高度 public const int InitialSpeed 120; // 初始帧间隔ms public const int SpeedStep 5; // 每吃一个加速多少ms public static Point StartHead new Point(Width / 2, Height / 2); public static Point StartTail new Point(Width / 2 1, Height / 2); }把配置独立出来的意义在于以后想调整地图大小改GameConfig一处即可不用在逻辑代码里到处翻魔法数字。4.2 游戏主循环与事件响应游戏主循环是整个项目的发动机我建议用while循环加Thread.Sleep控制帧率而不是用Timer。道理很简单控制台应用里用Timer事件回调在不同线程还得处理线程同步用while Sleep所有逻辑都在主线程顺序执行代码逻辑更直观。主循环的骨架public void Run() { Initialize(); DrawAll(); while (gameState GameState.Running) { HandleInput(); Update(); Renderer.Instance.Draw(this); Thread.Sleep(currentSpeed); } Renderer.Instance.ShowGameOver(score); }这里我用了状态机而不是简单的bool标记。GameState枚举我至少定义四个值Ready、Running、Paused、GameOver。有了状态机以后加暂停、加游戏结束界面都特别顺手。键盘响应依赖Console.KeyAvailable和Console.ReadKey配合private void HandleInput() { if (!Console.KeyAvailable) return; ConsoleKeyInfo key Console.ReadKey(true); switch (key.Key) { case ConsoleKey.UpArrow: case ConsoleKey.W: ChangeDirection(Direction.Up); break; case ConsoleKey.DownArrow: case ConsoleKey.S: ChangeDirection(Direction.Down); break; case ConsoleKey.LeftArrow: case ConsoleKey.A: ChangeDirection(Direction.Left); break; case ConsoleKey.RightArrow: case ConsoleKey.D: ChangeDirection(Direction.Right); break; case ConsoleKey.Spacebar: TogglePause(); break; } }KeyAvailable的作用是非阻塞检查有按键才读取没按键直接返回保证游戏循环不会卡在ReadKey那里等输入。这个API的语义很清晰查询 读取。这里一定要先查再读不然读操作会把循环阻塞住。4.3 食物生成、计分和游戏状态管理更新逻辑里最核心的就是蛇的移动private void Update() { Point newHead GetNextHead(); bool isEating newHead.Equals(food); // 保存旧尾部用于擦除 Point oldTail snake[snake.Count - 1]; // 插入新头 snake.Insert(0, newHead); if (isEating) { score; currentSpeed Math.Max(40, currentSpeed - GameConfig.SpeedStep); GenerateFood(); OnFoodEaten?.Invoke(this, EventArgs.Empty); } else { // 没吃到就移除尾部 snake.RemoveAt(snake.Count - 1); Renderer.Instance.ClearPoint(oldTail); } if (IsCollision(newHead)) { gameState GameState.GameOver; OnGameOver?.Invoke(this, EventArgs.Empty); } }食物生成要注意不能生成到蛇身上。用do-while循环配合snake.Contains来检测private void GenerateFood() { Random rnd new Random(); Point newFood; do { newFood new Point(rnd.Next(1, GameConfig.Width - 1), rnd.Next(1, GameConfig.Height - 1)); } while (snake.Contains(newFood)); food newFood; Renderer.Instance.DrawFood(food); }这段代码里还埋了一个事件——FoodEaten。别小看这个事件它是我刻意加进来的解耦设计GameManager只管发事件至于积分UI怎么刷新、要不要播放音效、要不要写日志都是订阅者的事。这正是C#上位机开发里最常用的模块解耦方式你在处理扫码枪触发、Socket收包通知UI刷新时用的也是这一套。5. 常见问题与排查技巧5.1 典型Bug和排查实录控制台贪吃蛇虽然简单但我在帮人review代码时见过不少重复的坑把这些真实问题整理成下表你写的时候对照着自查现象根本原因解决方法与心得画面疯狂闪烁每帧调用Console.Clear全量重绘改成SetCursorPosition定点绘制只更新蛇头、蛇尾、食物位置蛇可以180度掉头方向切换时没有检查当前方向统一收口到ChangeDirection方法返回前判断是否反向按键响应不灵卡顿感明显直接调用Console.ReadKey导致主循环阻塞使用Console.KeyAvailable先查询是否有输入再读取食物生成在蛇身上随机坐标只调用一次未做冲突检查使用do-while循环结合snake.Contains重新生成分数显示区域被蛇身覆盖绘制顺序错误比分和地图共用一块区域预留固定区域画UI蛇移动时避开该区域蛇加速后越来越难控制每吃一个食物就大幅减少Sleep时间用Math.Max设置最小间隔下限避免无限加速其中最经典的就是方向掉头和闪烁问题这两个几乎是每个新手必踩的坑也是面试官最爱问的“你在这个项目里遇到过什么问题”的绝佳素材。能把背后的原因和解决方案讲清楚这个项目的含金量在面试里完全够用了。5.2 从贪吃蛇到真实项目的扩展方向如果你不满足于让蛇跑起来这里给你几个思考方向每一个都对应着实际工程里的某项能力方向一加入事件驱动机制改造。把吃到食物、游戏结束、暂停恢复都改成事件发布让计分、绘制、音效都作为订阅方存在。做完这一步你就基本掌握了C#里事件和委托的实战用法后面做上位机扫码枪触发、Socket收包通知界面刷新都顺理成章。方向二把控制台换成WinForms或WPF。用Timer替换Thread.Sleep用控件事件替换Console.ReadKey。你会直观地感受到UI线程和后台任务的区别这是做任何带界面的C#项目都绕不开的知识点。方向三加入序列化和存档系统。把最高分保存到本地文件启动时读取。这一步能帮你吃透File、StreamReader/Writer或JsonSerializer这些在真实项目里几乎是每天都要用的。方向四网络版贪吃蛇。用TCP或UDP实现双人对战一个人控制一条蛇。涉及Socket编程、序列化协议、多线程这已经摸到成熟网络项目的门槛了。热词里还有像“反射”、“单例模式”、“委托事件”这种东西我的建议是贪吃蛇这个项目重点用单例和事件就够了反射这类进阶特性不要硬塞进来容易把项目搞得复杂无比。先把基础打牢后面单独练反射不迟。根据我个人经验做完贪吃蛇后我建议你顺手把它改动几次第一次改成用事件解耦第二次改成存读档第三次试着包一层简单的UI。这个过程比照着教程敲十遍代码都管用。等你把这几步都走完回头再碰“上位机开发”、“Socket通讯”这些话题你会发现那些高深概念的内核早就见过了——无非是事件循环、状态更新、界面刷新这三件事的循环往复。本文还有配套的精品资源点击获取
返回列表