1. 项目概述与案例价值拆解
1.1 这个案例到底在做什么
如果你正在用Unity做2D物理休闲游戏,这个案例几乎是绕不开的一课。标题里的"愤怒的小鸟"系列课程,到了第五阶段,其实已经脱离了最基础的"搭场景、绑脚本",开始进入游戏完整度打磨的关键环节:UI反馈、粒子特效和渲染层级。
我拆解一下这四个步骤的实际内容:
- 14添加失败和胜利的游戏UI界面:游戏不能只有玩的过程,输赢之后的反馈界面决定了玩家的完整体验。这个环节要制作胜利和失败两种状态的UI面板,并让它们按照正确的时机出现。
- 15修改火花粒子系统:小鸟撞击物体的碰撞特效,默认的火花粒子往往过于夸张或者尺寸不匹配,需要根据实际场景调整粒子的生命周期、颜色、大小曲线。
- 16渲染层级关系:UI、粒子、游戏世界三者的绘制顺序,直接影响画面是否正确显示。这一步涉及Sorting Layer、Order in Layer以及相机深度。
- 17把粒子系统显示在UI之前:这里有一个非常典型的Unity渲染坑——粒子系统的默认渲染层级在UI之下,导致喷溅的火花被UI面板遮挡,需要调整渲染顺序让粒子显示在UI上方。
这个案例适合两类人:一类是刚学完Unity基础、正在做第一个完整小游戏的新手,另一类是已经会做功能但不太清楚Unity渲染机制细节的初级开发者。前者可以从中学到UI面板搭建和事件回调的完整思路,后者正好补上Sorting Layer和粒子渲染顺序这块知识盲区。
1.2 为什么这个阶段是新手分水岭
做过几个小Demo之后你会发现,功能都做完了,但游戏"不好看""不完整"。问题往往不是出在功能逻辑上,而是出在这些容易被忽略的收尾环节。UI反馈、粒子特效和渲染层级,恰恰是判定一个游戏从"能运行"升级到"能玩"的关键。
很多自学者在完成核心玩法后,就停在了"弹弓能发射、小鸟能飞、猪能被打碎"这个阶段。接下来就不知道该干什么了。这个案例的四个步骤,正好填补了这个空白——它告诉你一个游戏做完核心玩法之后,还要经历哪些打磨步骤。
而且这四个步骤之间是有依赖关系的:先要有UI界面,才会暴露出"粒子被UI遮挡"的问题;先调整完粒子和UI,才能理清整棵场景树的渲染层级。跟着这个顺序做,你不会走弯路。
2. 胜利与失败UI界面的完整实现
2.1 UI面板搭建的两种路径
在Unity里做UI面板,很多新手的第一反应是直接用Canvas + Panel + Button堆出来。这种做法在小Demo里没有问题,但一旦界面变多,代码和UI的耦合会越来越严重。更常见的做法是做一个"UI管理器"来统一管理各个面板的显示和隐藏。
不过对于愤怒的小鸟这类单页面休闲游戏,我个人建议用最简单直接的方式:场景里放置两个独立的Canvas——一个用于游戏主UI(分数、弹弓剩余小鸟),一个用于结束面板。结束面板默认隐藏,只在游戏结束时弹出。
我在这里需要补充一个很多教程不会提的细节:结束面板的Canvas是否要单独创建的问题。如果你把胜利UI和失败UI放在同一个Canvas下,调整Raycast Target和父子遮挡关系时容易出各种小毛病。分开两个Canvas,各自负责自己的显示和隐藏逻辑,代码会清爽很多,Scene视图里的层级结构也一目了然。
实际操作时,我会按以下步骤搭建:
- 场景中创建一个新的Canvas,命名为"ResultPanel",Canvas Scaler选择Scale With Screen Size,参考分辨率设为1920x1080。
- 在Canvas下创建两个Panel,分别命名"WinPanel"和"LosePanel"。
- 每个Panel内部放一张背景图、一个主标题文本、一个按钮(继续/重玩)、可能再加一个分数提示。
- 两个Panel初始状态都设为SetActive(false),由脚本控制显示。
这里有个非常容易踩的坑:Canvas Scaler的参考分辨率不设置,或者设置得不匹配,会导致在不同分辨率设备上UI错位。尤其是手机横屏游戏,1920x1080是目前通用性较好的参考值,匹配度低的设备可以靠Match Width Or Height的值来折中。
2.2 游戏状态判断与UI触发逻辑
UI面板做好之后,接下来就是游戏状态判断。愤怒的小鸟的核心逻辑里,什么时候算胜利、什么时候算失败,很多新手会想得很复杂,比如要检测所有的猪是否都被销毁、所有的小鸟是否都已用完。
实际实现时,这个逻辑并不复杂。你只需要维护两个计数:
- 场上剩余的猪的数量,或者说是被销毁的猪的数量;
- 弹弓上和小鸟队列中,是否还有可用的小鸟。
比较推荐的做法是给猪添加一个死亡事件,比如当猪的HP归零或发生碰撞销毁时,发送一个消息给游戏管理器。游戏管理器收到消息后,将猪的剩余数量减一,然后检查是否为0,如果是0就把游戏状态切换为胜利。
我见过很多初学者的写法是把猪的销毁逻辑直接写在猪的脚本里,然后在猪的脚本里查找UI并显示。这样做短期能用,但代码结构会很混乱。更好的做法是借用事件或者直接通过静态的游戏管理器来统一管理:
// 敌人(猪)被销毁时调用 void OnEnemyKilled() { enemyCount--; if (enemyCount <= 0) { GameManager.Instance.ShowResult(true); } }实现GameManager的简单单例模式,是所有Unity初级开发者的必备技能。这里不展开讲单例的完整写法,只需要理解一点:UI显示这类全局性事件,尽量不要让每个个体对象直接去操作UI,而是把一个"游戏总管"作为所有事件的中转站。
在判断失败时有一个容易忽略的场景:小鸟飞出边界后又飞回来、或者小鸟被弹到屏幕外但还没完全销毁。对失败判定的时机要设置一个合理的延迟,通常是等最后一发小鸟碰撞停止、确认没有任何物体在运动之后,再显示失败界面。否则你会看到小鸟还在飞,失败面板就已经弹出来了,非常出戏。
2.3 UI面板显示的细节处理
UI显示这块,除了基本的SetActive(true),还有几个细节值得注意。
一个是按钮点击的响应逻辑。重玩按钮在点击之后,除了重新加载场景,还要确保所有状态都被重置,包括分数、剩余小鸟数量、猪的数量、UI面板状态。最简单的做法是直接SceneManager.LoadScene重启当前场景,这样状态天然全部重置,不需要手动恢复。如果游戏还包含跨场景的数据,这种做法就不合适了,需要把必要数据存到静态类或存档系统里。
另一个细节是按钮的Transition设置。Unity的UI按钮默认有Color Tint过渡,如果不设置高亮色和按下的颜色,用户在点击时会感觉整个UI是"死的",没有反馈感。我建议把过渡方式保持为Color Tint,然后稍微调亮按下时的颜色,交互手感会立刻提升。
还有一个很多人不知道的建议:不要在WinPanel和LosePanel里放两个一模一样的重玩按钮,然后分别绑定不同的方法。直接在UI管理器里绑定一个统一的重玩方法,两个按钮都调用同一个。这样后续修改逻辑时不用改两个地方。
3. 火花粒子系统参数调整实战
3.1 为什么默认火花粒子不好用
在Unity中创建粒子系统做碰撞火花,默认的Parameters会导致几个常见问题:
- 粒子太大太密,撞一下满屏都是火花,反而看不清核心的碰撞效果;
- 粒子颜色太亮(默认纯白),和场景风格不搭;
- 粒子生命周期过长,明明碰撞已经结束了,粒子还在飘,显得很拖沓;
- 粒子的Sorting Mode或者渲染层级不对,导致火花出现在错误的位置。
实际场景里,小鸟撞到木头、撞到玻璃、撞到猪,这三类碰撞的火花风格应该是不同的。木头碰撞偏向于碎屑飞溅,玻璃碰撞则更偏向于细小的闪光碎片。如果都使用同一套粒子参数,效果会非常违和。
但大部分初级课程里不会分别做三套粒子参数,通常只做一套通用的火花粒子,然后复制几个出来微调。这个案例的办法也类似:先在通用火花系统上调好参数,然后复制到不同对象上,再分别微调颜色和缩放。
3.2 核心参数调整方案
针对愤怒的小鸟这种偏卡通风格的2D游戏,我推荐一套比较稳妥的粒子参数起点:
- Duration(持续时长):直接设为0.1甚至更小。因为火花粒子是瞬时喷发型的,越短越干净。
- Start Lifetime(初始生命周期):0.3到0.6之间,根据火花是"碎片型"还是"闪光型"选择。碎片型短一点,闪光型可以略长。
- Start Speed(初始速度):3到8之间,速度太快会飞过屏幕,太慢又看不出爆炸感。
- Start Size(初始大小):0.1到0.3,具体取决于游戏视角尺寸。如果游戏相机是正交投影且size在5到10之间,这个范围是安全的起点。
- Max Particles(最大粒子数):50到80,不要贪多。粒子数量超过100,在移动设备上很容易掉帧,尤其是同时多个碰撞发生时。
- Gravity Modifier(重力影响):0.3到1,让火花在飞出后有一点下坠感,更加自然。
- Color over Lifetime(颜色随生命周期变化):开一条渐变色曲线,从亮黄色(或亮橙色)逐渐过渡到透明。这个渐变是火花效果是否精致的关键,纯色粒子看起来像贴纸。
这里我补充一个操作细节:最好在粒子系统的Start Rotation里加一点随机范围,比如0到360度,这样火花碎屑的方向不会千篇一律。Start Rotation的Randomize系数记得设到1。
3.3 用曲线还是用常量
很多新手在粒子系统里只设置常量,从不打开曲线编辑面板。但实际上,粒子效果的质感高低,往往就体现在曲线控制上。
以Size over Lifetime为例,如果你希望火花是"炸开之后迅速缩小消失",那就应该在Size over Lifetime区域添加一个曲线:起点1(也就是100%大小),终点0.1甚至0。这样的粒子的效果会比默认常量更有"膨胀-收缩"的节奏感。
另外,粒子系统的Renderer折叠区域里有一个关键项——Render Mode。默认是Billboard(公告板),这种模式在3D视角下是始终朝向相机的,但在2D游戏里,如果你希望火花是扁平贴着游戏平面的碎片,可以切换为Mesh模式,或者直接保留Billboard但把Material改成默认的Sprites-Default。很多教程不强调这个材质切换,结果粒子的显示效果和场景精灵不一致,看起来就像叠了一层发光滤镜。
我自己的经验是:2D游戏的火花粒子,用Sprites-Default材质配合Billboard模式,是最省心且效果最自然的组合。如果项目里用了URP,HDRP或内置渲染管线不同,粒子材质的选择又会不一样,这个具体项目需要具体分析,这里不再展开。
4. 渲染层级关系深度梳理
4.1 Sorting Layer与Order in Layer的关系
这个环节是很多Unity新手最困惑的地方。UI、2D精灵、粒子,这三种对象都涉及"谁画在谁上面"的问题。Unity的渲染顺序主要取决于下面几个因素:
- Camera的Depth(深度值):深度大的相机后渲染,后渲染的会盖住先渲染的。
- Sorting Layer(排序层):一个优先级直接决定的层,默认层Default,优先级最低。可以在Project Settings里添加自定义层。
- Order in Layer(层内排序):同一Sorting Layer内,数值大的渲染在前(盖住数值小的)。
在实际开发中,如果你有一个主相机负责渲染2D场景,又有一个单独的相机负责渲染UI并设置Clear Flags为Depth Only,那么UI相机的Depth通常设为更大的值,这样UI总是显示在场景上方。
但如果你只有一个相机,UI是放在单独画布上的,那么Canvas的渲染模式和Sorting Order会决定UI是否遮挡场景。最常见的做法是保持Canvas默认的Screen Space - Overlay模式,此时UI会自动渲染在所有非Overlay物体之上。
问题就出在粒子系统上:粒子系统默认属于Default Sorting Layer,它的Order in Layer默认为0。如果你的UI是Overlay模式,UI天然就在粒子的上面,无论你如何调整颗粒子的Order in Layer都无法盖到UI上方。这个限制导致我们需要用另一个方法来解决粒子在UI之前显示的需求。
4.2 相机设置与场景绘制的常见配合
如果场景复杂到需要多个相机配合,新手往往会把所有相机都设置为渲染整个场景,导致两个相机的画面叠加、闪烁。正确的做法是:
- 主相机(Main Camera)负责渲染游戏世界,Depth设为0;
- UI相机(如果有)负责渲染Canvas里的内容,Depth设为1或更高;
- UI相机的Culling Mask只选择UI所在层,Clear Flags选择Depth Only(只清深度,保留背景)。
场景中如果还有背景层、地面层、物件层等,应该通过Sorting Layer分好。比如我习惯设置这样的排序层:
| Sorting Layer | 用途 |
|---|---|
| Background | 背景、远景装饰 |
| Default | 地面、地形 |
| Gameplay | 主要游戏对象(小鸟、猪、木块) |
| Foreground | 前景遮挡物、动态特效 |
| UI | 通常UI不参与Sorting Layer,而用Overlay Canvas |
这里的关键点是,渲染层级不只是"看得到"的问题,还直接影响游戏手感。试想一下,当小鸟撞进一堆木块,木块碎屑飞起来时却显示在背景后面,玩家会明显感觉到画面违和。
4.3 粒子与UI遮挡问题的本质
在愤怒的小鸟这个案例里,当游戏结束时弹出UI面板,面板上写着"Victory"或"Game Over",但是如果小鸟撞击产生的火花粒子还在飞,而且这些粒子被UI面板遮住,玩家就看不到完整的碰撞反馈了。
这个问题的本质是:默认的UI为Overlay模式的Canvas会永远显示在3D/2D场景之上,包括粒子系统。要使粒子能显示在UI之前,有以下几种思路:
把粒子系统也放到UI层:把粒子放入Canvas子物体下,用RectTransform来控制位置,此时粒子的渲染会遵循UI的渲染顺序,可以显示在UI面板之上。但这个方案的缺点是粒子的坐标管理会在世界坐标和屏幕坐标之间来回切换,非常麻烦。
把UI拆分为两个Canvas:一部分UI(比如背景面板和按钮主体)放在一个Overlay Canvas中,而"火星粒子"的Canvas设置为Screen Space - Camera模式,并把该Canvas的Sorting Order设置为比主UI更高。然后粒子系统放在这个Canvas下,这样粒子就会渲染在普通UI之上。
调整相机方案:让UI采用Screen Space - Camera模式,而不是Overlay模式。此时UI不再永远顶层,而是作为Canvas,在相机裁剪范围内的某个平面上渲染。然后,把粒子系统和UI的Sorting Order进行比较,确保粒子的Order in Layer大于UI物体的Order in Layer。这种思路更接近"所有物体都在同一渲染空间下排序"的玩法。
对愤怒的小鸟这个案例来说,我推荐做一次对比实验,把第2种方案和第3种方案各做一遍,观察效果差异。因为这两种方案对后续项目的影响是非常大的:方案2会增加Canvas数量,方案3则对已有的UI系统调整要求更高。
5. 粒子显示在UI之前的实操与避坑
5.1 Screen Space - Camera改造步骤
根据推荐方案,我们采用"第二Canvas配合Screen Space - Camera"的做法来解决粒子显示顺序问题。以下是完整的改造流程:
第一步:创建UI专用相机
在场景中新建一个相机,命名"UICamera",将其Depth设置为高于主相机(比如主相机Depth=0,这个相机Depth=1)。同时设置该相机的Clear Flags为Depth Only,Culling Mask只保留UI层。这个UICamera的Projection可以保持Perspective,但如果你使用的是Screen Space - Camera模式,相机的FOV需要和主相机尽量一致,避免UI透视形变。
第二步:将主UI Canvas改为Screen Space - Camera模式
选中主Canvas,在Canvas组件中把Render Mode切为Screen Space - Camera,然后把Render Camera指定为UICamera。这时的UI平面会投射到UICamera的视角下。正常情况下UI的显示效果与Overlay模式几乎一样,但从此UI不再有"永远最顶层"的优先级。
这里需要特别注意一个参数:Canvas组件里的Plane Distance(平面距离)。默认值是100。如果设得太大,UI平面离相机太远,透视感会让UI看起来大小异常;如果设得太小,又容易出现UI穿模或者和场景物体交叉。2D游戏里建议从5到10之间开始调试。
第三步:创建粒子专用Canvas
再新建一个Canvas,命名"FXCanvas",同样设置为Screen Space - Camera模式,Render Camera同样指定为UICamera。在FXCanvas下创建一个空物体,挂上ParticleSystem或者直接把粒子系统拖到Canvas下使其成为子物体。
关键操作来了:把FXCanvas的Sorting Order设置为大于主Canvas的数值。比如主Canvas的Sorting Order是0,FXCanvas就设为1。这样,即使两个Canvas都在相机面前渲染,FXCanvas的粒子也会绘制在主Canvas的UI之上。
5.2 粒子坐标变换与位置对齐
粒子放进Canvas之后,最直接的变化是它的坐标会以UI坐标为准。如果你之前已经把粒子的位置保存在世界坐标中,放进Canvas后它的位置显示会很奇怪,甚至直接落在屏幕角落。
这里有两种处理思路:
- 如果粒子是全局特效,比如"全屏爆裂火花"之类,放入Canvas并用锚点调整位置即可;
- 如果粒子是跟随某个对象的局部特效,比如撞击某个木块时的火花,那么放弃"放进UI Canvas"的方案,改用3D世界坐标 + 调整Sorting Layer的方式。
在愤怒的小鸟这个场景中,最典型的是"小鸟撞到猪之后,火花在猪的位置喷发"。这种情况下,我推荐不把粒子放到FXCanvas,而是继续把粒子放在世界坐标中。然后我们使用另一种技巧:
给粒子系统所在的对象(或者粒子系统自己)添加一个Sprite Renderer或直接调整它的Order in Layer,同时把主Canvas的Render Mode保持为Screen Space - Camera,并且把主Canvas的Sorting Order设为较低值(比如0)。既然主Canvas不再处于Overlay模式,那么就不再有"UI永远最高"的限制。此时把粒子系统的Sorting Layer设为UI层,Order in Layer设为1,就可以让粒子渲染在UI之上,同时粒子依然留在世界坐标里,运动逻辑不用改。
这个方法是最省事的,也是我在实际开发中运用最多的方案。
5.3 一个容易忽视的坐标陷阱
在把粒子系统挂到Canvas下做UI特效时,还有一个让人崩溃的陷阱:粒子系统的Transform坐标会变成UI的局部坐标。如果你的粒子系统是动态生成的,比如代码里实例化一个粒子预制体,挂在Canvas子物体下面,它的Position字段显示的数字就不再是世界坐标了。
我在做类似项目时踩过一个坑:有一段代码用Camera.WorldToScreenPoint把世界坐标转屏幕坐标,然后赋值给粒子的Transform。把这个粒子挂在Canvas下面之前,这个逻辑是对的;挂到Canvas下面之后,因为Canvas本身的尺寸不一定和屏幕像素一一对应,这个转换就出现了偏差,火花喷发的位置整体偏移。
遇到这种情况,你可以有两种方案:一是把粒子放回世界坐标,按上一小节的办法调Sorting Layer;二是把粒子仍然放在Canvas下,但赋坐标时使用RectTransformUtility.ScreenPointToLocalPointInRectangle等UI专用的坐标转换方法。这两种方案都能解决问题,但显然第一种的代码改动更小,新手更友好。
5.4 粒子Sorting Layer与Canvas的互相影响
最后再提一个常见的渲染层级混乱场景:主UI是Overlay模式,而粒子被强行放到Default层且Order in Layer调得很高,但粒子依然显示在UI后面。这种情况会让很多新手误以为"Unity的UI和粒子渲染顺序是不受控的",其实只是因为你没有把UI从Overlay模式改成Screen Space - Camera模式。
我用一个对比表格来总结:
| 情况 | UI模式 | 粒子Sorting Layer/Order | 结果 |
|---|---|---|---|
| UI Overlay,粒子Default,Order=0 | Overlay | Default/0 | 粒子在UI后面(被遮挡) |
| UI Camera模式,粒子Default,Order=0 | Screen Space - Camera | Default/0 | 粒子可能在UI前面,可能后面,取决于相机与平面距离 |
| UI Camera模式,粒子UI层,Order=1 | Screen Space - Camera | UI/1 | 粒子必定在UI前面 |
| UI Overlay,粒子UI层,Order=1 | Overlay | UI/1 | 粒子依然在UI后面(Overlay模式UI优先级最高) |
把这个表存下来,遇到同类问题可以快速排查。这也是这个案例里"17把粒子系统显示在UI之前"的精髓所在。
6. 常见问题速查与实操心得
6.1 问题排查清单
在我带过的模拟项目和给同行做代码评审的过程中,这些环节的问题出现频率非常高。我整理了一份问题排查清单,对应这个案例的四个编号小节:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 胜利/失败UI没有按预期弹出 | 游戏状态计数逻辑不严谨,事件没被调用 | 检查猪的销毁事件是否确实发送,UI管理器是否监听 |
| UI弹出时位置偏移或大小异常 | Canvas Scaler参数没设置好 | 重置参考分辨率,调整Match Width or Height |
| 火花粒子稀疏或者完全没有 | Max Particles过小,Emission Rate过低 | 检查Emission模块的Rate over Time,必要时用Burst |
| 火花粒子显示在UI下面 | 主Canvas是Overlay模式 | 将主Canvas改为Screen Space - Camera,或单独建FXCanvas |
| 粒子挂到Canvas后位置错乱 | 坐标变换不一致 | 用RectTransformUtility做UI坐标转换或把粒子放回世界坐标 |
| 多个相机的画面重叠闪烁 | 相机Clear Flags和Culling Mask配置错误 | 设置Clear Flags为Depth Only,限制Culling Mask |
6.2 动手实验时的几条建议
做这个案例时,我建议你不要只照着视频抄一遍,而是至少额外做三个小实验:
第一个实验:把UI的模式从Overlay切换到Screen Space - Camera,然后故意改错Plane Distance,观察UI的缩放和透视变化。这个实验能帮你建立"UI其实是在一个三维平面上的对象"的空间感。
第二个实验:给同一个粒子系统分别设置Order in Layer为-1、0、1,并配合不同的Sorting Layer,观察最终显示顺序。把这个过程录下来,你会对Unity渲染顺序彻底通。
第三个实验:把胜利UI和失败UI分别使用两个Canvas,在两个Canvas之间切换显示,比较这种方式和单个Canvas切换面板的体验差异。两种做法没有绝对的优劣,但你会更清楚什么场景下选哪个。
6.3 从案例延伸到实际项目
愤怒的小鸟这个案例级别虽然不高,但它背后覆盖的渲染与UI知识,足够延伸到很多2D项目。只要你的游戏涉及中文或英文UI、需要特效反馈、有输赢判定,这四个步骤的思维方式就可以复用。
我个人的经验是,在做一个游戏Demo时,后20%的"收尾工作"往往决定游戏整体观感。UI反馈是否及时、特效是否自然、层级是否清晰,这些表面功夫恰恰是最能拉开作品差距的地方。同样是做一个弹弓发射游戏,A同学做到能发射就停了,B同学把胜利失败界面、碰撞火花、层级渲染全部打磨到位,两者的完成度完全是两个等级。
这个案例中涉及到的所有操作,我建议从头到尾手动做一遍,不要复制预制体了事。手动创建UI面板、手动调粒子曲线、手动设置Sorting Layer,会让你对这些系统的关联有更深刻的记忆。复制预制体确实快,但遇到问题时你就不知道从哪里排查了。
从这条路再往后走,可以继续学习对象池优化,解决粒子频繁生成导致的内存问题;也可以进一步学习自定义Shader,让火花粒子拥有更高级的混合模式;又或者接触DOTween,让UI面板的弹出和关闭带补间动画。愤怒的小鸟这个项目虽然简单,以此为跳板触达的Unity知识面,一点都不会浅。
最后说一个小技巧:调粒子参数时,不要只盯着Scene视图,建议把Game视图也打开,并且使用1倍分辨率预览。因为Scene视图下的粒子显示和实际Game视图往往差异不小,特别是半透明粒子的叠加效果。我在实际项目里因为偷懒只看Scene视图,浪费过不少时间在调一个实际渲染完全不同的效果上。先用Game视图确定大方向,再用Scene视图微调细节,效率会高很多。