简介:这是一份面向Unity 2D游戏开发初学者与进阶练习者的「雷霆战机」完整演示工程,适合用来学习经典竖版飞行射击游戏的实现思路,也可作为课程设计或自学练手的参考案例。压缩包为rar格式,共收录1740个文件,整体约455.24MB,其中包含105个dll运行库、24个mat材质、17个unity场景、6个prefab预制体、6个cs脚本以及png贴图、mp3音效、json配置等,覆盖工程资源、脚本逻辑与运行依赖,目录结构保留了完整的项目组织方式。目前已有513人学习下载,说明该案例具备一定的参考价值。读者可从中了解战机移动、子弹发射、敌机生成、碰撞检测与分数统计等核心模块的搭建方式,并借助现成场景与预制体快速还原游戏效果,对照脚本理解各功能之间的调用关系,从而缩短从零搭建射击类小游戏的学习周期。
1. 雷霆战机演示文件到底交付了什么:从一份 Unity2D 案例说起
很多人搜「unity2d游戏案例:雷霆战机演示文件」,脑子里想的其实是另一件事:我能不能拿一份现成的 2D 射击案例,把飞机、子弹、敌机、爆炸这套东西跑起来,然后改成自己的玩法。这个诉求非常具体,也非常合理。雷霆战机这类竖版飞行射击,几乎是每个 Unity 2D 新手绕不开的练手项目,因为它把 2D 游戏最核心的几块——精灵渲染、碰撞检测、对象池、UI 计分、音效触发——全塞进了一个体量可控的场景里。演示文件的价值不在于它多完整,而在于它把「一个能动的 2D 游戏」拆成了可读的脚本和预制体,让你能顺着结构去改数值、换素材、加敌机类型。这篇文章就按这个思路走:先讲清这类案例的工程结构,再落到能复现的脚本和参数,最后把踩过的坑摊开讲。适合已经装好 Unity、能新建 2D 工程,但还没独立做过完整射击玩法的从业者。
2. 拆开雷霆战机演示文件的工程结构:场景、预制体与脚本怎么分工
拿到一份 2D 射击案例,第一件事不是急着点运行,而是先看它的目录和场景层级。雷霆战机这类演示文件,结构通常高度相似:一个主场景负责游戏循环,若干预制体负责可复用对象,脚本按职责拆成玩家控制、子弹发射、敌机生成、碰撞结算、UI 更新几块。理解这个分工,后面改起来才不会牵一发动全身。
2.1 场景层级与渲染顺序:2D 图像堆叠为 3D 观感的底层逻辑
Unity 2D 本质上是把精灵按 Z 轴或 Sorting Layer 排序后正交投影,所谓「2d图像堆叠为3d图像」的观感,靠的就是渲染顺序加视差。雷霆战机演示文件里,场景层级一般长这样:Main Camera(正交,Size 约 5)、Background(星空,可做两层滚动)、Player、EnemySpawner、BulletPool、UI Canvas。渲染顺序由 Sorting Layer 决定,常见分层是 Background → Enemy → Player → Bullet → Effect → UI。如果子弹被敌机挡住,八成是 Sorting Layer 或 Order in Layer 没设对,而不是代码问题。
摄像机这块有个高频坑:正交摄像机的 Size 决定纵向可见范围,横版和竖版取值完全不同。竖版射击一般把 Size 设成 5,配合 16:9 屏幕,纵向能看到 10 个单位。如果你的飞机一出生就在屏幕外,先查这个值,再查 Player 的初始 Y 坐标。
提示:2D 项目里不要用透视摄像机做主体,透视会让精灵随距离缩放,碰撞盒和视觉对不上,排查起来非常费劲。
2.2 预制体与对象池:子弹和敌机为什么不能直接 Instantiate
新手写射击游戏最常见的写法是每次开火Instantiate(bulletPrefab),敌机也是生成一个销毁一个。这个写法在演示阶段能跑,但子弹一多就会触发 GC,帧率开始抖。雷霆战机演示文件里如果做了对象池,你会看到一个 BulletPool 脚本,里面维护一个Queue<GameObject>,开火时从池里取,命中后归还而不是 Destroy。
下面是一个最小可用的对象池实现,直接挂到场景里的空物体上即可:
using System.Collections.Generic; using UnityEngine; public class BulletPool : MonoBehaviour { public static BulletPool Instance; // 单例,方便子弹脚本调用 public GameObject bulletPrefab; // 子弹预制体 public int initialSize = 30; // 预生成数量,按最大同屏子弹数估 private Queue<GameObject> pool = new Queue<GameObject>(); void Awake() { Instance = this; for (int i = 0; i < initialSize; i++) { GameObject b = Instantiate(bulletPrefab); b.SetActive(false); pool.Enqueue(b); } } public GameObject Get() { if (pool.Count == 0) // 池空了就补一个,避免卡死 { GameObject b = Instantiate(bulletPrefab); b.SetActive(false); pool.Enqueue(b); } GameObject obj = pool.Dequeue(); obj.SetActive(true); return obj; } public void Return(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj); } }逻辑说明:Awake里预生成initialSize个子弹并全部禁用,避免运行时反复实例化。Get在池空时补生成,保证不会因为池耗尽导致开火失效。Return只做禁用和入队,不销毁。参数上,initialSize建议按「同屏最大子弹数 × 1.5」估,雷霆战机这类弹幕不密集的玩法,30 到 50 足够;如果是弹幕玩法,得往上加到 200 以上,否则补生成会频繁触发。
子弹脚本里归还的时机要卡在碰撞或出屏,两个地方都要写,漏一个就会泄漏:
void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Enemy")) { BulletPool.Instance.Return(gameObject); // 命中归还 } } void Update() { if (transform.position.y > 6f) // 出屏归还,6 是摄像机 Size 5 加余量 { BulletPool.Instance.Return(gameObject); } }2.3 脚本职责划分:玩家、敌机、生成器各管什么
演示文件里脚本一多就容易乱,判断标准很简单:一个脚本只改一类东西。PlayerController 只管读取输入和移动,不负责开火;PlayerShooter 只管发射节奏和子弹方向;EnemySpawner 只管生成位置和间隔;Enemy 只管自身血量和死亡表现。这样拆的好处是,你想把单发子弹改成三连发,只动 Shooter,不会碰到移动逻辑。
玩家移动用Input.GetAxisRaw还是GetAxis,直接影响手感。GetAxis带平滑,飞机有惯性感;GetAxisRaw是瞬时值,响应更硬。雷霆战机这类街机手感一般用GetAxisRaw,再自己加一点插值。移动边界用Mathf.Clamp卡住,别让飞机飞出屏幕:
void Update() { float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); Vector3 dir = new Vector3(h, v, 0).normalized; // 归一化,防止斜向更快 transform.position += dir * speed * Time.deltaTime; Vector3 pos = transform.position; pos.x = Mathf.Clamp(pos.x, -xLimit, xLimit); // xLimit 约 2.5,按屏幕宽算 pos.y = Mathf.Clamp(pos.y, -yLimit, yLimit); // yLimit 约 4.5 transform.position = pos; }normalized这行是血泪经验:不归一化的话,斜向移动速度是直向的 1.414 倍,玩家一按斜方向就明显变快,手感很怪。xLimit和yLimit要按摄像机可见范围减掉飞机半宽来设,竖版 16:9、Size 5 的情况下,x 方向约 2.5,y 方向约 4.5。
3. 让演示文件真正跑起来:从输入到碰撞的最小可玩闭环
结构看懂之后,下一步是让它动起来并且能打中东西。这一章把输入、发射、碰撞、计分串成一条线,每一步都给可抄的代码和参数。跑通这条线,你就有了一个能玩的最小闭环,剩下的敌机类型、道具、Boss 都是在这条线上加分支。
3.1 输入与移动:GetAxisRaw 和边界钳制的参数怎么定
输入映射在 Unity 里默认就有 Horizontal 和 Vertical,对应 WASD 和方向键。如果你要做手机端,得换成触摸拖拽,逻辑是记录Touch.deltaPosition累加到飞机坐标上,同样要 Clamp。演示文件如果只做 PC,保持默认映射即可。
速度参数speed的取值很讲究。竖版射击里,玩家从屏幕底部移到顶部大约 9 个单位,如果希望 1.5 秒走完,速度就是 6。实际手感上,5 到 8 之间比较舒服,低于 4 会显得拖沓,高于 10 容易撞弹。这个值不要写死在代码里,暴露成public float speed = 6f,在 Inspector 里调,调的时候直接运行看手感,比改代码重启快得多。
边界钳制有个容易忽略的点:飞机有宽度,xLimit要减去飞机半宽,否则飞机边缘会出屏。假设飞机精灵宽 1 个单位,屏幕横向可见 5.6 个单位(16:9、Size 5),那xLimit = 5.6/2 - 0.5 = 2.3。这个数算一次记下来,后面换素材要重算。
3.2 子弹发射节奏与方向:fireRate 和弹道偏移的调法
发射逻辑的核心是两个变量:fireRate(每秒发几发)和子弹方向。雷霆战机默认是向上直射,fireRate在 8 到 12 之间比较像原作。实现上用计时器累加,别用InvokeRepeating,因为后者不好动态改频率:
public float fireRate = 10f; // 每秒发射次数 private float timer; void Update() { timer += Time.deltaTime; if (timer >= 1f / fireRate && Input.GetKey(KeyCode.Space)) { timer = 0f; Fire(); } } void Fire() { GameObject bullet = BulletPool.Instance.Get(); bullet.transform.position = transform.position + Vector3.up * 0.6f; // 从机头出 bullet.transform.rotation = Quaternion.identity; }1f / fireRate是发射间隔,fireRate = 10就是每 0.1 秒一发。Vector3.up * 0.6f是让子弹从机头而不是机身中心出,0.6 约等于飞机半高加一点余量,具体按素材调。如果要做三连发,就在Fire里循环三次,每次给 x 方向加偏移,偏移量 0.2 到 0.3 之间,太大会散得不像样。
子弹速度一般设 12 到 18,太快看不清弹道,太慢追不上敌机。子弹预制体上要挂Rigidbody2D(Body Type 设 Kinematic)和CircleCollider2D(Is Trigger 勾上),Kinematic 是因为子弹不需要物理模拟,只要触发检测,这样性能更好。
3.3 碰撞检测与计分:Layer 怎么分、Trigger 怎么配
2D 碰撞在 Unity 里靠 Layer 和 Layer Collision Matrix 控制。演示文件里至少要有 Player、Enemy、Bullet、PlayerBullet 四个 Layer。在 Edit → Project Settings → Physics 2D 里,把不需要互相检测的组合取消勾选,比如 PlayerBullet 和 Player 之间不该有碰撞,取消后能省掉无意义的检测。
碰撞结算脚本挂在敌机上,用OnTriggerEnter2D接收:
public int hp = 1; public int score = 100; void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("PlayerBullet")) { BulletPool.Instance.Return(other.gameObject); // 子弹回收 hp--; if (hp <= 0) { ScoreManager.Instance.AddScore(score); gameObject.SetActive(false); // 敌机也走池或直接禁用 } } }hp和score暴露成 public,方便在 Inspector 里给不同敌机配不同数值。计分用单例 ScoreManager,内部维护一个 int 和 UI Text 引用,AddScore里更新文本。这里有个坑:UI Text 每帧刷新会有开销,正确做法是只在分数变化时更新,别放 Update 里。
注意:Trigger 检测要求至少一方有 Rigidbody2D。子弹有,敌机可以没有,但敌机如果也要移动,建议也挂一个 Kinematic 的 Rigidbody2D,避免移动时触发检测丢失。
4. 避坑与排查:雷霆战机演示文件里最容易翻车的五处
演示文件跑不起来或者跑起来不对劲,问题基本集中在下面五类。每条按现象、原因、解决写,照着查能省不少时间。
4.1 子弹打不中敌机:碰撞层级和 Trigger 的连锁问题
现象:子弹穿过敌机,没有任何反应,分数不涨。原因通常有三个:一是子弹或敌机的 Collider2D 没勾 Is Trigger;二是两者 Layer 在 Physics 2D 矩阵里被取消了检测;三是子弹速度太快,一帧跨过了敌机,触发检测漏掉。解决:先确认 Is Trigger 勾选,再查 Layer 矩阵,最后把子弹的 Rigidbody2D 的 Collision Detection 设成 Continuous,或者把子弹速度降到 15 以下。高速子弹漏检测是 2D 物理的经典问题,Continuous 能缓解但不能根治,弹幕玩法建议用射线检测代替。
4.2 飞机移动斜向变快:归一化漏写
现象:按斜方向时飞机明显比直方向快。原因:new Vector3(h, v, 0)在斜向时长度是 1.414,直接乘速度就快了。解决:加.normalized。这个坑几乎每个新手都踩过,代码里看到方向向量没归一化,基本就是这个毛病。
4.3 对象池归还后子弹还在飞:SetActive 和状态重置漏了
现象:子弹命中后归还池里,下次取出时位置还在上次命中的地方,或者速度不对。原因:归还时只SetActive(false),没有重置 transform 和速度。解决:在Return里或Get时重置位置、旋转、速度。更稳妥的做法是给子弹脚本加OnEnable,在里面重置状态,这样无论谁调用都保证干净。
4.4 分数 UI 不刷新或闪烁:Update 里刷 Text 的代价
现象:分数显示卡顿,或者数字跳变。原因:把text.text = score.ToString()放进了 Update,每帧都在改 UI,触发重建。解决:只在AddScore里更新一次。如果分数变化频繁,可以加个脏标记,LateUpdate 里统一刷。
4.5 打包后敌机不生成:协程在失活对象上跑不了
现象:编辑器里正常,打包后敌机不出现。原因:敌机生成用协程写在某个会被禁用的对象上,对象一失活协程就停了。解决:把生成协程挂在常驻的 Spawner 对象上,或者改用计时器在 Update 里累加。协程依赖 MonoBehaviour 的生命周期,这点在打包后更容易暴露。
5. 从演示文件到能改的工程:数值调优与扩展的实操技巧
演示文件跑通只是起点,真正有价值的是你能顺着它改出自己的东西。这一章讲几个我实际改这类案例时常用的技巧,都是能直接落地的。
先说数值调优的顺序。别一上来就改代码,先把所有可调参数暴露到 Inspector:玩家速度、子弹速度、发射频率、敌机血量、生成间隔、敌机速度。然后按「玩家手感 → 子弹节奏 → 敌机压力」的顺序调。玩家手感不对,后面全白搭;子弹节奏决定打击感;敌机压力决定难度曲线。调的时候用 Unity 的 Play 模式实时改,改完记下数值,别关掉就忘。
再说扩展方向。雷霆战机演示文件最容易加的三样东西:多弹道、敌机编队、道具掉落。多弹道就是在Fire里循环生成带角度偏移的子弹,用Quaternion.Euler(0, 0, angle)旋转方向。敌机编队用一个二维数组存生成模式,Spawner 按行读取,比硬编码灵活。道具掉落用概率触发,敌机死亡时Random.value < dropRate就生成道具,dropRate设 0.1 到 0.2 之间,太高会失去稀有感。
性能上,2D 射击游戏的主要开销在 Draw Call 和 GC。Draw Call 靠图集(Sprite Atlas)压,把同屏出现的精灵打进一个图集,能显著降 Draw Call。GC 靠对象池和避免字符串拼接,分数显示用StringBuilder或者缓存字符串,别每帧ToString拼。粒子特效是内存泄漏重灾区,特效播放完一定要销毁或归还池,别让它一直挂在场景里。
最后说验证方法。改完一个版本,别只看编辑器,打包一次跑真机。真机上的帧率、触摸响应、内存占用和编辑器差别很大。我一般会在真机上开 Profiler 看三样:GC Alloc 是不是每帧都有、Draw Call 有没有超 100、内存有没有持续上涨。这三样正常,基本就没大问题。
我自己改这类案例的习惯是:先把演示文件原样跑通,截一张运行图存着,然后每改一个功能就对比一次,确保没把原来的东西改坏。这个习惯帮我省过很多次回滚的麻烦。希望帮到你。
本文还有配套的精品资源,点击获取