简介这是一份基于Unity 3D引擎开发的太空飞机射击游戏期末作业完整工程适合Unity初学者、游戏开发相关专业学生及需要完成大作业实践项目的开发者参考学习。项目围绕太空背景场景搭建、飞机移动控制、子弹射击与碰撞检测、计分系统、游戏状态管理、音效播放等核心模块展开覆盖了C#脚本编写、Animator动画控制、UGUI界面设计、Rigidbody物理组件应用等关键技能能够帮助读者系统理解一款小型3D射击游戏从场景搭建到功能实现的全流程。压缩包约87.43MB共包含11022个文件主要涉及C#脚本(1413个)、meta资源信息(2536个)、png纹理素材(672个)、Unity场景/预制体/材质等工程资产以及dll运行库目录结构较为完整。项目可直接用Unity打开查看完整实现从飞机移动、射击逻辑到碰撞销毁、分数UI均有对应代码与资源支撑已有1991人学习浏览适合想通过完整示例快速上手Unity开发、参考射击类游戏核心逻辑与工程组织的同学使用。1. Unity 3D期末作业做太空飞机射击游戏为什么它是最不容易翻车的选题Unity 3D期末作业如果只让我推荐一个稳的方向太空飞机射击游戏一定排前三。它的画面够炫、玩法边界清晰哪怕只做“移动、发射、敌机、碰撞、计分”这五件事演示效果也远好过一堆半成品功能点。更关键的是这个品类对美术资源要求极低用引擎自带的几何体加一点粒子效果就能撑起全场新手从零开始一周课余时间就能跑出一个能演示、能交差、能讲清设计思路的成品。这篇笔记适合三类人第一次碰 Unity 想选个稳选题的学生、想给期末项目加一点可玩性但怕玩脱的人以及帮别人看项目时想快速定位问题的同学。2. 动手前先定三件事视角方案、碰撞层级与最小玩法闭环2.1 第三人称追尾视角为什么课堂演示最吃香太空射击游戏通常有两种视角第一人称座舱视角和第三人称追尾视角。期末作业我一般直接建议做第三人称追尾。第一人称需要处理仪表盘、瞄准框、机舱遮挡美术和交互成本都高演示时观众隔着一个屏幕也看不清细节。追尾视角最大的好处是“你看到的飞机就是裁判看到的飞机”操作反馈直观子弹轨迹、敌人来袭方向、爆炸范围一目了然。追尾视角还有一个隐藏优势相机不需要自己写旋转逻辑只要把 Main Camera 挂在飞机后面用 LateUpdate 跟随位置就行。后期想换成立式弹幕玩法只需要把相机挪到侧面代码改动量极小。对比之下第一人称视角至少要多写一套相机抖动和视野缩放逻辑对期末作业来说属于纯消耗。相机参数上我一般会把相机放在玩家后上方约 6 到 10 个单位的距离偏移量设为(0, 2.5, -8)。z 轴偏移负值表示相机在飞机后方y 轴 2.5 保证能看到飞机背部和前下方的敌机。视角范围 Field of View 设在 55 到 60 度之间太宽会出现边缘畸变太窄会看不清两侧来袭的敌人。2.2 碰撞层级与物理配置新手最容易埋雷的地方很多同学把脚本挂好、预制体拖好一运行却发现子弹没打中敌人或者飞机被自己的子弹一击秒杀。问题几乎都出在碰撞配置上。Unity 默认所有物体都在 Default 层级子弹、敌机、玩家互相触发你根本没法区分“玩家撞敌人”和“子弹撞玩家”。我建议开局就把玩家的层级设为 Player子弹设为 Bullet敌机设为 EnemyUI 和墙体放在 Environment。然后在 Edit - Project Settings - Physics 里打开层碰撞矩阵把 Bullet 设置成只与 Enemy 碰撞。这个操作比写一百行 if 判断都管用而且一劳永逸。碰撞体选择也有讲究。飞机和敌机这类造型复杂的模型用 Mesh Collider 听着精确但性能差且容易和子弹的 Trigger 检测产生漏判。用 Box Collider 或 Capsule Collider 包裹飞机主体就够了子弹用自己的小 Box Collider 并勾选 Is Trigger。这样OnTriggerEnter检测稳定物理模拟开销也可以忽略不计。2.3 最小玩法四件套移动、开火、生成、销毁一个能当期末作业交上去的太空飞机射击游戏最少得具备四个系统玩家移动、玩家开火、敌机生成、碰撞销毁。这四个系统彼此独立又能串成一条完整的游戏循环。我写项目时习惯先把这四个系统的最小版本跑通再回头加计分、特效、音效。代码如下这是玩家控制脚本的完整最小版本using UnityEngine; public class PlayerController : MonoBehaviour { public float speed 15f; // 巡航速度单位是 Unity 单位/秒 public float boundaryX 9f; // 水平边界防止飞出屏幕 public float boundaryY 5f; // 垂直边界防止飞到画面外 public GameObject bulletPrefab; // 子弹预制体拖拽赋值 public Transform firePoint; // 弹口位置放在机头前方 void Update() { // 键盘输入横轴和纵轴范围都是 -1 到 1 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); // 移动Time.deltaTime 保证帧率不同时速度一致 Vector3 move new Vector3(h, v, 0f) * speed * Time.deltaTime; transform.position move; // 边界钳制超过边界就拉回最边缘 float x Mathf.Clamp(transform.position.x, -boundaryX, boundaryX); float y Mathf.Clamp(transform.position.y, -boundaryY, boundaryY); transform.position new Vector3(x, y, transform.position.z); // 发射默认鼠标左键或空格键 if (Input.GetButtonDown(Fire1) || Input.GetKeyDown(KeyCode.Space)) { Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); } } }这段代码的几个关键参数值得说明。speed 设 15 是经验值在太空场景中这个速度配合追尾视角会显得轻快又不至于失控如果你做的是弹幕玩法可以降到 8 到 10。boundaryX 和 boundaryY 要根据实际相机视野调整正交相机视野里边界大约是9f和5f透视相机则需要先跑一遍看飞机到屏幕边缘再回来微调。发射采用GetButtonDown而不是GetButton因为按住连发在最小版本里不需要等后面做道具系统再加入自动连发。3. 让飞机飞起来移动手感、边界钳制与相机跟随3.1 直接用 Transform 还是刚体期末作业的取舍移动手感的第一个争论点是“用 Transform 移动还是用 Rigidbody 移动”。对这个项目我建议直接改 Transform除非你的飞机需要做碰撞反弹或者被爆炸冲击波推开。原因有两个一是期末作业的物理场景极其简单刚体带来的真实受力在这里体现不出来二是刚体移动容易在低帧率时出现穿透或抖动排查问题的时间远超省下的那点代码量。但要注意直接改 Transform 的飞机是可以和敌人碰撞体相交的。所以碰撞销毁逻辑必须依赖OnTriggerEnter或OnCollisionEnter来显式处理不能让物理系统帮你解决一切。如果你在飞机上挂了 Rigidbody 且没有勾选 Is Kinematic反而会引入额外的受力玩家按方向键时会产生延迟感。我见过一个朋友的飞机在按下方向键后要半秒才有反应排查半天发现是刚体质量设成了 0.01惯性太大。3.2 边界钳制与加速度三行代码救回手感太空射击最影响手感的问题是“飞机像在冰面上滑行”。这通常是因为代码直接修改位置而玩家按下的方向键弹起、落下都有瞬时值导致飞机立刻达到最大速度再立刻减到零。用 Unity 自带的Mathf.SmoothDamp可以让加速度和减速度都有过渡过程大概三行public float smoothTime 0.15f; // 达到目标速度所需时间越小越跟手 private Vector3 velocityRef Vector3.zero; private Vector3 targetPos; void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); // 目标位置原位置加上这一帧期望的位移 targetPos transform.position new Vector3(h, v, 0f) * speed * Time.deltaTime; // SmoothDamp 会生成一个带缓冲的中间位置消除冰面手感 transform.position Vector3.SmoothDamp( transform.position, targetPos, ref velocityRef, smoothTime); // 边界钳制仍然保留 float x Mathf.Clamp(transform.position.x, -boundaryX, boundaryX); float y Mathf.Clamp(transform.position.y, -boundaryY, boundaryY); transform.position new Vector3(x, y, transform.position.z); }smoothTime 参数直接决定手感0.1 到 0.15 是最跟手的区间0.3 以上会有明显的迟滞感适合做宇宙飞船大质量漂移。注意 SmoothDamp 的 velocityRef 是需要跨帧保持的千万别在 Update 里 new 一个新的 Vector3否则每次都会重置缓冲手感立刻变回冰面。如果你的期末答辩需要展示“我做了数值调优”这个参数就是绝佳的素材把 smoothTime 从 0.3 一路调到 0.1让台下老师看着飞机从漂移感变成跟手感比嘴上说“我调过手感”有力得多。3.3 相机跟随用 LateUpdate 干掉抖动相机跟随挂在 Update 里会导致一个经典问题相机和飞机的执行顺序不确定偶尔出现飞机已经移动但相机还停在上一帧位置画面就会一卡一卡。正确做法是把跟随逻辑放在 LateUpdate 中它会在所有 Update 执行完之后再跑一次保证相机看到的是飞机最新位置。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; // 要跟随的飞机 public Vector3 offset new Vector3(0f, 2.5f, -8f); // 相机相对飞机的偏移 public float smoothTime 0.2f; // 跟随缓冲时间 private Vector3 velocityRef Vector3.zero; void LateUpdate() { Vector3 desiredPosition target.position offset; transform.position Vector3.SmoothDamp( transform.position, desiredPosition, ref velocityRef, smoothTime); transform.LookAt(target); } }offset 的 z 轴用负值是因为 Unity 左手坐标系下相机的正前方是 z 轴正方向飞机在原点时相机放 z -8 才能照到飞机。smoothTime 建议设 0.15 到 0.25太大会让镜头像绑了橡皮筋太小则失去了追尾镜头应有的缓冲。注意目标飞机如果做了爆炸销毁销毁代码里要判断相机 target 不为空否则下一帧 LateUpdate 会直接报空引用。4. 射击、敌机与爆炸特效把“能打”变成“能看”4.1 子弹用对象池还是 Instantiate课堂项目我这样选网上教程都推荐对象池理由是避免频繁实例化造成的卡顿。但我的建议是如果你的子弹同时在场数量不超过 30 发直接Instantiate完全够用期末作业演示也不会掉帧。对象池能复用的前提是你反复创建和销毁同样的对象而太空射击游戏的子弹生命周期极短当场生成当场销毁池子反而多了一套“取对象、放对象、清状态”的逻辑复杂度上去了展示效果并没有变好。真正需要对象池的信号是子弹携带复杂材质或需要预热的粒子系统创建瞬间有明显掉帧。如果只是普通白色球体加一个点光源特效Instantiate就够了。我把子弹脚本写成这样using UnityEngine; public class Bullet : MonoBehaviour { public float speed 25f; // 子弹飞行速度 public float lifeTime 2f; // 存活时间超时自动销毁避免泄漏 public int damage 1; // 对敌机造成的伤害 void Start() { // 超时销毁防止子弹飞出边界后永不清理 Destroy(gameObject, lifeTime); } void Update() { // 沿自身 z 轴正方向飞行 transform.position transform.forward * speed * Time.deltaTime; } void OnTriggerEnter(Collider other) { // 只对带 EnemyTag 的对象生效 if (other.CompareTag(Enemy)) { Destroy(other.gameObject); // 真实项目中这里应该扣血 Destroy(gameObject); // 子弹命中后消失 } } }speed 设 25 到 30 肉眼感觉最舒服太慢子弹显得发飘太快则几乎没有飞行过程看不出是射击。damage 字段在这个版本里只做占位后面接入敌机血量系统时直接用。lifeTime 设 2 秒对应最远射程约 50 到 60 个单位边界外的敌机打不到也正常。4.2 敌机生成器波次、数量与难度曲线的写法敌机生成器我用协程写最省事。协程可以暂停继续天然适合做间隔生成。难度曲线不必复杂核心规则就是“存活敌机数量上限随当前击毁数缓慢增加”。这样既不会前期一堆敌人压过来打不完也不会后期没有压力。using System.Collections; using UnityEngine; public class EnemySpawner : MonoBehaviour { public GameObject enemyPrefab; // 敌机预制体 public float spawnInterval 1.2f; // 生成间隔秒 public int baseMaxEnemies 3; // 初始最大存活数 public int enemiesKilled 0; // 累计击毁数用于提升难度 private int aliveEnemies 0; // 当前存活数 void Start() { StartCoroutine(SpawnLoop()); } IEnumerator SpawnLoop() { while (true) { // 每轮根据击毁数动态计算最大存活数上限设 10 防止失控 int maxEnemies Mathf.Min(baseMaxEnemies enemiesKilled / 3, 10); if (aliveEnemies maxEnemies) { SpawnOneEnemy(); } yield return new WaitForSeconds(spawnInterval); } } void SpawnOneEnemy() { // 在屏幕上方随机 x 坐标生成y 固定在场景顶部 float x Random.Range(-8f, 8f); Vector3 pos new Vector3(x, transform.position.y, transform.position.z); GameObject enemy Instantiate(enemyPrefab, pos, Quaternion.identity); aliveEnemies; } // 由敌机销毁时调用注意用公共方法而不是事件简单直接 public void OnEnemyDestroyed() { aliveEnemies--; enemiesKilled; } }难点在于“敌机销毁时调用OnEnemyDestroyed”。最简单的办法是在敌机脚本的 OnTriggerEnter 里找到场景中的生成器再调用性能略差但在课堂项目里无所谓。更稳的做法是敌机挂载的时候直接把生成器传入敌机脚本销毁时直接调引用。我最常用后者避免依赖FindObjectOfType。4.3 特效与音效用引擎自带对象省下两百行爆炸特效不用找素材包Unity 自带的粒子系统足够。新建一个空对象挂上 ParticleSystem选一颗球形粒子Start Lifetime 设 1 到 1.5 秒Start Speed 设 0Start Size 设 1把 Shape 设成 SphereEmission Rate 设 30 到 50这样一个简单爆炸云就出来了。代码里在敌机销毁时生成特效对象然后让特效自毁public GameObject explosionPrefab; // 爆炸特效预制体 public void OnTriggerEnter(Collider other) { if (other.CompareTag(Bullet)) { Instantiate(explosionPrefab, transform.position, Quaternion.identity); Destroy(gameObject); } }特效预制体记得让自身的 ParticleSystem 勾选“Play On Awake”这样生成后不用额外调用。音效同理用一个 AudioSource 挂在特效物体上启动时播放一段爆炸音效配合自动销毁即可。别把音效做成常驻组件否则屏幕外敌机爆炸也会“啪”地响一声观众会觉得很奇怪。4.4 UI 计分与 Game Over 判定给演示留个体面收尾计分不复杂用静态变量最简单。创建 UI Canvas加一个 Text 显示分数。敌机销毁时加分玩家销毁时扣一条命演示时至少要有 Game Over 状态否则演示到一半飞机死了画面就僵在那里。用静态变量省去了各种获取引用的麻烦但对多场景项目要记得在开始时重置否则第二次进入游戏分数会继承上一次的结果。我的计分核心逻辑是这样public class GameState : MonoBehaviour { public static int score 0; // 总分 public static int lives 3; // 剩余命数 public Text scoreText; // 拖拽到 UI 面板 public static void AddScore(int point) { score point; } public static bool LoseLife() { lives--; if (lives 0) { Time.timeScale 0f; // 暂停游戏并显示 Game Over 面板 return true; } return false; } }在敌人脚本的 OnTriggerEnter 里调用GameState.AddScore(10)就完成了加分。Game Over 时Time.timeScale 0这招最省事全场物理和输入全部暂停但 UI 动画还能继续播放。注意如果你用了协程协程里的WaitForSeconds会被 timeScale 影响所以要给 WaitForSeconds 传true作为第二参数否则协程也会被暂停。5. 期末作业五大常见坑从穿模毁档到打包黑屏5.1 NullReferenceException代码挂错对象比逻辑错更常见现象一运行控制台立刻爆红通常是 Object reference not set to an instance of an object。最典型的场景是脚本挂在普通 GameObject 上但bulletPrefab和firePoint忘了从 Inspector 拖拽赋值代码在按键时才访问按键瞬间才报错。原因Inspector 里的公共变量没有赋值或者赋值了但在 Awake 被覆盖。很多同学会在 Start 里写GetComponent却没考虑脚本在场景加载时先于组件赋值执行。解决养成 Awake 里做判空检查的习惯并保证所有公共引用先拖拽赋值再运行。void Awake() { if (bulletPrefab null) Debug.LogError(bulletPrefab 未赋值请拖拽子弹预制体到 Inspector); if (firePoint null) Debug.LogError(firePoint 未赋值请把机头位置的空物体拖拽过来); }这条检查能省下答辩前最后十分钟的紧张局面。别问我是怎么知道的我见过某学弟演示前两分钟控制台刷了一屏红最后才发现脚本挂错了对象。5.2 飞机被自己的子弹打死碰撞矩阵没设好现象开火后飞机血条疯狂往下掉第一发子弹出去自己先炸了。原因默认碰撞矩阵里所有层都互相碰撞。子弹的 OnTriggerEnter 判断的是 EnemyTag 没错但如果玩家身上也误挂了 EnemyTag或者玩家碰撞体比视觉模型大了一圈子弹刚出膛就撞到了玩家自己的碰撞体。解决第一步先按 2.2 把 Bullet 层级和碰撞矩阵独立出来让子弹只碰撞 Enemy 层。第二步检查玩家身上是否误挂 EnemyTag这一步在演示项目里最容易被忽略因为复制敌机预制体改造成玩家时Tag 往往没改。第三步在子弹代码里加一层保险if (other.CompareTag(Enemy) false) return;这行代码码率高一点但能挡住奇奇怪怪的误伤情况。5.3 相机穿模与抖动把 Main Camera 抓进玩家子物体现象相机过一会儿就卡进飞机内部或者镜头剧烈抖动看起来像空间站没对接上。原因相机的 target 指向飞机模型的一个子物体而这个子物体在飞行过程中发生旋转或位置偏移导致 offset 计算错误。另一个常见原因是相机刚体或碰撞体没移除相机和飞机发生物理挤压。解决检查相机物体上有没有多余的 Collider 和 Rigidbody有就删掉相机只负责渲染不该参与物理。如果抖动只出现在高速飞行时把相机跟随的 smoothTime 从 0.2 调到 0.1并且确认移动用的是 SmoothDamp 而不是直接赋值直接赋值会导致镜头硬跟着目标跳看起来就像抖动。5.4 打包后没声音或没贴图场景没加进 Build 列表现象编辑器里一切正常导出后飞机是紫色、场景一片黑、音效全无。这三个问题各有各的原因但最常被忽略的只有一个。原因Build Settings 里的 Scenes in Build 列表遗漏了当前场景。编辑器测试时会自动加载当前场景打包后只加载列表里勾选的场景。漏掉场景的表现不是报错而是整个游戏画面黑屏或空场景。解决打开 File - Build Settings把当前场景拖进 Scenes in Build并确保列表第一个场景是游戏初始场景。音效没有则检查 AudioSource 是否设成 Play On Awake以及音频文件是否在 Audio 文件夹内被引擎识别。贴图发紫是材质球丢 Shader 或贴图丢失重新拖拽一遍贴图到对应槽位通常就好了。5.5 场景整体发黑没烘焙灯光或者后处理没关现象场景里明明放了好几个平行光和点光源运行后仍然黑到看不见飞机。原因Unity 的灯光在默认渲染管线下没问题但如果你用了 URP 或 HDRP场景灯光需要重新配置。另一个常见原因是创建场景时选择的是空 3D 场景默认 Skybox 是黑的背景一片黑玩家下意识以为整个场景坏了。解决在 Window - Rendering - Lighting 里把 Skybox Material 设回默认或者在相机上把 Clear Flags 设为 Solid Color背景选深蓝色。URP 用户要注意在 Render Pipeline Asset 里启用 Main Light 的阴影否则方向光不生效。做完这步如果还是黑的去检查平行光旋转角度是不是朝上照初级项目里 90% 的“整场全黑”都是平行光角度不对。6. 交付前十分钟自测检查清单和一键重置的小技巧交作业前十分钟我习惯按固定顺序自查一遍避免演示现场翻车。先打开 Build Settings 确认当前场景在列表里再运行游戏按顺序检查一、飞机上下左右移动是否跟手按方向键时镜头有没有抖动二、空格键发射子弹子弹是否能正常飞向屏幕中央命中敌机时有没有爆炸特效和加分三、连续几轮敌机生成后数量是否还在合理范围内会不会一次刷出二十个敌人四、故意让飞机被撞一次确认掉命逻辑生效Game Over 面板能弹出来五、暂停游戏检查 UI 分数和飞行状态是否被冻结。全过一遍只需要三分钟能拦下 90% 的低级事故。一个很实用的小技巧是给游戏做“一键重置”方便演示失败后快速恢复现场。不用跑回编辑器按停止再重新运行直接在游戏里按 R 键重新加载当前场景即可using UnityEngine; using UnityEngine.SceneManagement; public class ResetGame : MonoBehaviour { void Update() { if (Input.GetKeyDown(KeyCode.R)) { // 重置计分和命数再重载场景 GameState.score 0; GameState.lives 3; Time.timeScale 1f; SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } } }有些同学会在 Game Over 界面卡住因为 timeScale 被设成 0 后所有 Update 都停了重置脚本也失效。所以我习惯把重置脚本里的输入判断放在Input.GetKeyDown之前加上Time.timeScale 1f并且把重置脚本挂在一个不随游戏暂停的 UI 对象上。另一个替代方案是给相机挂脚本因为相机接收输入一般不受 timeScale 影响。最后说一个我这两年养成的习惯每写完一个系统就顺手在 Inspector 里把所有公共变量改成有意义的默认值再写一行判空。这比任何注释都管用因为期末作业阶段你不会记得三天前那个speed到底是 15 还是 50更不会记得自己有没有把bulletPrefab拖上去。项目越大这个习惯越能省命。这套流程我从头到尾写完一个可玩的太空飞机射击项目不到四个小时按这个顺序来你也能赶上截止时间。希望帮到你。本文还有配套的精品资源点击获取