简介:这是一份面向C#初学者与游戏开发入门者的飞行射击小游戏完整源码,以飞机、子弹、敌人等对象为核心,帮助读者在实战中理解游戏运行机制。源码覆盖C#基础语法、面向对象设计、Windows Forms界面绘制、游戏循环、定时器与帧率控制、碰撞检测、资源加载、事件处理及调试优化等关键知识点,飞机与子弹的移动、发射逻辑均有完整实现,适合作为课程设计或自学练手项目。压缩包共61个文件,约1.25MB,以17个cs源码文件为主体,辅以png、jpg、gif图像与wav、mp3音频资源,另有dll、exe、resx、csproj、sln等工程与运行文件,目录结构清晰,便于按模块阅读。目前已有259人学习下载。通过研读这份源码,读者可掌握游戏对象建模、循环调度与碰撞算法的落地写法,积累图形渲染与资源管理的实践经验,为后续开发更复杂的游戏项目打下基础。
1. 从一份 C# 极品飞机小游戏源码说起:它到底能跑出什么
很多人第一次拿到「C#极品飞机小游戏源码」这类资源,第一反应是双击 .sln 然后 F5,结果要么报错,要么窗口一闪而过,要么飞机贴图全丢。我拆过不少这类 WinForms 游戏源码,它本质上是一个用 C# 写的 2D 竖版射击小游戏,核心逻辑围绕玩家飞机、敌机生成、子弹碰撞、分数统计和关卡难度曲线展开。它解决的不是「教你 C# 语法」的问题,而是给你一个能直接跑起来、能改参数、能换素材的完整可运行工程。适合谁?适合刚学完 C# 基础、想找一个有画面有交互的练手项目的人,也适合想拿它当课程设计或毕设底稿的从业者。它不依赖 Unity 或 Raylib,纯 WinForms + GDI+ 绘制,所以对环境和显卡几乎没有要求,一台装了 Visual Studio 的普通办公机就能跑。
2. 工程结构与运行链路:先搞清楚它怎么把飞机画到屏幕上
2.1 源码目录里到底有什么
拿到源码包后,别急着编译。先看目录结构,这类 WinForms 游戏通常长这样:
PlaneGame/ ├── PlaneGame.sln ├── PlaneGame/ │ ├── Program.cs │ ├── MainForm.cs │ ├── MainForm.Designer.cs │ ├── GameObjects/ │ │ ├── Player.cs │ │ ├── Enemy.cs │ │ ├── Bullet.cs │ │ └── Explosion.cs │ ├── Resources/ │ │ ├── player.png │ │ ├── enemy.png │ │ └── bullet.png │ └── Properties/ │ └── Resources.resxProgram.cs是入口,MainForm.cs是主循环和绘制逻辑,GameObjects下每个类对应一种游戏实体。资源图片一般放在Resources文件夹并通过Resources.resx嵌入程序集。如果你看到的是.resx引用而不是直接读文件路径,说明作者用了嵌入资源方式,这样发布时不会丢图。
2.2 游戏主循环是怎么转起来的
WinForms 本身没有游戏循环,这类源码通常用System.Windows.Forms.Timer或System.Timers.Timer来驱动。常见做法是在MainForm构造函数里初始化一个 16ms 或 33ms 的 Timer,Tick 事件里做三件事:更新所有对象位置、检测碰撞、调用Invalidate()触发重绘。
// MainForm.cs 片段 private Timer gameTimer; private void InitGame() { gameTimer = new Timer(); gameTimer.Interval = 16; // 约 60 FPS gameTimer.Tick += GameLoop; gameTimer.Start(); } private void GameLoop(object sender, EventArgs e) { UpdatePlayer(); UpdateEnemies(); UpdateBullets(); CheckCollisions(); this.Invalidate(); // 请求重绘,触发 OnPaint }Interval = 16是经验值,对应约 60 帧每秒。设成 10 会更快但 CPU 占用上升,设成 33 会明显卡顿。Invalidate()只是标记区域无效,真正绘制在OnPaint里用Graphics对象完成。如果你把Invalidate()换成Refresh(),会强制同步重绘,帧率反而更不稳,这是新手常翻车的地方。
2.3 双缓冲与闪烁问题
默认 WinForms 绘制会闪,因为先擦背景再画前景。这类源码一般会在MainForm构造函数里开双缓冲:
public MainForm() { InitializeComponent(); this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); this.UpdateStyles(); }AllPaintingInWmPaint把擦背景和绘制合并到OnPaint,OptimizedDoubleBuffer启用双缓冲。少了这两句,飞机移动时会有明显拖影。如果你拿到源码发现画面闪,先检查这里,而不是去改 Timer 间隔。
3. 把源码跑起来并改出第一个可玩版本
3.1 环境准备与编译
你需要 Visual Studio 2019 或 2022,安装时勾选「.NET 桌面开发」工作负载。打开.sln后,先看项目属性里的目标框架。老源码可能是.NET Framework 4.7.2或4.8,新一点的可能是.NET 6/8。如果是 Framework 版本,确保本机装了对应运行时;如果是 .NET 6+,直接dotnet build也能过。
# 在解决方案目录下 dotnet restore dotnet build -c Release dotnet run --project PlaneGame/PlaneGame.csproj如果报「未能找到类型或命名空间」,九成是using少了或者资源文件没包含进项目。右键Resources文件夹里每张图,确认「生成操作」是「嵌入的资源」或「内容」且「复制到输出目录」为「始终复制」。
3.2 玩家移动与边界限制
玩家飞机通常用键盘方向键或 WASD 控制。核心是改Player.X和Player.Y,但必须做边界钳制,否则飞机会跑出窗体。
private void UpdatePlayer() { int speed = 5; if (Keys.Left) player.X -= speed; if (Keys.Right) player.X += speed; if (Keys.Up) player.Y -= speed; if (Keys.Down) player.Y += speed; // 边界限制 player.X = Math.Max(0, Math.Min(ClientSize.Width - player.Width, player.X)); player.Y = Math.Max(0, Math.Min(ClientSize.Height - player.Height, player.Y)); }speed = 5是每帧位移像素数。如果你觉得飞机太飘,改成 3;觉得太肉,改成 8。边界用Math.Max/Min钳制,比写 if-else 干净。注意ClientSize是窗体客户区,不含标题栏和边框,用Width/Height会把飞机卡在边框里。
3.3 敌机生成与难度曲线
敌机不能一上来就满屏,常见做法是用一个计数器控制生成间隔,并随分数提高逐渐缩短间隔。
private int enemySpawnCounter = 0; private int spawnInterval = 60; // 帧数间隔 private void UpdateEnemies() { enemySpawnCounter++; if (enemySpawnCounter >= spawnInterval) { enemySpawnCounter = 0; SpawnEnemy(); // 每 500 分缩短一次间隔,最低 20 帧 if (score > 0 && score % 500 == 0) spawnInterval = Math.Max(20, spawnInterval - 5); } // 移动和移除逻辑... }spawnInterval初始 60 帧,约每秒一个。分数每涨 500 就减 5 帧,最低 20 帧。这个曲线比线性加难度更平滑。如果你把spawnInterval直接设成 10,开局就会劝退。
3.4 碰撞检测的两种写法
这类小游戏一般用矩形相交检测,简单够用。
private bool IsCollide(GameObject a, GameObject b) { Rectangle ra = new Rectangle(a.X, a.Y, a.Width, a.Height); Rectangle rb = new Rectangle(b.X, b.Y, b.Width, b.Height); return ra.IntersectsWith(rb); }Rectangle.IntersectsWith是 GDI+ 自带方法,比手写四边比较快。注意图片透明区域也算在矩形内,所以飞机翅膀尖碰到敌机边缘就会判定死亡。想更精确可以用像素级检测,但性能会掉,小游戏没必要。
4. 避坑与排查:那些让你怀疑人生的报错
4.1 现象:编译通过但运行闪退
原因:Program.cs里Application.Run(new MainForm())之前抛了异常,或者资源图片路径不对导致Bitmap构造失败。解决:在Program.cs的Main里加try-catch把异常写到日志文件,或者直接在 VS 里按 F5 看输出窗口的异常信息。常见的是Resources.player返回 null,检查.resx里是否真的嵌入了图片。
4.2 现象:飞机移动卡顿、画面撕裂
原因:Timer 间隔太小加上没开双缓冲,或者OnPaint里做了耗时操作(比如每帧重新加载图片)。解决:确认SetStyle双缓冲已开;把图片加载放到构造函数里只做一次,OnPaint里只画不加载。如果用了Image.FromFile每帧读盘,换成Properties.Resources.player从内存取。
4.3 现象:敌机或子弹越界后不消失,内存越跑越高
原因:对象移出屏幕后没有从集合里移除,List<Enemy>只增不减。解决:在更新循环里倒序遍历,把Y > ClientSize.Height或X越界的对象RemoveAt。注意必须倒序,正序删除会跳过元素。
for (int i = enemies.Count - 1; i >= 0; i--) { if (enemies[i].Y > ClientSize.Height) enemies.RemoveAt(i); }4.4 现象:改了图片资源但运行还是旧图
原因:VS 缓存了旧的嵌入资源,或者.resx没同步更新。解决:清理解决方案(生成 → 清理解决方案),然后重新生成。如果还不行,把bin和obj文件夹手动删掉再编译。这是血泪经验,别问我是怎么知道的。
4.5 现象:发布到别的电脑上提示缺少 DLL 或框架
原因:目标机器没装对应 .NET 运行时。解决:如果是 .NET Framework,让对方装对应版本;如果是 .NET 6+,发布时选「独立部署」并勾选「生成单个文件」,这样会把运行时打包进去。代价是文件变大,但省去装环境的麻烦。
5. 进阶玩法:把这份源码改成你自己的东西
5.1 换素材与调参数
想换飞机图片,直接替换Resources里的 png,保持文件名和尺寸比例一致。尺寸不一致会导致碰撞矩形错位,看起来像「明明没碰到就死了」。我一般会把所有素材统一到 64x64 或 48x48,然后在代码里用Width/Height取实际值,不写死数字。
5.2 加一个简单的状态机
原始源码可能只有「游戏中」和「游戏结束」两个状态。你可以加一个enum GameState { Ready, Playing, Paused, GameOver },在GameLoop开头判断状态,非 Playing 就不更新逻辑。这样按 P 键暂停、按空格重新开始都很容易接。
private GameState state = GameState.Ready; private void GameLoop(object sender, EventArgs e) { if (state != GameState.Playing) return; // 更新逻辑... }5.3 用值元组解构简化碰撞返回
C# 支持值元组解构,可以让碰撞检测返回更丰富的信息,比如碰撞类型和伤害值。
private (bool hit, int damage) CheckHit(Bullet b, Enemy e) { if (IsCollide(b, e)) return (true, b.Damage); return (false, 0); } // 调用 var (hit, dmg) = CheckHit(bullet, enemy); if (hit) enemy.HP -= dmg;这比返回bool再查全局变量干净得多,也是 C# 高级编程里常提的技巧。
5.4 验证你改对了没有
改完别只看画面,做三个检查:第一,连续玩三分钟看内存是否稳定,任务管理器里进程内存不应持续上涨;第二,把窗口拉大缩小,飞机和敌机位置是否还正常;第三,断点打在OnPaint里,看每帧绘制对象数量是否和集合大小一致。我习惯在OnPaint末尾画一个FPS计数,用Stopwatch算,低于 50 就说明有性能问题。
private Stopwatch fpsWatch = Stopwatch.StartNew(); private int frameCount = 0; private int fps = 0; // 在 OnPaint 末尾 frameCount++; if (fpsWatch.ElapsedMilliseconds >= 1000) { fps = frameCount; frameCount = 0; fpsWatch.Restart(); } g.DrawString($"FPS: {fps}", this.Font, Brushes.White, 10, 10);从那以后我每次拿到这类 WinForms 游戏源码,都先跑一遍 FPS 和内存,再动手改逻辑。希望这份拆解能帮你少走几个弯路,顺利把飞机飞起来。
本文还有配套的精品资源,点击获取