做Unity初级案例做到第五部分,卡在14到17这四节是很多人的第一个坎。一听内容:添加失败和胜利的游戏UI界面、修改火花粒子系统、调整渲染层级关系、再把粒子系统显示在UI之前,每件事听起来都不大,但串在一起就绕。为什么UI老是盖住特效?为什么粒子调了半天还是一团白点?为什么同一个层级设置,UI和Sprite就是抢不过?这篇我把这四节按实战顺序重新拆一遍,顺便把渲染层级这个底层逻辑讲清楚。适合刚做完愤怒的小鸟核心玩法的初级学习者,也适合做完案例后想自己加界面、改特效的同学参考。
1. 先拆需求:这四节到底在解决同一个什么问题
1.1 四小节任务拆解
先别急着动手,把这四节的任务按“最终显示效果”捋一遍。
第14节做失败和胜利的UI界面,本质上是给游戏一个“状态出口”:小鸟用完没打掉目标,展示失败;目标清完,展示胜利。UI不是美术问题,而是一个游戏状态切换问题。
第15节修改火花粒子系统,本质上是让撞击反馈更可信。默认生成的粒子是白点、方形、没有生命力,看着像故障,不像撞击。改成橙黄色、有爆发感、迅速消失,玩家才会觉得“这一下真的打中了”。
第16节渲染层级关系,是这四节里最容易被跳过却最要命的一节。愤怒的小鸟是2D游戏,场景里的背景、柱子、小鸟、弹弓、粒子全部在同一平面显示,谁压谁完全靠渲染顺序决定。不搞懂这一节,后面所有特效层都会乱。
第17节把粒子系统显示在UI之前,是前两节的综合应用:火花的渲染层级要比UI更高,让撞击火花能够盖住按钮、分数条这些界面元素。表面上看是调一个Order,实际上牵扯到Canvas渲染模式、粒子Sorting Order、相机叠加方式。
所以这四节不是四个独立小技巧,而是同一个问题链:让游戏结束有一个明确界面,让撞击有反馈,让所有反馈按正确顺序出现在屏幕上。
1.2 渲染顺序的底层逻辑:谁先画,谁就被谁盖
理解这四节,核心是搞懂Unity的渲染顺序。顺序记不住没关系,但这个规则一定要吃透。
Unity渲染时大致按三个层次决定先后:相机深度、Sorting Layer、Order in Layer。相机深度管“哪个相机先渲染”,深度小的先画,深度大的后画;同一个相机内部,再按Sorting Layer和Order决定物体绘制顺序。Sorting Layer是一个大层,Order in Layer是层内的一张张小编号。系统先画Sorting Layer靠前的内容,再画靠后的内容;同一个Sorting Layer里,Order小的先画,Order大的后画。Order大的内容会盖住Order小的内容。
我习惯用一个比喻:Sorting Layer是好几摞透明胶片,Order是同一摞里编好号的单张。你不能跨摞比较编号谁大谁小,只有同一摞(同一个Sorting Layer)里Order大才有意义。如果两个对象在不同Sorting Layer,就只比层顺序,Order会被忽略。
这里有一个初级项目里最常见的误操作:两个对象明明不在同一个Sorting Layer,却在那儿拼命调Order in Layer,调半天没用,然后把锅甩给Unity。实际上只要把两个对象归到同一个层,或者把要显示在前面的那个对象放到更靠后的Sorting Layer里,问题立刻解决。
另外,Canvas的渲染顺序和Sprite不是一套完全相同的规则。Canvas有三种Render Mode:Screen Space Overlay、Screen Space Camera、World Space。其中Screen Space Overlay是“最后画”的,所有普通场景对象都画完后才画它,所以你平时看到的UI永远盖住3D和2D物体。想让粒子跑到UI前面,第一件事就是放弃这个默认的Overlay模式。这一点后面详细说。
2. 失败与胜利UI界面的完整搭建流程
2.1 Canvas骨架与两种面板
UI搭建部分不复杂,但步骤顺序对了,后面能少改很多。
第一步,在Hierarchy里右键,选择UI -> Canvas。如果场景里还没有EventSystem,Unity会顺手帮你创建一个。注意别把这个EventSystem删了,UI按钮点击全靠它。
第二步,在Canvas下创建两个Panel。右键Canvas -> UI -> Panel,一个叫WinPanel,一个叫LosePanel。Panel自带Image,默认是一块半透明白色底板,可以在Image组件里把Color改成带一点透明度的黑色,这样面板看起来就像弹窗背景。把两个Panel的锚点都拉成四角对齐(Anchor Presets选stretch全屏),这样在不同分辨率下都不会跑位置。
然后在每个Panel下加标题和按钮。标题用UI -> Text,按钮用UI -> Button——Button创建后会自带一个子物体Text,直接改文字就行。WinPanel里建议放两个按钮,一个“下一关”,一个“重新开始”;LosePanel里放一个“重新开始”就好。为什么这么放?因为胜利后玩家更大概率想继续往下打,失败后最明确的需求就是重来。
层级结构大概是这样:
Canvas ├── WinPanel │ ├── Title │ ├── NextButton │ └── RetryButton └── LosePanel ├── Title └── RetryButton两个面板做出来后,在Inspector里把它们的激活状态取消,也就是勾掉最左上角的复选框,或者用代码SetActive(false)。不要让游戏一开始就显示胜负界面。
这里给一个建议:用Unity新版默认的TextMeshPro还是老的Legacy Text?如果项目是从零开始,优先用TextMeshPro,它显示清晰、字体问题少。但如果这个愤怒的小鸟案例是跟着老教程走的老工程,Legacy Text也能用,没必要为了UI把整个项目重构一遍。
2.2 控制脚本:显示面板、重新开始、下一关
UI界面必须有人控制。写一个GameManager脚本,挂在场景里一个空物体上,负责显示胜利/失败面板、重载当前关卡、加载下一关。
using UnityEngine; using UnityEngine.SceneManagement; public class GameManager : MonoBehaviour { [SerializeField] private GameObject winPanel; [SerializeField] private GameObject losePanel; private bool isGameOver; private void Start() { winPanel.SetActive(false); losePanel.SetActive(false); } public void ShowWin() { if (isGameOver) return; isGameOver = true; winPanel.SetActive(true); } public void ShowLose() { if (isGameOver) return; isGameOver = true; losePanel.SetActive(true); } public void RestartLevel() { SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } public void LoadNextLevel() { SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1); } }是不是觉得这代码太简单?对,游戏结束UI的控制逻辑本来就不复杂,真正容易翻车的是“什么时候调用ShowWin和ShowLose”。在愤怒的小鸟案例里,一般是在关卡脚本结算时调用:所有目标被清除,调用ShowWin;可用小鸟用完且场上还有猪或箱子,调用ShowLose。调用时机比UI本身更重要。如果在自己项目里接到其他地方,记住一个原则:结算逻辑只执行一次,用一个bool像上面的isGameOver拦住,防止重复弹窗。
然后是把按钮和脚本连起来。选中WinPanel下的“重新开始”按钮,在Button组件的OnClick区域点击加号,把挂有GameManager的对象拖进去,从函数下拉列表里选GameManager -> RestartLevel。同理绑定“下一关”按钮到LoadNextLevel,LosePanel的“重新开始”也绑定RestartLevel。
2.3 几个容易忽略的UI细节
- EventSystem必须存在。如果场景里没有EventSystem,Button的点击事件会全部失灵。创建Canvas时Unity一般会生成,但如果你手动清理过场景,就要重新右键创建UI -> EventSystem。
- Canvas Scaler建议设置成“Scale With Screen Size”,参考分辨率设成1920x1080。不然在不同窗口比例下,UI要么被拉伸,要么缩到角落里。
- Panel底板会拦截鼠标事件。这不是坏事,但如果你发现点了按钮没反应,先检查按钮上方是不是有一层别的Image盖住它。
- 游戏结束弹出面板后,要冻结玩家输入。比如小鸟已经发射出去了,游戏结束界面出现时不能还让玩家有拖拽操作。
3. 火花粒子系统参数修改与触发控制
3.1 从默认白点到“撞击火花”的调参路线
刚创建的ParticleSystem默认是一堆白色小方块持续向外飘。这个效果放在游戏里没有任何说服力。修改火花粒子,不需要学特别高深的粒子知识,把下面几个模块按顺序调一遍就够了。
| 模块 | 参数 | 推荐值 | 作用 |
|---|---|---|---|
| Main | Duration | 0.3 | 火花只存在一瞬间,不是持续火焰 |
| Main | Looping | 取消勾选 | 播完就停,避免一直冒烟 |
| Main | Start Lifetime | Random:0.15~0.4 | 每颗粒子存活时间不同,更自然 |
| Main | Start Speed | Random:1~5 | 有快有慢,像崩溅而不是匀速飘 |
| Main | Start Size | Random:0.1~0.25 | 大小错落,别像统一发的豆子 |
| Main | Start Color | 橙黄色 #FFA500 | 撞击火花主色调 |
| Emission | Rate over Time | 0 | 不要持续发射 |
| Emission | Bursts | Time 0,Count 15~25 | 碰撞瞬间一次性喷出一簇粒子 |
| Shape | Shape | Circle,Radius 0.05~0.1 | 从撞击点小范围向四周扩散 |
| Color over Lifetime | Gradient | 白黄 -> 橙 -> 红 -> 透明 | 火花先亮后暗,符合视觉逻辑 |
| Size over Lifetime | Curve | 从1到0.2甚至0 | 火花变小的过程,不会突然消失 |
| Renderer | Render Mode | Billboard | 粒子始终面向相机,2D下不会看到扁片侧面 |
很多人在Color over Lifetime这一步容易翻车。你要的颜色变化不是Start Color里一个固定色,而是“粒子存活期间颜色随时间变化”。展开Particle System的Color over Lifetime模块,把Gradient从左到右拉几个关键点:最左边偏白黄,中间橙,最后偏红并把透明度降到0。这一条渐变拉出来,火花立刻有内味。
另一个关键参数是Emission模块下的Burst。默认Rate over Time是10,它会一直冒粒子,这不是我们要的。把Rate over Time改成0,然后点击下图中的加号添加一个Burst,Time保持0,Count设成15到25。意思是“第0秒一次性喷25粒”,之后就没了。配上Duration 0.3秒,整体就是一次短促爆发。
3.2 播放时机:撞击瞬间触发,而不是一直开着
粒子参数调好了,还得控制播放时机。不可能让粒子系统一直在场景里喷,否则满屏都是火星。
一种最直接的做法是:把粒子系统做成独立对象,挂在场景里;当小鸟碰撞发生后,把粒子对象移动到碰撞点,然后执行播放。
using UnityEngine; public class CollisionFX : MonoBehaviour { public ParticleSystem spark; private void OnCollisionEnter2D(Collision2D collision) { if (spark == null) return; spark.transform.position = collision.contacts[0].point; spark.Stop(); spark.Clear(); spark.Play(); } }这段代码里有三个方法值得说。Stop是让粒子系统停下;Clear是把还没播完的残留粒子清掉;Play是重新开始播放。很多人只调用Play,结果发现第二次碰撞火花比第一次少,或者上一帧的残留和下一帧混在一起。因为粒子系统在Looping=false时虽然播完会停,但如果上一次还没播完又被调用Play,状态会叠加。固定的“Stop -> Clear -> Play”三连,是处理一次性粒子的标准动作。
还有一个更隐蔽的坑:如果粒子系统是小鸟的子物体,而小鸟在撞击后马上被销毁,粒子系统也会跟着销毁,火花根本来不及显示。建议把粒子系统独立放在场景里,甚至专门做一个“特效管理器”,用代码控制播放位置。初学者最容易在这里卡住,因为粒子参数调得很好,但一开打火花就消失,最后发现是父物体销毁导致的。
4. 渲染层级关系:Sprite、Canvas和Particle到底谁在上
4.1 在项目里创建并分配Sorting Layer
愤怒的小鸟这种2D游戏,场景里背景、地面、柱子、小鸟、弹弓、粒子,全部是SpriteRenderer或者ParticleSystemRenderer。它们没有Z轴深度排序(就算有也不可靠),视觉上谁压谁完全靠Sorting Layer体系。
打开Project Settings -> Tags and Layers,在最下面可以看到Sorting Layers。默认只有一个Default。点击右侧加号,添加几个自定义层。我的习惯是加一个Background、一个Ground、一个Foreground、一个FX。这样背景放Background,地面和场景建筑放Ground,可交互角色放Default或者再加一层,粒子、刀光这类特效放FX。
这里要强调一下Unity的绘制顺序:Sorting Layers列表里的层,从上到下依次绘制,越靠下越晚绘制,所以越靠下的层越容易出现在画面最前面。Order in Layer是在同一个Sorting Layer里比较,数值越大越晚绘制。
以愤怒的小鸟为例推荐这套分配:
| 对象 | Sorting Layer | Order in Layer |
|---|---|---|
| 背景 | Background | 0 |
| 地面、柱子 | Ground | 0 |
| 小鸟、弹弓 | Default | 10 |
| 火花粒子 | FX | 10 |
实际分配的时候不要死板,重点是要让“特效层”和“场景层”分开。如果后续发现小鸟被柱子挡住,但你又希望小鸟在前面,就把小鸟的Order调大,或者把小鸟放到比Ground更靠下的Sorting Layer里。
4.2 Canvas的排序和Sprite排序不是一个体系
这是第16节里最核心的认知点。
SpriteRenderer、ParticleSystemRenderer的排序走的是Sorting Layer和Order in Layer。但UI Canvas不一样,它的排序看Render Mode。
- Canvas是Screen Space Overlay时:Unity会在所有3D/2D场景对象渲染完之后,最后把UI画到屏幕上。不管你把粒子Sorting Order调到多高,都不可能盖住Overlay UI。Overlay模式是硬优先级。
- Canvas是Screen Space Camera时:UI会被挂在指定相机下,作为场景渲染的一部分,此时Canvas组件上的Sorting Layer和Order in Layer才真正参与到和Sprite、Particle的排序比较中。
- Canvas是World Space时:更接近一个放在世界空间里的3D画布,同样参与Sorting Layer排序。
所以在愤怒的小鸟案例中,如果想让火花粒子显示在UI之前,第一步就是把Canvas从Overlay切到Screen Space Camera。不切这一步,后面所有关于Sorting Order的讨论都白搭。
5. 粒子系统显示在UI之前的三种落地方式
5.1 先说结论:Overlay模式下别想用排序压过UI
很多人在第17节的操作是:选中粒子系统,把Renderer模块下的Order in Layer调成100,UI还是从上面盖下来。又去调Collision,把UI的Order调成0,粒子照样被盖。原因就是上面说的:Canvas是Screen Space Overlay,它的渲染优先级对所有场景对象是“绝对最上层”。
所以第17节的正确起点:把Canvas从Screen Space Overlay改成Screen Space Camera。这不是可选项,是必要条件。
5.2 方案一:Canvas切到Screen Space Camera,用Sorting Order控制层级
这是最贴近愤怒的小鸟案例、也最推荐的方案。
第一步,选中场景里的Canvas,把Render Mode改成Screen Space Camera。Canvas组件上会出现一个Render Camera字段,把主相机拖进去。Plane Distance默认是100,如果你发现UI显示不出来或者被裁剪得厉害,就把Plane Distance调小一点,比如1到5。这个值等于UI和相机之间的虚拟距离。
第二步,找到粒子系统身上的ParticleSystemRenderer,这通常在Particle System组件最下面的Renderer模块里。把Sorting Layer设为和Canvas同一个层,比如都放在Default(说“同一个层”是为了让下面的Order比较生效);然后把Order in Layer设成一个比Canvas大的数,比如粒子设10,Canvas设0。
第三步,确认Canvas上的Order in Layer。当Render Mode切到Screen Space Camera后,Canvas组件上会出现Sorting Layer和Order in Layer选项,默认Order是0。粒子的Order是10,大于0,所以粒子会在UI之后绘制,自然显示在UI前。
你可能会问:主相机还要渲染场景里的柱子和背景,它们的Sorting Order是0,粒子也是10,是不是粒子也会盖住场景?对,正是想要的效果。火花作为特效,本来就该显示在场景前面,只是之前被UI挡住。改成这个方案后,粒子的视觉层级变成“盖住场景,也盖住UI”,满足第17节的目标。
这一方案还有一个附带好处:粒子仍然由主相机正常渲染,不会额外增加相机开销。缺点是要去调Canvas模式,可能会让部分跟着老教程走的人不适应。不过这是标准做法,值得习惯。
5.3 方案二:用单独相机在UI之上叠加特效
如果你不想动Canvas的排序,但又确实需要粒子出现在UI前方,有一种常见替代:给特效单独做一个相机,让这个相机在所有主相机渲染完成后再渲染一次。
思路是这样的:把粒子放到一个单独Layer比如FXOnly,主相机的Culling Mask里取消勾选FXOnly,不让主相机画粒子。然后新建一个特效相机,Culling Mask只勾选FXOnly,Clear Flags改成Depth Only,Depth设得比主相机和UI相机都大。因为相机的Depth越大越靠后渲染,特效相机就能把粒子画在前面。
这个方案在做全屏大爆炸、伤害数字飘屏、屏幕特效时很实用,因为它不污染主相机的场景排序。但要注意:如果UI还是Overlay模式,特效相机再靠后也没有用,Overlay UI最终还是会压在所有相机内容上面。所以方案二在“粒子盖UI”这个具体目标下,同样要求你放弃Overlay模式,至少把UI放到某个相机渲染流程里。
给初学者的建议:如果只是为了愤怒的小鸟里一个碰撞火花,没必要上双相机。方案一是最优解。
5.4 方案三:把粒子当UI元素做
还有一种思路是把粒子系统放进Canvas下面,让它作为“UI粒子”参与Canvas内部渲染。Unity有一些开源的UI粒子组件,原理通常是通过自定义CanvasRenderer或者特殊Shader,让粒子可以跟Image、Button一起排UI层级。
这种方案适合什么场景?当你要做“火花从按钮边缘炸开”“角色头像暴击特效”“飘出伤害数字”这类和UI位置强绑定的效果。但初级案例一般不需要,而且引入额外组件容易让新手分不清粒子还是UI。
如果你手头没有现成组件,只想在Canvas下面挂一个ParticleSystem,让它的Sorting Order比按钮高,大概率还是不生效,因为普通ParticleSystemRenderer不是Canvas渲染管线里的东西。所以这一节的做法就是:不推荐在上手阶段折腾。
5.5 三种方案怎么选
直接给结论:
- 常规2D游戏,让粒子盖住UI,首选方案一。它只改一层Canvas模式和两个Order,无额外开销,逻辑很清晰。
- 粒子数量大、要单独管理、想做全局特效时,考虑方案二。前提是你已经理解相机Depth和Culling Mask。
- 粒子必须和UI元素强绑定,比如必须从某个按钮的位置炸开,再考虑方案三。新手先跳过。
6. 常见问题排查:UI、粒子与层级故障速查
6.1 Canvas改成Screen Space Camera后UI不见了
这是方案一最常见的“翻车现场”。多半原因是没有给Canvas指定Render Camera,或者Plane Distance设置不合理。Render Camera一空,UI就没有渲染载体。指定主相机后,把Plane Distance从默认的100往下调,UI一般就会重新出现。如果你场景里运动相机带大量位移,还要注意UI会跟着相机动,这是Screen Space Camera模式的正常行为。
6.2 火花粒子始终显示不出来
第一个要查的,不是粒子参数,而是粒子对象挂在谁下面。如果它挂在小鸟身上,小鸟又因为碰撞被Destroy,粒子系统会和父物体一起消失。这时候就算调参调得再漂亮,也看不到。解决办法是把粒子系统独立出来,碰撞发生时用脚本移动它的位置再播放。
第二个要查的是Camera的Culling Mask。如果粒子的Layer不在主相机渲染范围内,它也永远不会出现。检查粒子所在的Layer,确认主相机的Culling Mask包含了它。
第三个要查的是Emission的Burst。Burst只在指定时间触发一次,如果粒子系统在Awake/Start阶段已经自己播完一遍,你后面才挂上去触发,看起来就像没播。配合脚本用Stop -> Clear -> Play来重置状态。
6.3 粒子还是被UI挡住,或者UI被粒子挡住
如果粒子还是被UI挡,几乎可以锁定原因:Canvas仍然在Overlay模式。检查Canvas的Render Mode,必须改成Screen Space Camera,然后确认粒子的Order in Layer大于Canvas的Order in Layer。
反过来,如果粒子盖住了UI而且挡住了太久,那是火花生命周期太长。火花是瞬间效果,Duration和Start Lifetime不要给太长,否则玩家整个游戏过程都在被粒子遮视线。愤怒的小鸟里,火花总时长控制在0.3秒左右就很合适。
6.4 UI按钮点了没反应
检查场景里有没有EventSystem。没有EventSystem,所有按钮点击都无效。如果EventSystem存在,检查按钮上方是否被一个透明Image挡住。Panel自带的Image就是常见元凶,它会把点击事件吃掉。解决方式是调整UI的Hierarchy顺序,让按钮在Panel之上,或者去掉Panel上多余的Image组件。
还有一种是Canvas改成Screen Space Camera后,EventSystem的Event Camera已自动关联,但如果你的项目里有两个相机,要确保EventSystem的Event Camera指向实际渲染UI的那个相机。
6.5 常见问题速查表
| 现象 | 首先排查什么 | 建议处理方式 |
|---|---|---|
| 粒子被UI罩住 | Canvas Render Mode是否为Overlay | 改为Screen Space Camera,粒子的Order调大 |
| 粒子根本不显示 | 粒子对象是否挂在被销毁的物体下 | 粒子独立出来,碰撞时移动位置再播放 |
| UI切换相机后消失 | Render Camera是否为空,Plane Distance是否合理 | 指定主相机,调Plan Distance |
| 按钮点击无反应 | EventSystem是否存在,是否有透明Image盖住按钮 | 创建EventSystem,调整UI层级 |
| 场景中Sprite层级错乱 | 两个对象是否在同一Sorting Layer | 先统一Sorting Layer,再用Order in Layer微调 |
| 火花表现像是持续冒烟 | Emission模块Rate over Time是否没清零 | Rate over Time改成0,改用Burst一次性爆发 |
最后说一个我自己的排查习惯:遇到显示顺序不对,先别急着改粒子参数,把每一个关键对象的Sorting Layer和Order in Layer在Inspector里截个图列出来,再看一眼Canvas的Render Mode,问题往往一眼就能揪出来。在愤怒的小鸟这个案例里,把层级关系弄明白,比多调几次粒子参数重要得多。以后做炸弹爆炸、金币飘落、伤害数字冒字,你会一次次遇到今天这个问题,而解法都是同一个套路。