简介:这份C#小游戏项目合集包含飞机大战、俄罗斯方块、贪吃蛇、拼图游戏、连连看和五子棋七款经典游戏,面向刚入门C#的编程学习者,通过完整可运行的源码与逐行注释,帮助理解面向对象设计、窗体应用、事件机制及GDI+绘图等核心知识点。资源包共1286个文件,压缩后74.9MB,其中包含92个cs源代码文件、388个xml配置、146个dll依赖库以及89张png图片素材,还带有wav和mp3音效文件、11个可直接运行的exe程序及配套的pdb调试文件,便于读者对照学习与断点调试。目前已有1280人学习下载。通过实际运行和改造这些游戏,读者可以掌握游戏循环搭建、碰撞检测、键盘鼠标交互、消除判定算法、棋盘逻辑判断等具体实现技巧,并能学会如何整合图片、声音等资源编写完整的游戏界面,对巩固C#语法、熟悉Visual Studio项目结构和提升编程兴趣都很有帮助。
1. 从语法到游戏:六个能跑起来的 C# 练手项目
很多初学者学完 C# 语法后卡在同一个地方:面向对象、委托、事件都看了,但一打开 Visual Studio 不知道写什么。这六个小游戏项目正好补上这段距离——飞机大战、俄罗斯方块、贪吃蛇、拼图、连连看、五子棋,每个都用 WinForms + GDI+ 实现,代码注释密度比一般教程代码高不少,适合拿来做「读代码 → 改代码 → 重写代码」的练习。项目里包含完整解决方案文件,用 Visual Studio 2022 直接打开就能跑。无论你是刚学完 C# 基础、准备找上位机或桌面开发相关工作,还是想理解游戏循环、碰撞检测、二维数组这些概念在真实代码里长什么样,这几百个文件都值得花两个晚上拆一遍。
2. 飞机大战与 GDI+:游戏循环、双缓冲和键盘状态
2.1 游戏循环是怎么实现的
飞机大战是整个项目里最适合先读的一个,因为它最接近真实游戏的结构:一个循环、一堆对象、每帧更新、每帧绘制。窗体上放了一个System.Windows.Forms.Timer,Interval 设在 16 或 20 毫秒,对应大约 50 到 60 FPS。每次 Tick 触发GameLoop(),里面做三件事:处理输入、更新所有对象位置、刷新画面。这个结构和你以后写 C# 上位机循环采集数据时遇到的问题是同一类——界面刷新卡顿往往就是循环里做了耗时操作,后面会具体说。
玩家飞机的移动没有用键盘事件,而是在KeyDown和KeyUp里维护一个布尔状态字典。这样做的原因是事件本身是瞬时的,如果你在KeyDown里直接改坐标,按键按住时不会连续移动,只有按下瞬间动一下。项目里的做法是:
// 键盘状态字典,KeyDown 时置 true,KeyUp 时置 false private Dictionary<Keys, bool> keyStates = new Dictionary<Keys, bool>(); private void Form1_KeyDown(object sender, KeyEventArgs e) { keyStates[e.KeyCode] = true; } private void Form1_KeyUp(object sender, KeyEventArgs e) { keyStates[e.KeyCode] = false; } // 在游戏循环中统一读取,而不是在事件里直接移动 private void UpdatePlayerPosition() { int speed = 8; if (keyStates.ContainsKey(Keys.Left) && keyStates[Keys.Left]) player.X -= speed; if (keyStates.ContainsKey(Keys.Right) && keyStates[Keys.Right]) player.X += speed; }这里的关键在于把「输入采集」和「状态更新」拆开。KeyDown只负责记录按键是否被按住,移动逻辑全部放在游戏循环里执行。speed 变量代表每帧移动的像素数,如果觉得飞机移动太快或太慢,优先调整这个值而不是改 Timer 的 Interval——因为 Interval 改动会影响所有对象的运动速度,包括敌人、子弹和背景滚动。
2.2 双缓冲和碰撞检测
如果你直接在这个项目里把 Timer 的 Interval 改成 1 毫秒,会发现画面严重闪烁。这不是 GDI+ 绘图性能不够,而是没开双缓冲。窗体设置了DoubleBuffered = true,它的原理是在内存中先创建一张与屏幕等大的画布,所有绘制操作都在这张画布上完成,绘制结束后一次性拷贝到屏幕。这个技巧在你以后写 C# 上位机自定义控件、写工业组态界面时同样适用——任何频繁Invalidate()的自绘控件闪烁,第一反应都是开双缓冲。
碰撞检测用的是最简单的轴对齐包围盒(AABB)方式,也就是判断两个矩形的范围是否相交。代码里抽取了公共方法:
// 判断两个矩形是否相交,用于子弹命中敌机、敌机撞玩家 private bool IsCollision(Rectangle a, Rectangle b) { return a.IntersectsWith(b); }Rectangle.IntersectsWith是 System.Drawing 自带的方法,它判断的是两个矩形四条边是否交叉。这个方法的判定很「粗」,适合飞机大战这种不太需要精确的场合。如果你的游戏里子弹很小、敌机形状不规则,这种做法会让人觉得「明明没打中却掉血」,那时再换成圆形碰撞检测——比较圆心距离和半径之和。
提示:项目里所有游戏都共享同一个双缓冲绘制模式,读懂飞机大战的循环结构,后面五个项目就只是数据结构和规则不同了。
3. 俄罗斯方块与贪吃蛇:数组旋转、队列和状态转移
3.1 二维数组与方块旋转的数学
俄罗斯方块是这套项目里数组运用最集中的部分。游戏主区域是一个int[rows, cols],0 表示空,非 0 表示有方块落下。每种方块保存为Point[]相对坐标集合,旋转操作就是算矩阵旋转。旋转不能改一个点就立刻写回原数组,否则会积累误差,正确做法是现将点复制到临时位置,验证旋转后是否合法(不越界、不重叠),合法才应用:
// 逆时针旋转一个方块,w 为旋转前宽度 private Point[] Rotate(Point[] cells) { int w = 4; // 方块矩阵宽度,标准方块为 4x4 Point[] rotated = new Point[cells.Length]; for (int i = 0; i < cells.Length; i++) { // 逆时针旋转公式: (x, y) -> (y, w - 1 - x) rotated[i] = new Point(cells[i].Y, w - 1 - cells[i].X); } return rotated; }旋转公式(x, y) -> (y, w - 1 - x)看起来简单,但方向容易弄反。顺时针和逆时针两种旋转会得出不同的贴墙方式,项目中用的是逆时针。写完旋转逻辑后,建议用 I 字形和 L 字形方块各测一次,重点看靠墙旋转时是否被卡住,很多俄罗斯方块实现都会有「踢墙」处理——方块靠右边时旋转会越界,常规做法是旋转后发现越界就将方块左移一列再试。
3.2 贪吃蛇的链表移动
贪吃蛇实现里最有价值的部分是它的移动方式。刚学的人会把蛇身存成数组,每次移动把每个身体段都往前挪一格。这种做法能用但低效,项目里用的是队列(Queue<Point>)思路:头部加入新位置,尾部移除一格,整个蛇身自然就「平移」了。由于 C# 的Queue没有直接访问队尾元素的方法,代码里用了计数变量记录尾巴位置:
// 蛇移动:head 是新头部坐标,tail 是当前尾部坐标 private void MoveSnake(Point head) { Queue<Point> newBody = new Queue<Point>(snakeBody); newBody.Enqueue(head); // 新头部入队 Point tail = newBody.Dequeue(); // 尾部出队 // 先判断下一步是否咬到自己,再真正更新蛇身 if (snakeBody.Contains(head) && !head.Equals(tail)) { GameOver(); return; } // 只有吃到食物时才不删除尾部,实现增长效果 if (head.Equals(foodPosition)) { newBody.Enqueue(tail); // 保留原尾部,实现蛇身加长 } snakeBody = newBody; }注意移动和吃食物的顺序。如果先判断吃食物再移动,会出现「蛇头到了食物位置但没吃到」的错位。项目中的Contains(head)判断包含了「新头部是否与旧蛇身重叠」的逻辑,但要把特殊情况排除——蛇尾巴刚好移走后新头到达这个位置,不算撞到。
3.3 游戏状态机的关键点
飞机大战、俄罗斯方块、贪吃蛇三个游戏都有暂停和结束状态,项目中用枚举GameState区分状态。这个模式建议尽快掌握,它是小型游戏最基础的结构。微信小游戏、Unity 游戏里同样采用状态机制,只是写法更复杂。状态机体系下,Timer的 Tick 事件里通常这样组织逻辑:
// 游戏循环的骨架,任何游戏都适用 private void GameTimer_Tick(object sender, EventArgs e) { switch (gameState) { case GameState.Running: Update(); CheckCollision(); Render(); break; case GameState.Paused: // 只绘制暂停画面,不执行 Update RenderPauseScreen(); break; case GameState.GameOver: // 停止移动,等待用户按键重新开始 break; } }Update()里更新坐标和逻辑,Render()里画图,二者分离。很多初学者把绘制和更新混在同一个方法里,导致调试时不知道是位置变了没画出来,还是画了但位置没变。
4. 拼图、连连看与五子棋:洗牌算法、路径搜索和评估函数
4.1 拼图的洗牌与可解性
拼图解谜的实现有细节值得拆开看。它采用的是一维数组存储图片索引,再按网格尺寸换算成拼图块坐标。这张图在初始化时会打乱顺序,但「随机打乱」和「可解的打乱」不是一回事。如果只是用Random.Shuffle把数字随便打乱,大约一半的布局是永远拼不回来的。某些项目不处理,只是闷头开始游戏,用户拼到最后发现无解,体验很差。
统计上,判断拼图是否可解要看逆序数,再加上空格所在的奇偶行:
// 判断 3x3 或 4x4 拼图是否可解 private bool IsSolvable(int[] tiles, int colCount) { int inversions = 0; for (int i = 0; i < tiles.Length - 1; i++) { if (tiles[i] == 0) continue; // 0 表示空白块 for (int j = i + 1; j < tiles.Length; j++) { if (tiles[j] == 0) continue; if (tiles[i] > tiles[j]) inversions++; } } // 空白行从底部数,colCount 为奇数时只看逆序数 int blankRow = Array.IndexOf(tiles, 0) / colCount; bool isOddCols = (colCount % 2 == 1); if (isOddCols) return (inversions % 2 == 0); // 偶数列时逆序数奇偶与空格所在行奇偶需相反 return (inversions + colCount - blankRow) % 2 == 1; }这段代码在初学时会觉得是「数学题」,但它教会一个更重要的工程习惯:校验输入比处理错误更值得花时间。拼图无解是逻辑问题不是异常问题,运行时无法恢复。
4.2 连连看的路径判定:最多拐两次弯
连连看是这个项目里算法含量最高的一个。它的核心是一道搜索题:两个格子能否消除,取决于它们之间是否存在一条路径——这条路径最多经过两个拐点、且整条路径上不能有其他图案。判断两个格子是否能连通的代码可以读一读:
// 简化版检测:横向/纵向/单拐/双拐路径 private bool CanConnect(Point p1, Point p2) { // 1. 先看同一行或同一列是否直连(无障碍) if (p1.X == p2.X || p1.Y == p2.Y) { if (IsLineEmpty(p1, p2)) return true; } // 2. 单拐点:用 (p1.X, p2.Y) 或 (p2.X, p1.Y) 做中间点 if (IsLineEmpty(p1, new Point(p1.X, p2.Y)) && IsLineEmpty(new Point(p1.X, p2.Y), p2) && IsEmptyCell(new Point(p1.X, p2.Y))) { return true; } // 3. 双拐点:遍历每一行/列作为中间线,用两个中间点连接 // (遍历二维数组的行和列,找到两条线段和一条中间线都为空的情况) return HasTwoCornerPath(p1, p2); }IsLineEmpty的实现就是沿直线从起点扫描到终点,遇到非空格子就返回 false。双拐的情况则要遍历棋盘上每条空白行和列,把两个单拐路径组合起来。这段代码是逐步优化的好素材——先实现直连和单拐,再加双拐,每加一档逻辑都要重新验证之前能通过的情况。
4.3 五子棋的 AI:评分表和简单贪心
五子棋的 AI 没有用复杂算法,而是一个经典的贪心评分策略。每次轮到电脑时,遍历每个空位,分别计算该位置对己方和对方的「价值」:在横向、纵向、两条对角线四个方向上,统计以该位置为中心向两端连续同色棋子的数量,再按连子数给分。这个项目里最有学习价值的是评分表的设计:
| 棋型 | 示例 | 评分 |
|---|---|---|
| 活四 | _OOOO_ | 100000 |
| 冲四 | _OOO_X或XOOO_ | 10000 |
| 活三 | _OOO_(两边空) | 5000 |
| 眠三 | _OO_OX或XOO_O_ | 1000 |
| 活二 | _OO_(至少一端有 2 空) | 500 |
电脑按「防守优先」还是「进攻优先」取决于这个分数怎么组合使用。项目里常见的写法是「取空位中己方最高分与对方最高分之和最大的位置」,这样既不会只进攻不顾防守,也不会只会堵别人。如果你想改造成更有攻击性的 AI,可以把权重调成己方分数 × 1.1 再比较,但别调太高,否则会变成无限冲四的「搅局流」。
5. 把这六个项目真正变成自己的代码
5.1 从哪里开始读最不劝退
面对六个项目加一堆.cs文件,不要按顺序一个个读。我建议按这个顺序:飞机大战 → 贪吃蛇 → 拼图 → 俄罗斯方块 → 连连看 → 五子棋。飞机大战帮你建立「循环和事件」模型,贪吃蛇帮你理解「数据驱动表现层」,拼图让你认识到「初始化也有合法性」,俄罗斯方块是算法密集区适合单独攻读,连连看的路径搜索是面试级难度,五子棋 AI 可以放在最后当加分项。
读每个项目时先找入口:Program.cs里的Application.Run或者 WinForms 的Main方法,接着找到主窗体的构造函数和 Timer 事件。项目中代码注释非常详细,很多关键步骤上方都用中文写清楚了「这里的作用是什么」「为什么要这样写」,按注释行往下追逻辑线即可。
5.2 改一个数字,看一个全局影响
初学者最容易犯的错是「看懂逻辑」却从不验证自己的理解。主动改几个参数来验证理解:
- 把飞机大战的
speed从 8 改成 40,观察碰撞判定是否会变困难——你会发现飞机「瞬移」式移动时,每帧移动距离大于子弹宽度,会出现子弹「穿过」飞机的视觉效果,这是快速移动物体共同的检测盲区。 - 把贪吃蛇的 Timer Interval 从 100 改成 300,区分「速度慢」正确做法是调整 Interval 而不是在
MoveSnake里加Thread.Sleep。 - 把拼图从 4x4 改成 3x3,
IsSolvable里的奇偶逻辑是否仍然生效。 - 把五子棋的活三和活二评分互换,观察 AI 的走棋风格变化。
每个改动只用一行,但理解深度差很多。
5.3 封装一个自己的 GameBase 类
读完全部六个项目后,你可以尝试把它们共性的东西抽出来。举一个可以从飞机大战开始做的「窗体基类」:所有游戏都有初始化、更新、绘制、暂停、结束五件事,你可以在System.Windows.Forms.Form上定义这几个虚方法,然后让各个游戏的窗体继承它。等你真的动手重构时,会发现「为什么原来要把循环和绘制拆开」这个问题,答案在自己写代码时逐渐就清楚了。
这个项目最有价值的地方在于:六个游戏覆盖了 GDI+ 绘制、键盘事件、数组状态维护、简单搜索算法、贪心策略这些桌面开发常见知识点。能把每个项目的注释读懂并改出一版自己的玩法,C# 的面向对象和常用集合类基本就能算掌握了,后续再去接触 WPF、Unity 或者上位机开发,不会再有「看不懂别人的代码结构」那种卡顿感。
本文还有配套的精品资源,点击获取