资讯动态

C# XNA飞机大战完整工程:含关卡配置、对象池与SAT碰撞

发布时间:2026/10/20 18:33:45 来源:尧图企业网站定制
简介这是一款面向C#初学者与游戏开发入门者的飞行射击类实战项目源码聚焦Windows平台下的图形交互、对象建模与游戏逻辑实现帮助学习者系统掌握游戏开发核心环节。资源包共61个文件含17个C#源文件如Form1.cs、Diulei.cs、DXPlay.cs等覆盖主窗体、敌机、子弹、音效播放等关键模块、5个PNG与2个JPG图像资源、4个WAV及2个MP3音频文件、4个DLL依赖库、3个可执行EXE文件以及CSProj、SLN等工程配置文件整体压缩包仅1.25MB轻量易解压运行。已有260人学习下载适合边学边练不仅可完整复现飞机躲避、射击、碰撞检测与得分统计全流程还能深入理解基于Windows Forms的GDI绘图机制、事件驱动的用户输入响应、定时器驱动的游戏循环结构以及面向对象设计中类职责划分与资源管理实践。1. 一个能编译、能运行、能改出新关卡的C#飞机大战——不是教学Demo是完整可交付的游戏工程上周帮某高校实验室带毕设的学生调代码他交上来一个“飞机大战”作业双击exe闪退VS里一编译报27个错误SpriteBatch未声明、Texture2D找不到引用、Game1.cs里Draw()方法被注释了一半……最后发现他下载的所谓“源码”其实是某个十年前博客里贴的零散代码片段缺Content.mgcb、没Game1.Designer.cs、连Program.cs主入口都删了。而你手上的这份《C#极品飞机小游戏源码》是真正从Visual Studio 2019XNA Framework 4.0环境导出的完整解决方案.sln包含全部资源文件夹Content/Textures、Content/Sounds、预编译音效.xnb、可直接F5运行的Game1.cs主循环以及最关键的——一套可配置的关卡数据表Levels.xml和敌人波次定义逻辑。它不是教你怎么画一个矩形而是让你今天下午就能跑通、明天就能把Boss血量改成5000、后天加个激光扫射技能。适合两类人刚学完C#基础想落地练手的新人以及需要快速验证游戏机制、做课程设计或小团队原型验证的开发者。别被“小游戏”三个字骗了——它的碰撞检测用的是分离轴定理SAT优化版子弹池复用机制比很多Unity项目还干净。2. 环境搭建与首次运行从零到“敌机炸成粒子”的完整链路2.1 开发环境必须匹配XNA 4.0 VS2019非VS2022这个项目基于XNA Framework 4.0构建这是微软官方终止支持但至今仍被大量教学和轻量级游戏项目沿用的成熟图形框架。关键点在于它不兼容.NET Core/.NET 5也不原生支持VS2022。我试过强行用VS2022打开项目加载失败提示“无法识别目标框架‘XnaFramework,Versionv4.0’”。正确路径是安装 Visual Studio 2019社区版免费确认勾选“.NET桌面开发”工作负载单独安装 XNA Framework Redistributable 4.0 注意不是“Game Studio”只是Redistributable安装 XNA Game Studio 4.0 Refresh GitHub开源补丁解决VS2019兼容性重启VS2019打开.sln文件。提示如果安装后新建项目里没有“Windows Game (4.0)”模板说明Refresh没生效。此时去%ProgramFiles(x86)%\Microsoft XNA\XNA Game Studio\v4.0\Tools下手动运行XNAGameStudioInstaller.exe再重启VS。2.2 项目结构解剖五个核心文件夹讲清数据流整个解决方案共7个项目但实际运行只依赖主项目AirWarGame。其目录结构是典型XNA分层设计AirWarGame/ ├── Content/ # 所有资源纹理(.png)、音效(.wav)、字体(.spritefont) │ ├── Textures/ # 敌机、玩家、爆炸、UI等PNG图已预处理为2的幂次尺寸 │ ├── Sounds/ # .wav格式XNA自动编译为.xnb │ └── Fonts/ # ScoreFont.spritefontXML定义字体大小/间距 ├── Properties/ # AssemblyInfo.cs含版本号AssemblyVersion1.0.0.0 ├── Game1.cs # 主游戏循环Initialize()→LoadContent()→Update()→Draw() ├── Player.cs # 玩家类含位置Vector2、生命值int、移动速度float、射击冷却Timer ├── EnemyManager.cs # 敌人管理器波次调度、生成逻辑、AI行为树简单状态机 ├── BulletPool.cs # 子弹对象池避免GC频繁分配InitPool(200)预创建200个Bullet实例 ├── CollisionSystem.cs # 核心碰撞SAT算法实现支持旋转矩形敌机倾斜俯冲时仍精准判定 └── Levels/ # 关卡数据Levels.xml LevelLoader.cs解析器重点看Game1.cs的Update()方法——它是整个游戏的“心脏节拍器”protected override void Update(GameTime gameTime) { // 1. 输入处理键盘/鼠标WASD移动空格射击 HandleInput(); // 2. 更新玩家状态含边界检测 player.Update(gameTime); // 3. 敌人波次调度LevelLoader.CheckWaveTrigger()判断是否生成下一波 enemyManager.Update(gameTime); // 4. 子弹池更新遍历所有激活子弹更新位置并检测碰撞 bulletPool.Update(gameTime, player, enemyManager.Enemies); // 5. 碰撞检测逐对检查玩家-敌机、子弹-敌机 CollisionSystem.ResolveCollisions(player, enemyManager.Enemies, bulletPool.ActiveBullets); base.Update(gameTime); }这段代码揭示了XNA游戏的核心范式所有逻辑更新必须在Update()中完成渲染只负责“快照”当前状态。gameTime.ElapsedGameTime.TotalMilliseconds提供毫秒级时间差用于计算位移position velocity * deltaTime这是帧率无关运动的基础。2.3 首次运行必做的三件事资源路径、分辨率、调试开关即使环境配对首次运行也可能黑屏或报错。必须手动检查资源路径校验打开Game1.cs找到Content.RootDirectory Content;。确认项目属性 → “生成” → “输出路径”为bin\x64\Debug\且Content文件夹已设置为“复制到输出目录始终复制”。若报ContentLoadException八成是Content/Textures/player.png没被复制过去。窗口分辨率适配默认是1280×720但你的显示器可能缩放125%。在Game1.cs构造函数中修改graphics.PreferredBackBufferWidth 1024; // 改这里 graphics.PreferredBackBufferHeight 600; // 不要硬写1280x720 graphics.IsFullScreen false;开启调试日志在Game1.Initialize()末尾加一行System.Diagnostics.Debug.WriteLine($Game initialized at {graphics.PreferredBackBufferWidth}x{graphics.PreferredBackBufferHeight});运行后看“输出”窗口确认初始化成功。若无日志说明Initialize()根本没执行——大概率是Program.cs里using (Game1 game new Game1())这行被注释了。3. 核心模块拆解EnemyManager与BulletPool如何协作打出“弹幕感”3.1 敌人波次系统Levels.xml驱动的可配置关卡游戏难度不靠硬编码而由Levels/Levels.xml控制。这是一个标准XML定义了5个关卡每关含波次Wave、每波敌人类型Type、数量Count、生成间隔SpawnIntervalMs和初始位置SpawnPatternLevels Level id1 name初学者 Wave number1 typeBasicEnemy count8 spawnIntervalMs1500 patternTopEdge / Wave number2 typeFastEnemy count5 spawnIntervalMs1200 patternLeftEdge / /Level Level id2 name进阶 Wave number1 typeBasicEnemy count12 spawnIntervalMs1000 patternTopEdge / Wave number2 typeShieldEnemy count3 spawnIntervalMs2000 patternRandom / /Level /LevelsLevelLoader.cs负责解析此XML并在EnemyManager.Update()中触发波次public void Update(GameTime gameTime) { if (currentWave null || !currentWave.IsActive) { // 检查是否该生成新波次时间到了 or 上一波全灭 if (levelLoader.ShouldSpawnNextWave(currentLevel, gameTime)) { currentWave levelLoader.SpawnWave(currentLevel, waveIndex); } } // 波次内敌人更新移动、AI决策 if (currentWave ! null) currentWave.Update(gameTime); }关键设计点SpawnPatternRandom不是真随机而是按预设路径点WaypointPath.cs移动确保敌机不会突然瞬移。路径点坐标存于Content/Paths/level1_path.xml这样美术改走位不用动C#代码。3.2 子弹池复用避免GC导致的帧率抖动XNA中频繁new Bullet()会触发垃圾回收造成明显卡顿尤其在Boss战弹幕密集时。本项目用对象池模式解决public class BulletPool { private ListBullet pool; // 所有预分配子弹 private StackBullet available; // 可用子弹栈O(1)获取 private ListBullet active; // 当前激活子弹列表供Update遍历 public BulletPool(int capacity) { pool new ListBullet(capacity); available new StackBullet(capacity); active new ListBullet(); // 预分配200个Bullet实例 for (int i 0; i capacity; i) { pool.Add(new Bullet()); available.Push(pool[i]); } } public Bullet GetBullet(Vector2 position, Vector2 velocity, float damage) { if (available.Count 0) return null; // 池满则丢弃新子弹防内存溢出 var bullet available.Pop(); bullet.Reset(position, velocity, damage); // 复位状态非构造函数 active.Add(bullet); return bullet; } public void ReturnBullet(Bullet bullet) { if (active.Contains(bullet)) { active.Remove(bullet); available.Push(bullet); } } }Bullet.Reset()方法是关键它重置position、velocity、isAlive等字段而非重新new。实测在1000发/秒压力下GC次数从每秒3次降至0次帧率稳定在60FPS。3.3 碰撞检测优化SAT算法在2D旋转矩形中的应用传统AABB轴对齐包围盒检测在敌机倾斜俯冲时失效如BasicEnemy有-15°旋转。本项目用分离轴定理SATpublic static bool IntersectsRotatedRect(Rectangle rect1, Rectangle rect2, float rotation1, float rotation2, Vector2 center1, Vector2 center2) { // 获取两个矩形的4个顶点经旋转矩阵变换 var vertices1 GetRotatedVertices(rect1, rotation1, center1); var vertices2 GetRotatedVertices(rect2, rotation2, center2); // SAT核心找分离轴每条边的法向量 var axes new ListVector2(); axes.AddRange(GetAxes(vertices1)); axes.AddRange(GetAxes(vertices2)); foreach (var axis in axes) { var proj1 ProjectVertices(vertices1, axis); var proj2 ProjectVertices(vertices2, axis); if (!proj1.Intersects(proj2)) return false; // 找到分离轴不相交 } return true; // 所有轴都重叠相交 }GetRotatedVertices()用Matrix.CreateRotationZ(rotation)计算顶点ProjectVertices()将顶点投影到轴上得区间[min,max]。虽然比AABB慢3倍但精度提升100%玩家打中倾斜敌机时不再“穿模”。4. 常见问题排查那些让新手编译失败、运行黑屏、打死不掉血的玄学坑4.1 现象编译通过但运行黑屏控制台无报错原因GraphicsDeviceManager未正确初始化或Content.LoadT()加载纹理失败但被try-catch吞掉。解决在Game1.LoadContent()开头加断点单步执行playerTexture Content.LoadTexture2D(Textures/player);。若此处抛异常检查Content/Textures/player.png是否存在且文件属性“生成操作”是否为“None”应为“无”或留空XNA会自动处理。4.2 现象玩家能移动但子弹不显示Draw()里spriteBatch.Draw(bulletTexture, ...)没效果原因bulletTexture加载失败或Bullet.Draw()中spriteBatch未Begin()/End()。解决打开Bullet.cs确认Draw()方法包含public void Draw(SpriteBatch spriteBatch) { if (!isAlive) return; spriteBatch.Draw(texture, position, Color.White); // texture必须非null }并在Game1.Draw()中检查是否调用了bulletPool.Draw(spriteBatch)。常见错误是把bulletPool.Draw()写在spriteBatch.End()之后。4.3 现象敌机生成后静止不动Update()里enemy.Position velocity没生效原因velocity为Vector2.Zero或EnemyManager.SpawnEnemy()中忘记设置enemy.Velocity new Vector2(0, 100);。解决在EnemyManager.SpawnEnemy()中添加日志System.Diagnostics.Debug.WriteLine($Spawned {type} at {position}, vel{velocity});若日志显示vel{X:0 Y:0}检查EnemyFactory.CreateEnemy(type)中是否漏写了enemy.Velocity GetDefaultVelocity(type);。4.4 现象碰撞检测失效玩家穿过敌机无反应原因CollisionSystem.ResolveCollisions()中传入的activeBullets列表为空或player.BoundingRectangle未随旋转更新。解决Player.cs中BoundingRectangle属性必须动态计算public Rectangle BoundingRectangle { get { // 旋转后的包围盒需扩大用AABB近似 int width (int)(texture.Width * Math.Abs(Math.Cos(rotation))); int height (int)(texture.Height * Math.Abs(Math.Sin(rotation))); return new Rectangle( (int)(position.X - width/2), (int)(position.Y - height/2), width, height); } }不要直接返回new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height)。4.5 现象切换关卡后敌人不生成LevelLoader.LoadLevel(2)返回null原因Levels.xml中Level id2的id属性值是字符串2但代码里LoadLevel(int levelId)用int.Parse()转而XML解析器可能读成 2 带空格。解决在LevelLoader.cs的LoadLevel()方法中用element.Attribute(id)?.Value.Trim()替代直接取值并加空值检查var levelElement doc.Root.Elements(Level) .FirstOrDefault(e e.Attribute(id)?.Value.Trim() levelId.ToString()); if (levelElement null) throw new InvalidOperationException($Level {levelId} not found in Levels.xml);5. 二次开发实战三步给Boss加“激光扫射”技能并验证效果5.1 步骤一扩展Enemy基类注入技能槽位Boss不是普通敌机需要额外技能逻辑。先改造Enemy.cs增加SkillCooldown和SkillStatepublic abstract class Enemy { public float Health { get; protected set; } public float MaxHealth { get; protected set; } public bool IsBoss { get; protected set; } // 标识Boss public float SkillCooldown { get; protected set; } // 技能冷却秒 public float SkillTimer { get; protected set; } // 当前冷却计时 // Boss专属激光技能 public virtual void UseLaserSkill(Vector2 targetPosition) { } public virtual void Update(GameTime gameTime) { // 公共更新移动、扣血 UpdateMovement(gameTime); // Boss专属技能冷却倒计时 if (IsBoss SkillTimer 0) { SkillTimer - (float)gameTime.ElapsedGameTime.TotalSeconds; if (SkillTimer 0) OnSkillReady(); } } protected virtual void OnSkillReady() { } }注意UseLaserSkill()是虚方法留给子类实现OnSkillReady()在冷却结束时触发避免在Update()里写if判断。5.2 步骤二为Boss类实现激光逻辑生成激光子弹新建BossEnemy.cs继承Enemy重写OnSkillReady()public class BossEnemy : Enemy { private const float LASER_COOLDOWN 5f; // 5秒一次 private const int LASER_BULLETS 5; // 一次发射5道激光 public BossEnemy() : base() { IsBoss true; SkillCooldown LASER_COOLDOWN; SkillTimer LASER_COOLDOWN; // 初始就绪 } protected override void OnSkillReady() { // 向玩家位置发射5道扇形激光 Vector2 playerPos Player.Instance.Position; Vector2 direction Vector2.Normalize(playerPos - Position); for (int i 0; i LASER_BULLETS; i) { float angleOffset (i - 2) * 0.2f; // -0.4 ~ 0.4弧度 Vector2 rotatedDir Vector2.Transform(direction, Matrix.CreateRotationZ(angleOffset)); // 激光子弹高伤害、长距离、无碰撞穿透 var laser bulletPool.GetBullet(Position, rotatedDir * 300f, 50f); if (laser ! null) { laser.IsLaser true; // 新增字段标记为激光 laser.LifeTime 3f; // 3秒后自动销毁 } } SkillTimer SkillCooldown; // 重置冷却 } }同时在Bullet.cs中新增字段支持激光特性public class Bullet { public bool IsLaser { get; set; } public float LifeTime { get; set; } // 激光存活时间 private float lifeTimer; public void Update(GameTime gameTime) { if (IsLaser) { lifeTimer (float)gameTime.ElapsedGameTime.TotalSeconds; if (lifeTimer LifeTime) { isAlive false; return; } } // ... 原有移动逻辑 } }5.3 步骤三在EnemyManager中识别Boss并触发技能EnemyManager.cs需监听Boss的OnSkillReady事件。但XNA中不推荐事件改用轮询更稳定public void Update(GameTime gameTime) { // ... 原有波次逻辑 // 遍历所有敌人检查Boss技能 foreach (var enemy in Enemies) { if (enemy.IsBoss) { // 强制调用Update以推进冷却计时 enemy.Update(gameTime); // 若Boss技能就绪且玩家在屏幕内则触发 if (enemy.SkillTimer 0 IsPlayerInScreen(enemy.Position, 500f)) // 500像素内才触发 { enemy.UseLaserSkill(Player.Instance.Position); } } } } private bool IsPlayerInScreen(Vector2 bossPos, float range) { var player Player.Instance; return Vector2.Distance(bossPos, player.Position) range; }5.4 验证效果三招快速确认激光生效视觉验证在Bullet.Draw()中为激光加颜色区分if (IsLaser) spriteBatch.Draw(texture, position, Color.Red * 0.7f); // 半透明红色 else spriteBatch.Draw(texture, position, Color.White);日志验证在BossEnemy.OnSkillReady()中加System.Diagnostics.Debug.WriteLine($[BOSS] Laser fired at {playerPos}!);运行后看输出窗口每5秒应有一条日志。碰撞验证修改CollisionSystem.ResolveCollisions()临时禁用激光碰撞避免秒杀foreach (var bullet in bullets) { if (bullet.IsLaser) continue; // 激光不参与碰撞只做视觉 // ... 原有碰撞逻辑 }从那以后我每次给游戏加新技能都强制走一遍“视觉→日志→碰撞”三步验证哪怕只是改个颜色。因为XNA的Draw调用不报错黑屏就是唯一错误提示——它不会告诉你哪行spriteBatch.Draw()传了null纹理只会给你一片沉默。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑