用C#写贪吃蛇是练习面向对象和基础数据结构的经典入门项目。它不需要任何第三方游戏引擎,依靠控制台或者简单的WinForms界面就能跑起来。整个游戏的本质,就是在一个固定大小的网格里,控制一条由若干节点组成的蛇不断移动、吃食物、变长,同时避免撞墙或撞到自己。

一、游戏核心模型设计
贪吃蛇的游戏状态可以拆成三个部分:蛇本身、食物、以及游戏边界。蛇最好用有序的坐标集合来表达,头部在集合开头,尾部在末尾。每次移动时,根据当前方向在头部前面插入一个新坐标,如果没吃到食物就把尾部坐标删掉,这样蛇就“前进”了。用List<Point>或者Queue<Point>都能实现,List更方便随机访问做碰撞检测,Queue则天然符合先进先出的身体滑动逻辑。
食物就是一个随机生成在空白格子里的坐标。边界通常就是控制台窗口的宽高,或者自己定义的二维数组大小。下面这段代码展示了如何用List<Point>定义蛇与食物,以及基本的初始化方式。
using System;
using System.Collections.Generic;
class SnakeGame
{
// 用List保存蛇身,index 0是头
private List<Point> snake = new List<Point>();
private Point food;
private int width = 20;
private int height = 10;
public void Init()
{
// 初始蛇长度为3,横向放在中间
snake.Add(new Point(5, 5));
snake.Add(new Point(4, 5));
snake.Add(new Point(3, 5));
GenerateFood();
}
// 在空白位置随机生成食物
private void GenerateFood()
{
Random rnd = new Random();
do
{
food = new Point(rnd.Next(0, width), rnd.Next(0, height));
} while (snake.Contains(food));
}
}
二、移动与方向控制逻辑
方向控制是贪吃蛇里最容易出bug的地方。常见错误是允许玩家直接反向,比如蛇往右走时马上按左键,结果头直接撞到第二节身体。正确做法是记录一个“当前方向”枚举,并且只接受与原方向不相反的新方向输入。在每一步Tick里,根据方向算出新的头部坐标。
移动的核心步骤是:计算新头坐标,插入到List开头,判断是否吃到食物,没吃到就移除末尾。这样无论蛇多长,每一步只是增减一个节点,时间复杂度是O(1)级别(Contains判断食物除外)。下面示例展示了移动方法和方向枚举的配合。
enum Direction { Up, Down, Left, Right }
class SnakeGame
{
private Direction dir = Direction.Right;
public void ChangeDirection(Direction newDir)
{
// 禁止180度反向
if (dir == Direction.Left && newDir == Direction.Right) return;
if (dir == Direction.Right && newDir == Direction.Left) return;
if (dir == Direction.Up && newDir == Direction.Down) return;
if (dir == Direction.Down && newDir == Direction.Up) return;
dir = newDir;
}
public void Move()
{
Point head = snake[0];
Point newHead = head;
if (dir == Direction.Right) newHead.X++;
else if (dir == Direction.Left) newHead.X--;
else if (dir == Direction.Up) newHead.Y--;
else if (dir == Direction.Down) newHead.Y++;
snake.Insert(0, newHead);
if (newHead.Equals(food))
{
GenerateFood(); // 吃到食物,不删尾,蛇变长
}
else
{
snake.RemoveAt(snake.Count - 1);
}
}
}
三、碰撞检测与游戏结束判定
碰撞分为两种:撞墙和撞自己。撞墙很好判断,只要新头坐标的X小于0、大于等于width,或者Y小于0、大于等于height,就结束游戏。撞自己则是检查新头坐标是否出现在snake集合里(注意要排除还没移除的旧尾,不过上面先插入再决定是否删尾的逻辑已经自然规避了这个问题)。
把检测逻辑放在Move之后调用最合适。一旦返回true表示游戏结束,主循环就可以跳出并输出分数。下面给出具体检测方法,以及简单的控制台主循环示例。
class SnakeGame
{
public bool CheckCollision()
{
Point head = snake[0];
// 撞墙
if (head.X < 0 || head.X >= width || head.Y < 0 || head.Y >= height)
return true;
// 撞自己(从第二节开始比)
for (int i = 1; i < snake.Count; i++)
{
if (snake[i].Equals(head))
return true;
}
return false;
}
}
class Program
{
static void Main()
{
SnakeGame game = new SnakeGame();
game.Init();
while (true)
{
// 这里省略了键盘读取和方向变更代码
game.Move();
if (game.CheckCollision())
{
Console.WriteLine("游戏结束,长度:" + game.GetLength());
break;
}
System.Threading.Thread.Sleep(200);
}
}
}
四、渲染与体验优化思路
控制台版本可以用Console.SetCursorPosition把光标移到对应坐标,用不同字符画蛇头和蛇身,比每次清屏重画效率更高,也避免闪烁。如果迁移到WinForms,可以用Panel的Paint事件配合Timer,用FillRectangle画格子。食物用红色,蛇头用亮绿,身体用深绿,视觉上一眼就能分清。
进阶玩法包括:随分数提高缩短Timer间隔实现加速、记录最高分、增加障碍物层。这些都不改变上面说的核心模型,只是在Move和生成食物时多一层判断。理解了List管理蛇身、方向锁、双类型碰撞检测这三块,你写的C#贪吃蛇就已经具备可玩性,后续扩展只是堆功能而已。