简介这是一个面向C#初学者的飞机射击游戏完整源码包适合希望入门游戏开发、理解面向对象编程与游戏循环机制的学习者。源码包含游戏对象创建、交互、碰撞检测、计时器控制、资源加载等核心模块通过研读可掌握C#在Windows窗体项目中的实战应用。压缩包共61个文件以17个cs源文件为主体配合png/jpg图片、wav/mp3音频、dll库及exe可执行程序等资源整体约1.25MB结构简洁便于对照学习。已有259人学习下载。项目清晰展示了类与对象设计、GDI绘图、事件处理、碰撞检测算法及多线程基础等知识点并附有可直接运行的exe便于边运行边调试源码。对于想从零构建小游戏的开发者来说是一份能快速上手的实践范本。1. 开局一台WinForms这是个能直接跑的C#飞机大战源码包C#极品飞机小游戏源码说白了就是一份用C#写的、能直接编译运行的飞机大战Demo源码。如果你正在找C#游戏源码用来交作业、练手或者做二次开发这个项目比大多数网上的半成品靠谱得多——它不依赖Unity或Godot这类重型引擎也不需要配置复杂的环境拿到手用Visual Studio打开按一下F5就能看到游戏窗口跑起来。我拆完这套源码的第一感觉是代码量不大但游戏该有的东西它都有——玩家战机控制、敌机刷新、子弹射击、碰撞检测、计分、音效、难度递增整整一条逻辑链是完整的。适合三类人刚学完C#语法想找个项目练手的初学者、需要交课程设计的大专院校学生、想快速改出一款射击小游戏做活动或演示的在职开发者。项目本身不挑机器Windows系统装个VS就能跑这份解析我会把它的架构、核心代码、常见坑和改造思路一次讲透。2. 架构设计双缓冲渲染与对象池小游戏也讲套路2.1 项目结构与模块划分先看目录结构。整个解决方案是一个标准的WinForms项目如果用Visual Studio打开你会看到与项目名对应的主目录下有这样几个关键文件PlaneWar/ ├── Properties/ │ ├── AssemblyInfo.cs │ └── Resources.resx ├── bin/Release/ // 编译后的可执行文件 ├── obj/ ├── MainForm.cs // 主窗体游戏主循环、渲染入口 ├── MainForm.Designer.cs // 设计器生成代码控件布局 ├── GameConfig.cs // 配置类窗口尺寸、速度、刷新率 ├── GameObject.cs // 游戏对象基类坐标、尺寸、碰撞盒 ├── Player.cs // 玩家飞机移动、射击、受击 ├── Enemy.cs // 敌机类生成、移动、销毁 ├── Bullet.cs // 子弹类单发、连发、对象池 ├── GameManager.cs // 逻辑管理器生成、碰撞、计分 └── Program.cs // 入口启动窗体这种分法在C#游戏源码里属于标准玩法——把配置、实体、逻辑、渲染拆开。相比把所有代码堆在Form里这套结构最大的好处是改起来痛快你调子弹速度不用去翻渲染代码改碰撞判定不用动绘制逻辑。我拆过很多网上那种几百行塞一个文件的小游戏后续想改个功能都跟做手术似的这套至少分出了手术室。移动端小程序、Web游戏常见的ECS架构在小体量项目里反而过度设计WinForms小游戏用类分层就够了。2.2 双缓冲渲染为什么画面不闪小游戏引擎里拖动窗口会闪屏WinForms自带的控件一旦密集刷新就会疯狂闪烁几乎所有飞机大战源码都会用双缓冲来解决。这个项目在MainForm的构造函数里就做了这样一个关键处理public MainForm() { InitializeComponent(); // 开启双缓冲减少闪烁 SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); UpdateStyles(); // 初始化游戏管理器 gameManager new GameManager(this.ClientSize.Width, this.ClientSize.Height); gameTimer new Timer(); gameTimer.Interval 16; // 约 60 FPS gameTimer.Tick GameLoop; gameTimer.Start(); }这段代码是整款游戏稳定运行的地基。SetStyle那三行枚举的叠加告诉WinForms所有绘制都由代码自己来并且使用双缓冲画布。OptimizedDoubleBuffer的含义是先把所有图像画到内存里的一块画布上画完之后一次性推送到屏幕视觉上就没有撕裂感了。Timer的Interval 16毫秒是经验值——60 FPS的刷新间隔既不会让CPU跑满又能保证画面流畅度这是做WinForms小游戏的老司机普遍采用的做法。新手最容易漏掉的就是UpdateStyles()这行不加这行前面的设置不生效画面照样闪成幻灯片。2.3 游戏主循环时间驱动的状态机有了Timer驱动接下来就是游戏循环了。这个项目的GameLoop方法就是整个游戏的心脏private void GameLoop(object sender, EventArgs e) { // 更新逻辑 gameManager.Update(); // 检查玩家是否死亡决定游戏状态 if (!gameManager.IsPlayerAlive()) { gameTimer.Stop(); MessageBox.Show(游戏结束得分 gameManager.GetScore().ToString()); } // 强制重绘触发 OnPaint Invalidate(); }Update和Paint分离是这里的关键。Update做纯逻辑运算——移动飞机、刷新子弹位置、判定碰撞、清理死亡对象Invalidate()只负责通知窗体重新绘制真正的画图动作在OnPaint里执行。这种玩法好处很明显逻辑速度和绘制速度解耦你可以把Update频率调到更高来让碰撞判定更灵敏也可以降低重绘频率来减少CPU占用不用互相牵连。很多初学写小游戏都是直接在Paint里改对象坐标结果一调窗体力就卡成PPT问题就出在逻辑和渲染搅在一起。这游戏的管理器在Update里处理的东西多可以说这一段函数承担了“游戏状态机”的角色执行顺序严格按“玩家输入→移动子弹→生成敌机→碰撞检测→清理尸体”来走顺序乱一个环节比如先碰撞后移动子弹就可能穿模。3. 核心模块拆解从按下方向键到敌机爆炸的完整链路3.1 玩家飞机操控、射击与受击逻辑玩家飞机的实现是这个C#游戏源码里最直观的一块。先看Player.cs里的移动方法public void Move(Direction dir) { switch (dir) { case Direction.Left: X Math.Max(0, X - Speed); break; case Direction.Right: X Math.Min(BoundaryRight, X Speed); break; case Direction.Up: Y Math.Max(0, Y - Speed); break; case Direction.Down: Y Math.Min(BoundaryBottom, Y Speed); break; } }这里Math.Max和Math.Min是防止飞机飞出屏幕边界的经典手法。BoundaryRight和BoundaryBottom在GameConfig.cs里定义为窗体内边缘宽度减去飞机身位宽意思就是飞机顶到边界时不允许继续前进。Speed是每帧移动的像素数这个项目里默认是8像素每帧60FPS下每秒能横跨半个屏幕玩家手感不算肉。想改灵敏度就调这个常量能直观到手感变化。发射子弹的代码也值得看public void Shoot(ListBullet bulletPool, int powerLevel) { if (cooldown 0) return; int bulletCount powerLevel 3 ? 3 : (powerLevel 2 ? 2 : 1); for (int i 0; i bulletCount; i) { Bullet b new Bullet(this.X this.Width / 2 - 2, this.Y - 10); b.SpeedY -10; // 向上飞 if (bulletCount 1) { b.SpeedX (i - 1) * 2; // 散弹效果 } bulletPool.Add(b); } cooldown 8; // 射击CD }cooldown是发射间隔控制变量每帧减1降到0才允许下一次射击。CD设为8也就是在稳定60FPS下约8帧一发每秒射出7到8发。powerLevel用来做火力升级——吃到道具后玩家可以射出双弹甚至三弹。代码里SpeedX (i - 1) * 2让两侧的子弹带有水平偏移形成扩散效果这比单纯一束竖直的子弹打出去观感要好得多是一个性价比很高的细节。3.2 敌机与子弹对象池和批量生成敌机的生成在这个项目里用了“生成控制器”的思路不是随机乱刷。GameManager.cs里有一个SpawnTimer计数器每隔45帧就生成一次敌机同时用随机数决定敌机类型private void SpawnEnemy() { Random rand new Random(); int type rand.Next(0, 3); Enemy enemy new Enemy(); enemy.Type type; // 根据类型设定血量、速度、外形 switch (type) { case 0: // 小型机 enemy.Health 1; enemy.Speed 5; break; case 1: // 中型机 enemy.Health 3; enemy.Speed 3; break; case 2: // 大型机 enemy.Health 6; enemy.Speed 2; break; } enemy.X rand.Next(0, gameWidth - enemy.Width); enemy.Y -enemy.Height; // 从屏幕顶部外进入 enemies.Add(enemy); }敌机血量不同是这个游戏后期有难度曲线的关键。小型机一枪就爆大型机要吃六发子弹才炸。enemy.Y -enemy.Height这个细节很讲究——如果从0开始生成敌机是“闪现”进画面的会显得突然。从屏幕外进入画面有一个缓冲过程其实是在告诉玩家“飞机来了”这个处理在很多商业飞行射击游戏里也是一样的。子弹的存活管理在GameManager里负责遍历ListBullet当子弹的Y坐标小于-10飞出屏幕顶部时就直接从列表中移除。这里没做对象池复用属于小体量游戏的将就方案GC压力不大。如果想要更高效可以把ListBullet改成固定容量的队列但那是性能优化的活儿不是当前阶段必须处理的。3.3 碰撞检测矩形相交判定撞机判定是这个项目最关键且最容易写崩的部分。这个游戏采用的是2D游戏里常见的AABB轴对齐包围盒碰撞检测代码集中在GameManager.CheckCollisions()private bool IsColliding(GameObject a, GameObject b) { return !(a.X a.Width b.X || b.X b.Width a.X || a.Y a.Height b.Y || b.Y b.Height a.Y); } private void CheckCollisions() { // 子弹打敌机 for (int i bullets.Count - 1; i 0; i--) { for (int j enemies.Count - 1; j 0; j--) { if (IsColliding(bullets[i], enemies[j])) { enemies[j].Health - bullets[i].Damage; bullets.RemoveAt(i); break; } } } // 敌机撞玩家 foreach (var enemy in enemies) { if (IsColliding(player, enemy)) { player.Health - 1; enemy.Health 0; } } // 清掉死亡对象 enemies.RemoveAll(e e.Health 0); }IsColliding的逻辑采用了“排除法”——只要两个对象的四条边中任意一条处在对方的外侧就没有重叠四边检查全部通过说明有相交区域。这段代码是从列表尾部向前遍历删除元素规避了RemoveAt导致索引错位的问题属于List移除元素的标准写法。这个碰撞盒是矩形的对飞机这种扁长形状来说误差可以接受。想要更精细的判定可以再往里加一层像素级检测但那种做法比较吃CPU小游戏用不上。这里的参数含义要清楚X、Y是对象左上角坐标Width、Height是真实绘制位图的宽高设置时尽量与图片尺寸一致否则会出现“子弹打中了但没判死”“看起来躲开了却被爆了”的诡异现象。3.4 绘制环节GDI与图片资源的调用绘制模块在MainForm.OnPaint中实现protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; // 1. 画背景 g.DrawImage(backgroundBmp, new Rectangle(0, bgY1, gameWidth, gameHeight)); g.DrawImage(backgroundBmp, new Rectangle(0, bgY2, gameWidth, gameHeight)); // 2. 画子弹遍历 foreach (var bullet in gameManager.GetBullets()) { g.DrawImage(bulletImage, bullet.X, bullet.Y); } // 3. 画敌机 foreach (var enemy in gameManager.GetEnemies()) { g.DrawImage(enemyImage, enemy.X, enemy.Y); } // 4. 画玩家 g.DrawImage(playerImage, player.X, player.Y); // 5. 画得分 g.DrawString(得分 gameManager.GetScore().ToString(), new Font(Arial, 14), Brushes.White, 10, 10); base.OnPaint(e); }背景用bgY1和bgY2两段图交替向上移动产生滚动效果。代码逻辑是每次Update让bgY1和bgY2递减当它们超出屏幕底部时重置到另一端顶部看起来就像是星空在往下流动。这种实现方式不需要额外的背景对象直接用坐标补位移动的地面感很强。DrawImage的重载版本很多四参数的版本传位图X、Y是按照原始像素大小绘制不缩放。如果资源图本身就是按Width/Height设定的尺寸设计的视觉上刚好对齐碰撞盒。要是换了美术资源、尺寸不一致绘制和碰撞会错开这是换素材时最常踩的坑之一。4. 运行排障与避坑双击跑不起来的五个典型原因4.1 编译运行步骤进入实战之前先把跑通这事的步骤交代清楚照着做不会翻车# 第一步用 Visual Studio 打开解决方案文件.sln # 第二步检查项目目标框架是否与本地环境匹配 # 第三步直接按 F5 编译并运行或者 CtrlShiftB 仅编译 # 第四步窗口弹出后用方向键/WASD 控制飞机按空格键发射如果项目里带的是源码而非工程文件那就新建一个“Windows窗体应用”项目把MainForm.cs、GameManager.cs、Player.cs等文件挨个添加进去再把资源图片放进Resources目录并生成引用。这一步很多新手容易卡住——资源文件不是拖进项目就生效还需要在Resources.resx中注册。VS里直接拖图片到“资源视图”面板会自动注册手改resx文件容易出错。4.2 常见问题编译失败 / 控件空白 / 图片丢失 / 运行卡顿现象一编译报错提示找不到类型或命名空间原因这类源码通常是在特定 .NET Framework 版本下写的本机新建项目的目标框架和源码不一致比如源码用Framework 4.5你的VS默认4.7.2部分新写法可能被移除或反之旧API在新的Core版本里不可用。解决右键项目选择属性把“目标框架”改成带有WinForms支持的版本。.NET Core/.NET 5的WinForms在项目文件里还要额外加一句UseWindowsFormstrue否则编译通过但窗体启动时黑屏报错。现象二窗口弹出来了但里面什么都没有一片灰白原因掉进“没有触发Paint事件”的坑。OnPaint没有被正确调用通常是你看的那份资源把绘制逻辑写在Form_Load里或者Timer没有启动。解决先加粗体提示——断点打在OnPaint里看它到底有没有被调用。如果确实没被调用检查窗体构造函数有没有执行SetStyle那三行再检查Timer有没有Start。然后手动调用Invalidate()这两件事缺一个绘制循环就推不动。现象三运行时报资源异常图片加载失败原因资源文件路径写错。网上流传的源码经常用Properties.Resources.xxx访问嵌入资源但你的工程里没把对应图片加进resx就会在Bitmap.FromFile时走相对路径。相对路径出错最常见是bin/Debug目录下没有Image文件夹。解决优先用Properties.Resources方式或直接把图片文件放到bin/Debug下一个固定的Images目录用Application.StartupPath /Images/player.png来定位。这个写法在任何开发机上都能跑通前提是资源不缺失。现象四游戏跑起来CPU占用100%原因Timer的Interval设成了0或者GameLoop里面没有做对象清理敌机子弹不断累积导致绘制越来越多。解决把Interval固定为16同时检查enemies.RemoveAll(e e.Health 0)这行是否被注释掉了。如果没清理游戏运行三分钟后就会有上百个对象在更新位置、绘制图像性能直接崩溃。现象五碰撞判定的效果和自己想象的不一致——打中了但没扣血原因IsColliding里的边界条件写反或者Width/Height没赋值默认是0导致永远只判定一个坐标点的重合。解决在GameObject基类的构造函数里初始化Width40; Height40;之类值再在派生类里按实际资源尺寸覆盖。调试时可以画辅助矩形——在OnPaint里暂时用Pen把每个对象的碰撞盒画出来数字对齐后再删掉这段调试代码。5. 二次开发把单机小游戏改成分数闯关版的改造套路飞机大战源码拿在手里最有价值的玩法不是原封不动跑通而是改成自己想要的样子。这里给几个成熟的改造方向以及对应的代码落点。第一个改造点增加关卡系统。在GameManager.cs里加一个Level字段每过20秒让敌机生成间隔缩短、移动速度整体增加。具体实现是把原来的SpawnTimer 45改成SpawnTimer 45 - level * 3并且下限不低于15。这样随着时间推移压力自然上升难度的曲线是渐进的不会突然跳到地狱模式。第二个改造点给玩家增加道具系统。比如在敌机死亡时有5%概率掉落一个“P”标志道具玩家碰触后powerLevel加一级。落点就在碰撞检测的“敌机死亡”分支里生成一个PowerUp对象再在OnPaint里绘制并参与碰撞判定。我给这个项目加的改造就是三火力加上侧移子弹代码就几十行但手感提升非常明显。第三个改造点音效。WinForms下最简单的是用System.Media.SoundPlayer子弹发射和爆炸各配一个WAV资源在射击和销毁代码处调用.Play()即可。注意不要用MP3格式SoundPlayer对WAV支持最好换MP3会出现不稳定的异常。验证改造效果的方法也简单每次改完把Interval临时调成50模拟低帧率环境下跑5分钟确认逻辑没有依赖帧率。顺带一提改配置类里的常量比直接改死数值要安全——比如Speed、SpawnInterval这些在GameConfig.cs里统一定义后续想调平衡性不用一个文件一个文件翻。从那以后我每次改这类小游戏源码都强制走一遍流程先梳理项目结构再理清主循环和碰撞检测最后才动手改。这个习惯救了我很多次因为网上很多源码的命名和注释并不可靠只有run起来之后自己加的断点才骗不了人。希望帮到你跑通了就去改第一行代码吧。本文还有配套的精品资源点击获取