
简介这是一份面向Unity 2D游戏开发初学者与进阶学习者的「雷霆战机」完整演示工程适合想通过经典竖版飞行射击案例掌握Unity 2D核心开发流程的开发者。资源包共1740个文件约455.24MB涵盖105个dll运行库、24个mat材质、17个unity场景、6个prefab预制体、6个cs脚本以及png贴图、mp3音效、json配置等完整保留了工程资源与依赖结构。已有513人学习下载说明该案例在同类教程中具备一定参考价值。读者可从中获得可直接运行的雷霆战机演示项目用于研究敌机生成、子弹碰撞、分数统计、场景切换等常见玩法的实现方式也可对照prefab与脚本组织方式理解Unity 2D工程的目录规范适合作为课程设计、毕业设计或自学练手的参考素材。1. 雷霆战机 Demo 拆解一份能跑起来的 Unity2D 射击游戏骨架很多人找 Unity2D 案例最怕下到那种“只有场景没有逻辑”的空壳工程——打开能看动不了改不动。这份雷霆战机演示文件不一样它是一套完整的竖版飞行射击玩法骨架玩家战机跟随触屏或鼠标移动、子弹按射速自动发射、敌机按波次生成并沿预设轨迹下压、碰撞后扣血或销毁、分数与生命值实时刷新。整套逻辑用 C# 写在 MonoBehaviour 上没有引入任何第三方插件Unity 版本兼容性做得比较宽。适合两类人一是刚学完 Unity 基础、想找一个能拆能改的完整小项目练手的新手二是需要快速搭一个 2D 射击原型、验证玩法手感的老手。下面我按“资源是什么、怎么跑、怎么改、坑在哪”的顺序把这份 Demo 拆开讲。2. 工程结构与核心脚本先看清骨架再动手2.1 目录层级与场景组织拿到工程后别急着点运行先花两分钟把目录看一遍。这份 Demo 的 Assets 目录结构是典型的 Unity2D 小项目组织方式没有过度分层找东西很快。常见做法是打开工程根目录重点看这几个文件夹目录/文件作用是否要改Assets/Scenes存放主场景通常命名为 Main 或 Game一般不动Assets/Scripts所有 C# 逻辑脚本核心改动区Assets/Prefabs玩家、子弹、敌机、爆炸特效预制体调参主要在这Assets/Sprites战机、子弹、敌机的 2D 贴图换皮时替换Assets/Audio射击、爆炸音效可选替换ProjectSettings物理、输入、渲染管线设置谨慎改场景里通常挂着一个空物体做管理器比如叫 GameManager 或 Spawner它负责生成敌机、刷新 UI、控制游戏状态。玩家、子弹、敌机都是独立预制体通过脚本里的引用互相调用。这种“管理器 预制体”的结构是 2D 射击游戏最省事的做法好处是改一个预制体不影响其他坏处是引用关系藏在 Inspector 里换场景容易丢引用。提示打开工程后先看 Console 有没有报错。如果提示脚本找不到或引用为空多半是预制体上的字段没拖不是代码问题。2.2 三个核心脚本的职责划分这份 Demo 的逻辑基本集中在三个脚本上理解它们的分工后面改起来就不会乱。第一个是玩家控制脚本通常叫 PlayerController 或 Player。它处理输入、移动边界限制、生命值。移动部分一般用Input.GetAxis或直接读鼠标/触摸位置然后transform.position做插值。边界限制用Mathf.Clamp卡住 x、y 坐标防止飞出屏幕。第二个是子弹发射脚本可能挂在玩家身上也可能单独做一个 Weapon 脚本。核心是计时器累加到间隔就实例化子弹预制体并给子弹一个向上的速度。射速、子弹速度、子弹存活时间都是公开字段方便在 Inspector 里调。第三个是敌机与碰撞脚本敌机自己往下移动碰到子弹或玩家时触发销毁和扣血。碰撞检测用 Unity2D 的OnTriggerEnter2D或OnCollisionEnter2D前提是至少一方有 Rigidbody2D且碰撞体勾了 Is Trigger。下面这段是玩家移动与边界限制的典型写法我按这份 Demo 的风格还原一下using UnityEngine; public class PlayerController : MonoBehaviour { // 移动速度Inspector 可调默认给 5 public float moveSpeed 5f; // 屏幕边界防止战机飞出可视区 public float minX -2.5f; public float maxX 2.5f; public float minY -4.5f; public float maxY 4.5f; void Update() { // 读取水平和垂直输入键盘和摇杆都走这套 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); // 按帧移动乘 deltaTime 保证不同帧率速度一致 Vector3 move new Vector3(h, v, 0) * moveSpeed * Time.deltaTime; transform.position move; // 用 Clamp 把坐标卡在边界内 float clampedX Mathf.Clamp(transform.position.x, minX, maxX); float clampedY Mathf.Clamp(transform.position.y, minY, maxY); transform.position new Vector3(clampedX, clampedY, transform.position.z); } }逻辑说明Update每帧读输入Time.deltaTime把速度从“每帧”换算成“每秒”这样高帧率和低帧率下手感一致。Mathf.Clamp是边界限制的关键三个参数分别是当前值、最小值、最大值。参数怎么改moveSpeed调大战机更跟手但太大容易穿模minX/maxX/minY/maxY要根据你的摄像机正交尺寸来定不是固定值。常见误用是直接写死transform.position new Vector3(h, v, 0)那样战机会瞬移手感全无。2.3 子弹发射与对象池的取舍子弹发射本身很简单难的是子弹多了之后性能掉。这份 Demo 如果用的是Instantiate和Destroy子弹一多就会触发 GC表现为偶尔卡一下。新手阶段先跑通没问题但要往正式项目走得知道对象池这条路。using UnityEngine; public class Weapon : MonoBehaviour { public GameObject bulletPrefab; // 子弹预制体 public Transform firePoint; // 发射点一般在机头 public float fireInterval 0.2f; // 射击间隔秒 private float timer 0f; void Update() { timer Time.deltaTime; if (timer fireInterval) { timer 0f; // 在发射点位置生成子弹旋转跟随发射点 Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); } } }逻辑说明timer累加每帧时间达到fireInterval就发射并清零。fireInterval越小射速越快0.1 就是每秒十发。firePoint是个空物体拖到机头位置子弹从那里出来。参数怎么改想改弹道就改子弹预制体上的速度脚本想改射速就改fireInterval。失败时看什么子弹不出来先看bulletPrefab有没有拖再看firePoint是不是空引用最后看子弹预制体有没有被误设成 inactive。对象池的思路是预先创建一批子弹用的时候激活、不用的时候隐藏避免频繁创建销毁。常见做法是写一个ObjectPool脚本用QueueGameObject存子弹Get时取出激活Return时隐藏入队。这份 Demo 如果没带对象池你可以自己加改动量不大但性能提升在弹幕密集时很明显。3. 从零跑通到改出自己玩法参数与预制体实操3.1 导入工程与版本兼容处理拿到工程压缩包后第一步是解压到一个纯英文路径下路径里不要有中文和空格这是 Unity 的老规矩中文路径偶尔会让资源导入失败。然后用 Unity Hub 添加这个工程目录Hub 会提示你选择 Unity 版本。这份 Demo 没有绑定很新的版本常见做法是用 Unity 2020 LTS 或 2021 LTS 打开兼容性最稳。如果你本地只有更新版本比如 2022 或 2023打开时可能会弹“升级 API”提示点确认即可2D 射击用到的 API 很基础升级一般不会出问题。打开后等资源导入完成找到 Scenes 文件夹下的主场景双击打开点播放键。如果播放后黑屏先看摄像机。2D 游戏摄像机一般设成正交投影Size 决定可视范围。如果 Size 太大战机会小得像蚂蚁太小边界限制的数值就对不上。常见做法是把摄像机背景设成深色Size 调到 5 左右然后根据实际画面调玩家脚本里的边界值。注意导入后如果材质显示成粉色说明渲染管线不匹配。这份 Demo 大概率用的是内置管线如果你工程设成了 URP需要把材质换成 URP 的 Sprite 材质或者把工程切回内置管线。3.2 敌机生成与波次控制敌机生成一般由一个 Spawner 脚本负责挂在场景里的管理器上。核心逻辑是定时在屏幕上方随机 x 位置生成敌机敌机自己往下移动。using UnityEngine; public class EnemySpawner : MonoBehaviour { public GameObject[] enemyPrefabs; // 多种敌机随机选 public float spawnInterval 1.5f; // 生成间隔 public float spawnY 6f; // 屏幕上方生成 public float minX -2.5f; public float maxX 2.5f; private float timer 0f; void Update() { timer Time.deltaTime; if (timer spawnInterval) { timer 0f; // 随机选一种敌机 int index Random.Range(0, enemyPrefabs.Length); // 随机 x 位置y 固定在屏幕上方 float x Random.Range(minX, maxX); Vector3 pos new Vector3(x, spawnY, 0); Instantiate(enemyPrefabs[index], pos, Quaternion.identity); } } }逻辑说明enemyPrefabs是数组可以放多种敌机Random.Range随机挑一个。spawnInterval控制难度越小敌机越密。spawnY要设在摄像机可视范围上方否则敌机会凭空出现。参数怎么改想加难度曲线可以把spawnInterval做成随时间递减比如spawnInterval Mathf.Max(0.3f, spawnInterval - Time.deltaTime * 0.01f)。失败时看什么敌机不出现先看数组是不是空的再看spawnY是不是在摄像机范围外太远。敌机自身的移动脚本通常很简单transform.Translate(Vector3.down * speed * Time.deltaTime)就够。如果要曲线轨迹可以用Mathf.Sin做左右摆动或者用 DOTween 这类补间插件但这份 Demo 为了零依赖大概率是纯代码移动。3.3 碰撞、扣血与游戏结束判定碰撞是射击游戏的核心交互。Unity2D 里两套碰撞回调OnCollisionEnter2D用于物理碰撞OnTriggerEnter2D用于触发器。射击游戏一般用触发器因为不需要物理反弹。要让触发器生效两个物体至少一个带 Rigidbody2D且碰撞体勾上 Is Trigger。子弹撞敌机通常子弹是触发器敌机是触发器或普通碰撞体都行。回调里判断标签然后扣血或销毁。using UnityEngine; public class Bullet : MonoBehaviour { public int damage 1; public float lifeTime 3f; // 子弹存活时间防止飞出屏幕不销毁 void Start() { // 到时间自动销毁避免子弹无限累积 Destroy(gameObject, lifeTime); } void OnTriggerEnter2D(Collider2D other) { // 只对敌机生效 if (other.CompareTag(Enemy)) { // 给敌机扣血敌机脚本里处理死亡 other.GetComponentEnemy()?.TakeDamage(damage); // 子弹命中后销毁 Destroy(gameObject); } } }逻辑说明Destroy(gameObject, lifeTime)是定时销毁防止子弹飞出屏幕后一直存在。OnTriggerEnter2D的other是碰到的碰撞体用CompareTag判断是不是敌机。GetComponentEnemy()?.TakeDamage用了空条件运算符敌机脚本不存在也不会报空引用。参数怎么改damage控制伤害lifeTime控制子弹存活一般 2 到 3 秒够用。失败时看什么碰撞不触发先确认至少一方有 Rigidbody2D再确认碰撞体勾了 Is Trigger最后确认标签拼写一致标签是大小写敏感的。游戏结束判定一般放在 GameManager 里玩家生命值归零时停掉生成、弹出结束界面。常见做法是用Time.timeScale 0暂停游戏再显示一个 UI 面板。注意暂停后按钮要用Time.unscaledDeltaTime或把按钮逻辑放在Update之外否则按钮也动不了。4. 避坑与排查这份 Demo 最容易翻车的五个地方4.1 现象播放后战机不动原因输入轴没配置或者脚本读的是触摸但你在用键盘。Unity 默认的 Horizontal/Vertical 轴是给键盘和摇杆的如果工程里 Input Manager 被改过轴名对不上就读不到值。解决打开 Edit Project Settings Input Manager确认 Horizontal 和 Vertical 存在且绑定正确。如果要做触屏得自己写触摸逻辑用Input.GetTouch或Input.mousePosition转换不能直接套键盘轴。4.2 现象子弹和敌机穿模碰撞不触发原因子弹速度太快一帧移动距离超过碰撞体厚度物理系统来不及检测。这是 2D 射击的经典问题叫“隧穿”。解决把碰撞检测模式改成 Continuous在 Rigidbody2D 的 Collision Detection 里选 Continuous。或者把子弹速度降下来同时把碰撞体做大一点。常见做法是子弹用触发器加 Continuous敌机碰撞体比贴图略大一圈。4.3 现象敌机越积越多帧率往下掉原因敌机飞出屏幕后没有销毁Instantiate出来的对象一直留在场景里。这是新手最常犯的错跑几分钟就卡。解决在敌机脚本里加Destroy(gameObject, 5f)或者判断transform.position.y -6f时销毁。更彻底的做法是上对象池但先加个定时销毁就能救急。4.4 现象换了自己的贴图后战机显示比例不对原因Sprite 的 Pixels Per Unit 不一致。Unity2D 里贴图的 PPU 决定一个 Unity 单位对应多少像素不同 PPU 的贴图放一起大小就会乱。解决选中贴图在 Inspector 里把 Pixels Per Unit 统一成同一个值比如都设成 100。或者调整 Transform 的 Scale 来补偿但统一 PPU 更规范。4.5 现象重新打开工程后预制体上的引用全空了原因脚本字段是 public 或[SerializeField]但引用的对象被删了或移动了位置。Unity 用 GUID 记录引用文件一挪GUID 对不上就丢。解决改预制体引用时尽量在 Inspector 里拖不要手动改.prefab文件。如果已经丢了重新拖一遍。预防办法是把关键引用在Awake里用GetComponent或Find兜底但Find性能差只适合初始化时用一次。5. 进阶把 Demo 改成可发布的弹幕玩法跑通之后这份 Demo 最大的价值是当骨架用。我一般会先做三件事加对象池、加难度曲线、加视觉反馈。对象池前面提过用Queue存子弹和敌机Get激活、Return隐藏。难度曲线把spawnInterval和敌机速度做成随时间递增比如每 30 秒提一档玩家能明显感觉到压力上来。视觉反馈是提升手感的关键命中时闪白、爆炸时生成粒子、屏幕轻微震动这几样加上去同样的逻辑玩起来完全不一样。网络热词里提到的 Unity2D 视觉特效资产其实就是这类粒子、拖尾、爆炸序列帧你可以从 Asset Store 找免费的 2D 特效包替换进去注意导入后检查材质和渲染层级。验证改动是否生效我习惯用一个笨办法在Update里Debug.Log关键数值比如当前敌机数量、子弹池剩余、玩家血量跑一分钟看日志有没有异常增长。日志干净了再关掉日志跑性能。另一个技巧是用 Unity Profiler 看 GC Alloc如果每帧都有几百字节的分配多半是Instantiate或字符串拼接在作怪对象池能把这部分压下去。最后说个我自己的习惯。每次改完一份 Demo我都会把改动前后的场景各跑一遍用手机录屏对比手感因为帧率、输入延迟这些东西光看代码看不出来。雷霆战机这类竖版射击手感差一帧玩家就能感觉到。从那以后我每次拿到新工程都强制先跑一遍原始版本、录个屏再动手改这样出了问题能立刻定位是自己改的还是本来就有。希望帮到你。本文还有配套的精品资源点击获取