ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#小游戏实战:从贪吃蛇到五子棋,掌握游戏循环与状态管理

C#小游戏实战:从贪吃蛇到五子棋,掌握游戏循环与状态管理 简介这是一套面向C#初学者的实战练手项目合集包含飞机大战、俄罗斯方块、贪吃蛇、拼图游戏、连连看、五子棋等七款经典小游戏适合刚接触C#、希望通过完整项目理解面向对象编程、事件驱动与WinForms界面开发的入门者。压缩包共1286个文件约74.9MB以92个cs源码文件为核心辅以xml配置、dll依赖库、png/jpg/ico图片素材、wav与mp3音效、resx资源文件及sln/csproj工程文件构成可直接编译运行的完整解决方案。目前已有1284人学习下载说明其内容经过一定规模的实践检验。七款游戏覆盖了窗体绘制、键盘与鼠标交互、定时器循环、碰撞检测、数组与集合操作、简单算法如五子棋胜负判断、连连看路径查找等常见知识点注释详细便于边读边改。读者可借此熟悉Visual Studio工程结构、资源管理与调试流程并在此基础上二次开发或扩展关卡、音效与难度设置是C#入门阶段较为系统的练习素材。1. 从七个 C# 小游戏入手初学者练手的真实价值在哪很多人学 C# 卡在语法都会、项目不会。变量、循环、类、继承背得滚瓜烂熟打开 Visual Studio 却不知道从哪写第一行。这七个 C# 小游戏项目——飞机大战、俄罗斯方块、贪吃蛇、拼图游戏、连连看、五子棋——恰好是打破这个僵局的东西。它们不是玩具每一个都逼你面对真实开发里绕不开的问题游戏循环怎么跑、碰撞怎么判、状态怎么存、界面怎么刷新。注释详细意味着你读代码时不用猜作者意图能直接看到「这一行在干什么、为什么这么写」。适合谁刚学完 C# 基础语法、想找一个能跑起来、能改得动、能讲得清的项目练手的人。下面按「先跑通一个、再拆解套路、最后避坑」的顺序把这条路走一遍。2. 先跑通贪吃蛇一个最小可运行 C# 游戏项目的完整拆解2.1 为什么选贪吃蛇作为第一个动手项目七个项目里贪吃蛇的依赖最少、逻辑闭环最短。它不需要图片资源不需要复杂物理一个二维网格加一个定时器就能跑。飞机大战要处理子弹和敌机生成俄罗斯方块要处理旋转矩阵拼图要处理图片切割连连看要处理路径搜索五子棋要处理胜负判定——这些都比贪吃蛇多一层。先跑通贪吃蛇你能拿到三样东西一个能运行的 C# 窗体程序、一套游戏循环的写法、一个可以往里加功能的骨架。常见做法是用 WinForms 的Timer驱动刷新用Panel或直接重写OnPaint画格子。也有用控制台做的版本但窗体版更接近后面几个项目的结构。我一般建议从窗体版入手因为飞机大战、俄罗斯方块都是窗体程序提前熟悉Graphics、Invalidate、KeyDown这些 API后面迁移成本低。2.2 贪吃蛇的核心数据结构与移动逻辑贪吃蛇的本质是一个队列。蛇头每次移动往队列头部加一个新坐标如果没吃到食物就把队列尾部去掉一个。吃到食物就不去尾蛇变长。这个逻辑用QueuePoint或者ListPoint都能实现但Queue更贴合语义。// 蛇身用队列存储队首是蛇头 private QueuePoint snakeBody new QueuePoint(); private Point food; private Direction currentDir Direction.Right; private int cellSize 20; // 每个格子20像素 // 移动一步 private void MoveSnake() { // 根据当前方向计算新蛇头坐标 Point head snakeBody.Peek(); Point newHead new Point(head.X, head.Y); switch (currentDir) { case Direction.Up: newHead.Y - 1; break; case Direction.Down: newHead.Y 1; break; case Direction.Left: newHead.X - 1; break; case Direction.Right: newHead.X 1; break; } // 撞墙或撞自己游戏结束 if (newHead.X 0 || newHead.X gridWidth || newHead.Y 0 || newHead.Y gridHeight || snakeBody.Contains(newHead)) { GameOver(); return; } snakeBody.Enqueue(newHead); // 新蛇头入队 if (newHead food) { GenerateFood(); // 吃到食物不去尾蛇变长 } else { snakeBody.Dequeue(); // 没吃到去尾 } }这段代码里QueuePoint的Enqueue和Dequeue正好对应蛇的生长和移动。gridWidth、gridHeight控制边界cellSize控制绘制尺寸。方向切换要注意不能直接反向比如向右走时按左键要忽略否则蛇头撞自己脖子。这个判断通常放在KeyDown事件里private void Form1_KeyDown(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Up: if (currentDir ! Direction.Down) currentDir Direction.Up; break; case Keys.Down: if (currentDir ! Direction.Up) currentDir Direction.Down; break; case Keys.Left: if (currentDir ! Direction.Right) currentDir Direction.Left; break; case Keys.Right: if (currentDir ! Direction.Left) currentDir Direction.Right; break; } }参数说明cellSize决定画面精细度20 像素在 400x400 窗口里是 20x20 网格改小格子更多但蛇移动视觉更快。Timer.Interval控制速度设 150 毫秒比较适合初学者设 80 毫秒就有挑战性了。GenerateFood要随机一个不在蛇身上的坐标简单做法是循环随机直到不冲突网格小的时候效率够用。2.3 从贪吃蛇迁移到俄罗斯方块和飞机大战贪吃蛇跑通后你会发现三个项目共享同一套骨架一个Timer驱动Tick事件在Tick里更新游戏状态然后Invalidate()触发OnPaint重绘。俄罗斯方块把「蛇身队列」换成「7 种方块形状的二维数组」把「移动一步」换成「下落一行」把「吃食物」换成「消行」。飞机大战把「蛇头」换成「玩家飞机」把「食物」换成「敌机生成」把「撞自己」换成「碰撞检测」。// 俄罗斯方块用二维数组表示当前方块 private int[,] currentPiece new int[4, 4]; private int pieceX, pieceY; // 下落一行 private void MoveDown() { if (!IsCollision(pieceX, pieceY 1)) { pieceY; } else { LockPiece(); // 固定到棋盘 ClearLines(); // 检查消行 SpawnNewPiece(); } } // 碰撞检测遍历方块占用的格子 private bool IsCollision(int x, int y) { for (int i 0; i 4; i) for (int j 0; j 4; j) if (currentPiece[i, j] ! 0) { int boardX x j; int boardY y i; if (boardX 0 || boardX 10 || boardY 20) return true; if (boardY 0 board[boardY, boardX] ! 0) return true; } return false; }board是 10x20 的二维数组0 表示空非 0 表示已固定方块。IsCollision同时检查边界和已有方块这是俄罗斯方块最核心的函数。飞机大战的碰撞检测更简单用矩形相交判断// 飞机大战矩形碰撞检测 private bool IsHit(Rectangle a, Rectangle b) { return a.IntersectsWith(b); } // 在 Tick 里遍历子弹和敌机 foreach (var bullet in bullets) { foreach (var enemy in enemies) { if (IsHit(bullet.Bounds, enemy.Bounds)) { enemy.Health--; bullet.IsActive false; if (enemy.Health 0) enemiesToRemove.Add(enemy); } } }注意遍历时不要直接删除集合元素用「标记删除 事后清理」的方式否则会抛InvalidOperationException。这是初学者最容易翻车的地方之一。3. 拼图、连连看、五子棋三个项目各自的技术门槛与实现要点3.1 拼图游戏图片切割与随机打乱拼图游戏的技术点集中在两处把一张图切成 NxN 小块以及打乱顺序后判断是否还原。切割用Bitmap.Clone指定矩形区域// 把原图切成 rows x cols 个小块 private Bitmap[,] SplitImage(Bitmap source, int rows, int cols) { int pieceW source.Width / cols; int pieceH source.Height / rows; Bitmap[,] pieces new Bitmap[rows, cols]; for (int r 0; r rows; r) for (int c 0; c cols; c) { Rectangle rect new Rectangle(c * pieceW, r * pieceH, pieceW, pieceH); pieces[r, c] source.Clone(rect, source.PixelFormat); } return pieces; }打乱时不能完全随机否则可能出现无解局面。常见做法是从还原状态出发随机做若干次合法移动这样保证一定有解。判断还原就是逐格比对当前顺序和目标顺序。参数上3x3 适合入门4x4 开始有难度5x5 以上对图片分辨率有要求建议原图至少 500x500 像素。3.2 连连看路径搜索与消除判定连连看的核心是判断两个相同图案之间是否存在最多拐两个弯的通路。这是典型的广度优先搜索变体。实现时把棋盘抽象成二维数组0 表示空非 0 表示图案编号。搜索时分别检查直线连通、一个拐点、两个拐点三种情况。// 检查两点是否在同行或同列且中间无障碍 private bool IsLineClear(Point a, Point b) { if (a.X b.X) { int minY Math.Min(a.Y, b.Y); int maxY Math.Max(a.Y, b.Y); for (int y minY 1; y maxY; y) if (board[y, a.X] ! 0) return false; return true; } if (a.Y b.Y) { int minX Math.Min(a.X, b.X); int maxX Math.Max(a.X, b.X); for (int x minX 1; x maxX; x) if (board[a.Y, x] ! 0) return false; return true; } return false; } // 一个拐点检查 (a.X, b.Y) 和 (b.X, a.Y) 两个候选拐点 private bool CanConnectOneCorner(Point a, Point b) { Point corner1 new Point(a.X, b.Y); Point corner2 new Point(b.X, a.Y); if (board[corner1.Y, corner1.X] 0 IsLineClear(a, corner1) IsLineClear(corner1, b)) return true; if (board[corner2.Y, corner2.X] 0 IsLineClear(a, corner2) IsLineClear(corner2, b)) return true; return false; }两个拐点的判断更绕需要沿着某一行或某一列扫描看是否存在一个中间点使得 a 到中间点、中间点到 b 都能用零拐点或一拐点连通。实际写的时候可以枚举 a 所在行和列上的所有空位作为候选中间点。连连看最容易出的 bug 是边界处理——棋盘外围要留一圈空位否则贴边的图案无法连通。3.3 五子棋胜负判定与 AI 落子五子棋的胜负判定比看起来简单每落一子只检查经过这个点的四个方向横、竖、左斜、右斜是否有连续五个同色。不需要全盘扫描。// 检查落子后是否五连 private bool CheckWin(int x, int y, int player) { // 四个方向横、竖、主对角线、副对角线 int[][] dirs new int[][] { new int[] {1, 0}, // 横 new int[] {0, 1}, // 竖 new int[] {1, 1}, // 主对角线 new int[] {1, -1} // 副对角线 }; foreach (var dir in dirs) { int count 1; // 当前子算一个 // 正方向延伸 for (int step 1; step 5; step) { int nx x dir[0] * step; int ny y dir[1] * step; if (nx 0 || nx 15 || ny 0 || ny 15) break; if (board[nx, ny] ! player) break; count; } // 反方向延伸 for (int step 1; step 5; step) { int nx x - dir[0] * step; int ny y - dir[1] * step; if (nx 0 || nx 15 || ny 0 || ny 15) break; if (board[nx, ny] ! player) break; count; } if (count 5) return true; } return false; }如果要加简单 AI用评分表就行对每个空位分别计算如果下黑子和下白子的分值取最大值。分值规则比如「活三」给 100 分「冲四」给 1000 分「活四」给 10000 分。这个评分函数不需要搜索树初学者也能写出来效果比随机落子好得多。棋盘大小建议 15x15这是标准五子棋尺寸board用int[15,15]0 空、1 黑、2 白。4. 七个项目共用的 C# 基础委托、事件与集合选型4.1 委托和事件在游戏循环里的实际用法七个项目里窗体程序都靠事件驱动。Timer.Tick是事件KeyDown是事件Paint也是事件。理解委托和事件才能看懂为什么timer.Tick new EventHandler(GameLoop)能跑起来。委托是方法的引用事件是委托的封装。在游戏里你不需要自己定义委托但需要知道是在往事件列表里加处理方法。// 典型的事件订阅 private Timer gameTimer; private void InitGame() { gameTimer new Timer(); gameTimer.Interval 150; gameTimer.Tick GameLoop; // 订阅Tick事件 gameTimer.Start(); } private void GameLoop(object sender, EventArgs e) { UpdateGameState(); // 更新逻辑 this.Invalidate(); // 触发重绘 }Invalidate()会触发OnPaint在OnPaint里用Graphics画所有元素。这个「更新逻辑 → 请求重绘 → 绘制」的循环是所有窗体游戏的基础。不要直接在Tick里画图那样画面会闪烁。4.2 数组和集合怎么选从蛇身到棋盘C# 里数组和集合的区别在游戏项目里体现得很直接。棋盘、方块形状这种大小固定的用数组int[,] board new int[15,15]。蛇身、子弹列表、敌机列表这种数量变化的用ListT或QueueT。数组访问快但长度固定集合灵活但有额外开销。场景推荐类型原因五子棋棋盘int[15,15]大小固定频繁随机访问俄罗斯方块棋盘int[20,10]同上且需要按行列遍历贪吃蛇蛇身QueuePoint频繁头尾操作语义匹配子弹/敌机列表ListBullet数量动态变化需要遍历和删除拼图小块Bitmap[,]二维固定结构按行列索引用ListT删除元素时不要在foreach里直接Remove。要么用for倒序遍历要么用RemoveAll配合条件要么标记后统一清理。这是血泪经验翻车一次就记住了。5. 避坑与排查七个项目里最容易卡住的五个地方5.1 画面闪烁严重拖动窗口后花屏现象游戏运行时画面不停闪拖动窗口后出现残影或花屏。原因直接在Tick里用CreateGraphics()画图或者没有开启双缓冲。解决重写OnPaint用e.Graphics绘制在窗体构造函数里设置this.DoubleBuffered true;或者SetStyle(ControlStyles.OptimizedDoubleBuffer, true);。双缓冲是 WinForms 游戏必开项不开的话画面基本没法看。5.2 按键响应迟钝或方向切换不灵现象按方向键后蛇要等一会才转向或者快速连按会漏掉按键。原因KeyDown事件受系统按键重复延迟影响且Timer.Interval太长时两次逻辑更新之间只记录最后一次按键。解决在KeyDown里只记录方向状态实际移动在Tick里根据记录的方向执行。另外把Timer.Interval降到 100 毫秒以下响应会明显改善。如果还不行检查窗体是否设置了KeyPreview true。5.3 集合遍历时删除元素抛异常现象飞机大战里子弹击中敌机后程序抛InvalidOperationException: Collection was modified。原因在foreach遍历ListT的过程中调用了Remove。解决用倒序for循环删除或者把要删除的元素先标记IsActive false遍历结束后用RemoveAll(x !x.IsActive)统一清理。这个坑在子弹、敌机、消除的拼图块上都会遇到。5.4 图片资源路径不对导致拼图空白现象拼图游戏运行后格子全是空白或红叉。原因用了绝对路径D:\images\puzzle.jpg换台机器就找不到或者图片没有复制到输出目录。解决把图片放进项目在属性里设置「复制到输出目录」为「始终复制」代码里用Path.Combine(Application.StartupPath, puzzle.jpg)拼路径。不要写死盘符。5.5 连连看贴边图案无法消除现象棋盘最外圈的图案明明相同却连不上。原因路径搜索时没有在棋盘外围留空位导致贴边图案没有绕行空间。解决棋盘数组比实际显示区域大一圈比如显示 10x10 就用int[12,12]最外圈始终为 0。搜索路径时允许经过外圈空位。这个设计在写连连看之前就要定好事后改很麻烦。6. 从能跑到好用给七个项目加一个统一的状态管理七个项目各自跑通后你会发现它们都在重复同一件事管理游戏状态。开始、进行中、暂停、结束每个项目都要写一遍。与其复制粘贴不如抽一个简单的状态机出来。不用上什么框架一个枚举加一个switch就够。// 游戏状态枚举 public enum GameState { Ready, // 准备开始 Running, // 进行中 Paused, // 暂停 GameOver // 结束 } // 在游戏主类里维护状态 private GameState currentState GameState.Ready; // Tick里根据状态决定行为 private void GameLoop(object sender, EventArgs e) { switch (currentState) { case GameState.Ready: // 等待玩家按键开始 break; case GameState.Running: UpdateGameState(); this.Invalidate(); break; case GameState.Paused: // 不更新逻辑但可以重绘暂停提示 this.Invalidate(); break; case GameState.GameOver: gameTimer.Stop(); this.Invalidate(); // 画结束画面 break; } } // 按键处理里切换状态 private void Form1_KeyDown(object sender, KeyEventArgs e) { if (currentState GameState.Ready e.KeyCode Keys.Space) { currentState GameState.Running; gameTimer.Start(); } else if (currentState GameState.Running e.KeyCode Keys.P) { currentState GameState.Paused; } else if (currentState GameState.Paused e.KeyCode Keys.P) { currentState GameState.Running; } // 其他按键处理... }这个状态机加到贪吃蛇里按空格开始、按 P 暂停、撞墙后显示「Game Over」并停止计时器。同样的结构复制到俄罗斯方块、飞机大战只需要改UpdateGameState里的内容。拼图、连连看、五子棋没有实时循环但也可以用Ready、Running、GameOver三个状态控制「是否接受鼠标点击」。验证方法很直接每个项目跑起来后依次测试开始、暂停、恢复、结束四个操作看状态切换是否正常计时器有没有在暂停时停止结束后能不能重新开始。如果暂停时蛇还在动说明gameTimer.Stop()没调用或者调错了地方。我自己的习惯是每写一个新游戏先把状态机搭好再写具体逻辑。这样后面加「重新开始」按钮、加「难度选择」、加「得分记录」都有地方挂。七个项目全部套用同一个状态机后代码结构会整齐很多回头改任何一个项目都能快速定位。希望帮到你。本文还有配套的精品资源点击获取
返回列表