ARTICLE DETAIL

资讯详情

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

C# Winform 小鸟过管道小游戏源码:从游戏循环到碰撞检测的完整实现

C# Winform 小鸟过管道小游戏源码:从游戏循环到碰撞检测的完整实现 简介这是一份面向C#初学者与Winform练手者的完整小游戏项目源码以经典“小鸟过管道”玩法为核心帮助读者在真实窗体应用中理解游戏循环、碰撞检测与事件驱动编程。项目通过键盘空格键或鼠标左键控制小鸟飞行上下管道随机生成触碰即失败每通过一个管道积一分并配有酷炫音效界面风格干净整洁。压缩包共114个文件约5.45MB包含20个cs源码文件、21张png图片、18个wav音效、6个gif动图及sln、csproj等工程文件资源与代码分层清晰。已有558人学习下载。源码注释完整、代码规范、结构独立可直接用VS2022打开运行也可点击EXE体验适合作为课程设计、毕业设计或自学C#窗体开发的参考案例便于快速搭建属于自己的小游戏程序。1. 从一份 C# Winform 小鸟过管道小游戏源码说起它到底能跑出什么很多人第一次搜「C# Winform 小鸟过管道小游戏源码」心里想的其实不是做游戏而是想找一个能跑、能改、能看懂的小项目把 Winform 的绘图、定时器、键盘事件、碰撞检测这几块串起来。市面上的小游戏源码要么是 Unity 工程要么是 Python 小游戏纯 Winform 的完整可编译版本反而不多。这份源码的价值就在这它不依赖任何第三方引擎只用 .NET Framework 自带的 GDI 和 Timer一个 Form 加几个类就能跑起来适合刚学完 C# 基础语法、想找一个「能看见画面动起来」的练手项目的人。它解决的问题很具体让你理解一个 2D 游戏循环在 Winform 里到底怎么落地。管道怎么生成、小鸟怎么受重力下落、按键怎么给一个向上的冲量、什么时候判定碰撞、分数怎么累加、游戏结束后怎么重开这些在游戏引擎里是内置能力在 Winform 里全要自己写。适合谁适合已经会写 Winform 窗体、拖过按钮、写过 Button_Click但没做过实时刷新画面的人。如果你连Application.Run和Paint事件都没用过建议先把这两块补上再往下看。2. 把游戏循环塞进 WinformTimer、双缓冲与重绘的取舍2.1 为什么不用 while 循环而用 Timer 驱动在控制台里写游戏习惯是while(true)里更新状态再打印。但 Winform 是消息驱动的主线程被while占住窗体就直接卡死连关闭按钮都点不动。常见做法是用System.Windows.Forms.Timer每隔固定毫秒触发一次Tick在Tick里更新所有对象的位置然后调用Invalidate()请求重绘。这里有个参数必须说清楚Interval。设成 16 大约是 60 帧设成 33 大约是 30 帧。新手容易把它设成 1以为越快越流畅结果 CPU 直接拉满画面反而因为重绘太频繁而闪烁。我一般用 16 到 20 之间兼顾流畅和 CPU 占用。// 游戏主定时器Interval16 约等于 60FPS private Timer gameTimer; private void InitTimer() { gameTimer new Timer(); gameTimer.Interval 16; // 单位毫秒16ms≈60帧 gameTimer.Tick GameLoop; // 每帧调用一次 gameTimer.Start(); } private void GameLoop(object sender, EventArgs e) { UpdateGame(); // 先更新逻辑重力、位移、碰撞 this.Invalidate(); // 再请求重绘触发 OnPaint }逻辑说明Tick事件里先做状态更新再做重绘请求顺序不能反。如果先Invalidate再更新画出来的是上一帧的旧状态视觉上会慢一拍。参数说明Interval越小帧率越高但 GDI 的绘制能力有限低于 10ms 基本是浪费Invalidate()不带参数是重绘整个窗体如果画面元素多可以传矩形区域只重绘脏区这是后期优化的点。2.2 双缓冲不设它画面会闪到你怀疑人生Winform 默认的绘制流程是「收到重绘消息 → 触发 OnPaint → 直接在屏幕 DC 上画」。每帧都直接画屏幕就会出现先擦背景再画前景的中间态肉眼看到的就是闪烁。解决办法是开启双缓冲让所有绘制先在一张内存位图上完成最后一次性贴到屏幕。有两种开法。一种是在窗体构造函数里设DoubleBuffered true简单但只对窗体自身生效。另一种是自定义一个继承Panel的控件在构造函数里设SetStyle把AllPaintingInWmPaint、UserPaint、OptimizedDoubleBuffer三个样式一起打开。我一般用后者因为游戏区域通常是一个独立的 Panel而不是整个窗体。public class GamePanel : Panel { public GamePanel() { // 开启双缓冲三件套缺一个都可能闪 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); this.UpdateStyles(); } }逻辑说明AllPaintingInWmPaint告诉控件忽略系统背景擦除消息UserPaint表示由我们自己负责绘制OptimizedDoubleBuffer启用内存缓冲。三个一起开闪烁基本消失。参数说明这段代码放在控件构造函数里UpdateStyles()要在设置之后立即调用否则样式不会马上生效。如果你用的是现成的Panel而不想新建类退而求其次在窗体上设DoubleBuffered true也能缓解但效果不如自定义控件彻底。3. 小鸟、管道、碰撞三个核心对象的建模与参数3.1 小鸟的物理模型重力、冲量与速度上限小鸟的运动本质是一维竖直方向的匀加速运动。每帧做两件事速度加上重力加速度位置加上速度。按键时给速度一个负值向上这就是冲量。听起来简单但参数没调好手感会非常「玄学」——要么轻飘飘像在月球要么重得像铅球。我一般用这几个初始值起步重力gravity 0.5像素/帧²按键冲量jumpImpulse -8像素/帧速度上限maxFallSpeed 12。注意这里的单位是「每帧」而不是「每秒」因为更新是按帧走的。如果你把 Timer 的 Interval 改了这些值要跟着调否则手感全变。public class Bird { public float X, Y; // 位置 public float Velocity; // 当前竖直速度 public const float Gravity 0.5f; // 每帧重力加速度 public const float JumpImpulse -8f; // 按键瞬间给的速度 public const float MaxFallSpeed 12f; // 下落速度上限 public void Update() { Velocity Gravity; // 重力持续作用 if (Velocity MaxFallSpeed) // 限制下落速度 Velocity MaxFallSpeed; Y Velocity; // 更新位置 } public void Jump() { Velocity JumpImpulse; // 直接覆盖不是累加 } }逻辑说明Jump里是直接赋值而不是Velocity 这是关键。如果写成累加连按会越飞越高游戏就没难度了。参数说明Gravity越大下落越快JumpImpulse的绝对值越大跳得越高两者要配合调。MaxFallSpeed是防止小鸟下落太快直接穿过管道这个上限在碰撞检测里也有意义。调参时建议先把重力固定只调冲量找到「按一下能上升约一个管道间隙」的感觉。3.2 管道的生成节奏与间隙参数管道是成对出现的上下各一根中间留一个间隙让小鸟穿过。管道的难点不在画而在生成节奏和间隙大小。生成太快玩家来不及反应间隙太小容错率低到劝退。常见做法是用一个列表存所有管道每帧让它们向左移动移出屏幕的移除同时在右侧按固定间隔生成新的。关键参数有三个管道宽度、间隙高度、生成间隔。我一般用宽度 60、间隙 150、水平间隔 220像素。间隙 150 是相对小鸟高度约 30 来说的留了足够容错。public class PipePair { public float X; // 管道左边缘横坐标 public int GapY; // 间隙上边缘的纵坐标 public const int Width 60; // 管道宽度 public const int GapHeight 150;// 间隙高度 public void MoveLeft(float speed) { X - speed; // 每帧左移 } // 判断小鸟是否撞上这对管道 public bool IsHit(Bird bird, int birdW, int birdH) { // 水平方向没重叠直接不算碰撞 if (bird.X birdW X || bird.X X Width) return false; // 竖直方向在间隙外就算撞 return bird.Y GapY || bird.Y birdH GapY GapHeight; } }逻辑说明碰撞检测先做水平判断再做竖直判断这是 AABB轴对齐包围盒的典型写法。水平不重叠就直接返回 false省掉后续计算。参数说明GapY是随机的生成时要在「顶部留白」和「底部留白」之间取一个范围避免间隙贴到屏幕边缘导致无法通过。Width和GapHeight一旦定下碰撞逻辑和绘制逻辑都要用同一组常量不要一处写 60 一处写 50这是血泪经验。3.3 分数与状态机别让游戏逻辑散落在各处一个能跑的游戏至少有三个状态准备、进行中、结束。新手常犯的错是把这些状态用一堆 bool 变量表示isRunning、isGameOver、isStarted混在一起改一处漏一处。正确做法是用一个枚举做状态机所有逻辑根据当前状态分支。public enum GameState { Ready, Playing, GameOver } private GameState state GameState.Ready; private int score 0; private void UpdateGame() { switch (state) { case GameState.Ready: // 等待按键小鸟原地浮动 break; case GameState.Playing: bird.Update(); UpdatePipes(); CheckScore(); CheckCollision(); break; case GameState.GameOver: // 停止更新等待重开 break; } }逻辑说明分数累加放在CheckScore里判断小鸟是否越过了某对管道的右边缘且尚未计分用一个Scored标记防止重复加分。参数说明状态切换的触发点要明确——Ready到Playing是第一次按键Playing到GameOver是碰撞或出界GameOver到Ready是再次按键。把这三个转换点写清楚逻辑就不会乱。4. 避坑与排查Winform 小游戏最容易翻车的五个地方4.1 画面闪烁严重双缓冲开了也没用现象设置了DoubleBuffered true画面还是闪。原因绘制代码里调用了this.BackColor或Graphics.Clear去擦背景而擦背景这个动作本身就在屏幕 DC 上发生了。解决把背景绘制也放进OnPaint里用e.Graphics.Clear(Color.SkyBlue)在内存缓冲上擦而不是在Tick里改BackColor。另外确认AllPaintingInWmPaint确实生效了。4.2 按键没反应或者长按只跳一次现象按空格小鸟不跳或者按住不放只跳一下。原因Winform 的KeyDown有系统级的重复延迟而且焦点可能在别的控件上。解决把窗体的KeyPreview true打开让窗体优先接收按键跳跃逻辑放在KeyDown里但用一个isKeyPressed标记在KeyUp里复位避免系统重复触发。如果要做「按住连跳」就在Update里检查标记而不是依赖KeyDown的重复。4.3 管道穿模小鸟高速下落时直接穿过管道现象小鸟下落快的时候明明该撞上管道却直接穿过去了。原因碰撞检测只在每帧做一次如果一帧内位移超过管道宽度就会「跳过」碰撞区。解决两个办法一是限制MaxFallSpeed让单帧位移小于管道宽度二是做连续碰撞检测把这一帧的位移拆成几个小步分别检测。小游戏里用前者就够了简单可靠。4.4 分数重复累加一次穿过加好几分现象穿过一对管道分数从 0 直接跳到 3 或 4。原因CheckScore每帧都在判断「小鸟是否在管道右侧」没有标记是否已经计过分。解决给每对管道加一个Scored布尔字段计分后置为 true下次判断时跳过。这个坑几乎每个人都会踩一次。4.5 游戏结束后 Timer 还在跑CPU 占用不降现象游戏结束画面停住了但任务管理器里 CPU 还是很高。原因gameTimer没有停止Tick还在触发只是状态机让它不更新逻辑但Invalidate可能还在调用。解决进入GameOver状态时调用gameTimer.Stop()重开时再Start()。或者在GameLoop开头判断状态非Playing直接 return连重绘都省掉。5. 从能跑到好用三个让这份源码更值得改的进阶技巧5.1 用资源文件替换纯色矩形画面立刻不一样纯色矩形画出来的小鸟和管道能跑但不好看。Winform 支持把 PNG 放进资源文件用Properties.Resources.xxx直接取。把小鸟换成一张带透明通道的 PNG管道换成带纹理的图绘制时用e.Graphics.DrawImage替代FillRectangle视觉提升是立竿见影的。注意图片尺寸要和碰撞盒对齐否则会出现「看着没撞上却判定撞了」的错觉。我一般让碰撞盒比图片略小一圈手感更宽容。5.2 用帧计数做难度曲线而不是改 Timer想让游戏越玩越难新手容易去改Timer.Interval结果帧率一变所有物理参数全乱。正确做法是保持帧率不变改管道移动速度和生成间隔。用一个帧计数器每过 300 帧把管道速度加 0.2间隙缩小 5 像素设一个下限防止难到不可玩。这样物理手感始终一致难度却在上升。private int frameCount 0; private float pipeSpeed 3f; private int gapHeight 150; private void UpdateDifficulty() { frameCount; if (frameCount % 300 0) // 每300帧提升一次 { pipeSpeed Math.Min(pipeSpeed 0.2f, 8f); // 速度封顶 gapHeight Math.Max(gapHeight - 5, 110); // 间隙下限 } }逻辑说明Math.Min和Math.Max分别给速度和间隙设了边界防止无限变化导致游戏不可玩。参数说明300 帧约等于 5 秒提升节奏适中速度封顶 8、间隙下限 110 是我实测下来比较合理的值你可以根据手感微调。5.3 用简单的音效反馈提升手感Winform 播放音效最省事的办法是System.Media.SoundPlayer加载一个 wav 文件跳跃时Play()碰撞时Play()另一个。注意SoundPlayer默认是同步播放会阻塞 UI 线程要用PlaySync之外的Play方法或者干脆用WindowsMediaPlayer组件异步播放。音效不用多跳跃和碰撞两个就够手感提升明显。这个技巧不复杂但很多人做小游戏时完全忽略导致画面在动却毫无反馈。最后说个我自己的习惯每次改完参数不要只跑一遍就下结论至少连续玩三局记录每局的分数和死亡原因。如果三局都是撞在同一类管道上说明生成逻辑有偏如果分数波动很大说明难度曲线不稳。调参这件事没有后悔药只能靠反复试。希望帮到你。本文还有配套的精品资源点击获取
返回列表