ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity显示层级完全解析:UGUI与Sprite跨体系排序方案

Unity显示层级完全解析:UGUI与Sprite跨体系排序方案 1. 层级问题绕不开UGUI和Sprite是两套完全独立的排序系统刚接触Unity的开发者十有八九会在显示层级上栽跟头。最常见的一幕是游戏里明明把血条UI挂在了角色头顶运行起来却被地面上的草、或者其他Sprite给盖住了又或者做了一个弹窗结果怎么调都盖不住角色脚下的特效。查了半天代码没毛病最后发现是层级控制方式的锅。要理解Unity的显示层级第一步必须明白一件事UIUGUI和Sprite图片资源在Unity里走的是两套完全不相关的渲染排序逻辑。你可以在Inspector面板里把某个Sprite的Order in Layer调到1000但只要这个UI是用默认的Screen Space - Overlay方式渲染的它永远在所有Sprite之上。反之把UI挂到World Space之后它又变成了一个可以参与世界坐标排序的普通物体会被3D模型挡住。这两套体系分别是排序体系核心组件主要控制手段UGUICanvas, GraphicRaycasterCanvas的sortingOrder、Hierarchy中的兄弟节点顺序SpriteSprite RendererSorting Layer、Order in Layer、物体Z轴位置3D物体Mesh RendererRender Queue、深度缓冲、渲染顺序与本文主题弱相关很多做UI的同事只盯着Canvas里的层级看很多做2D游戏的同事只盯着Sorting Layer看但一旦两者出现在同一个画面里——比如AR游戏、2D角色扮演游戏、有大量技能特效和浮动战斗数字的游戏——就会立刻发现我明明把UI的层级调对了为什么还是被遮住答案就在这里你调的是UGUI体系里的层级Sprite根本不认这套。此外还需要理解一个底层概念渲染队列Render Queue。Shader里的RenderQueue大致分为Background(1000)、Geometry(2000)、AlphaTest(2450)、Transparent(3000)、Overlay(4000)这几档。3D场景中的不透明物体走Geometry遵循深度缓冲判定谁挡谁而UI、Sprite、粒子这类透明物体走Transparent队列深度写入默认关闭主要依赖CPU端的排序来决定先画谁后画谁后画的自然会盖住先画的。所以整篇博文的核心逻辑就是UGUI里排UGUI的顺序Sprite里排Sprite的顺序当需要跨体系控制遮挡时再通过Sorting Layer、sortingOrder和相机距离这三个公共维度把它们拉到同一把尺子上来比较。理清这一条主线后面所有的问题都有解。2. UGUI层级的三把钥匙Canvas sortingOrder、Hierarchy顺序、嵌套Canvas2.1 同一Canvas内Hierarchy中的先后顺序就是渲染顺序在同一个Canvas下面你不需要设置任何额外的数值Hierarchy面板中从上到下的顺序就决定了渲染从底到顶的顺序。也就是越靠下的物体显示的时候越靠上层越能遮挡别人。这个规则很多新手会弄反因为做UI设计的时候设计稿里的底层背景在图层面板里通常在最下方到了Unity里你想让背景垫底就得把它的GameObject放在Hierarchy的第一位而弹窗、按钮、提示文字依次往下排越重要的、越需要弹出的组件越靠后。举一个最直观的例子一个登录界面Canvas ├── Background 背景图Hierarchy最上面 ├── Logo Logo图 ├── InputField 账号输入框 ├── Button 登录按钮 └── TipsText 提示文字Hierarchy最下面永远最上如果运行后发现登录按钮被背景图盖住点不到第一反应不用去查代码直接看Hierarchy里Button是否在Background上面。很多时候UI显示不出来的根本原因不是代码逻辑而是新实例化的UI被SetParent之后默认挂到了父节点列表的末尾——如果不小心把弹窗挂到了背景下面它自然就隐身了。这种顺序规则在代码里也有对应的APItransform.SetSiblingIndex(int index)、transform.SetAsLastSibling()、transform.SetAsFirstSibling()以及查询用的transform.GetSiblingIndex()。后面第五节会专门讲动态排序时会用到这里先记住SetAsLastSibling()把当前节点移到父节点列表末尾变成最上层显示。2.2 多个Canvas之间sortingOrder决定谁压谁如果整个微信小游戏或手游只有一个Canvas那日子会好过很多。实际项目里几乎不可能只有一个Canvas主界面一个Canvas、弹窗一个Canvas、Loading一个Canvas、Joystick一个Canvas甚至每个UI模块独立一个Canvas来方便管理。这时候同一Canvas内部的Hierarchy顺序对跨Canvas的情况就不起作用了。多个Canvas同时存在时Unity先比较的是Canvas组件上的Sorting Order在Canvas组件上的Sorting Order字段数值大的显示在上层。如果多个Canvas的Sorting Order相同再回头看Hierarchy中这些Canvas的先后顺序。这是我踩过的一个典型坑主界面Canvas的Sorting Order是0弹窗Canvas的Sorting Order也设置成了0。逻辑上弹窗应该盖住主界面但在不同Canvas下层级相同得靠Hierarchy顺序而弹窗如果在主界面之前创建的话弹窗就不会显示在最上层。后面我统一规定凡是弹窗类CanvasSorting Order必须≥100Loading类≥1000Toast类≥2000才彻底根治了这类问题。2.3 Canvas的三种Render Mode各自影响什么Canvas组件上有一个Render Mode它决定的是Canvas本身跟相机的关系也直接决定了UI能不能被3D物体遮挡。Screen Space - Overlay完全贴在屏幕上永远渲染在所有东西的最前面。不需要关联相机不管你的3D模型离相机多近都遮不住它。方便但是物理上不真实而且UI上无法做任何被遮挡的效果。Screen Space - CameraUI会摆在指定的相机前方某个平面上跟3D物体处于同一个渲染空间中。此时UI是会被3D物体遮住的你能通过调整Canvas的Plane Distance来控制UI离相机多远。这是AR、FPS等需求下最常见的模式。World SpaceCanvas变成一个世界空间里的普通矩形平面可以在任意位置旋转和缩放像一面屏幕挂在3D场景里。此时UI再也不是Special的东西了它的渲染顺序完全取决于Sorting Layer、Order in Layer、到相机的距离等跟Sprite完全处在同一个坐标系里比较优先级。这里直接回答很多人的疑问我想让一个角色从UI后面走过去怎么做 答案很简单把UI从Overlay改成Screen Space - Camera或者World Space再让角色的渲染层级在UI之上即可。如果你是Overlay模式对不起物理上它永远在最前面角色无论在哪都只能从UI下面走。2.4 嵌套Canvas子Canvas是一个独立的子排序空间还有一种情况比较隐蔽Canvas下面挂着另一个带Canvas组件的子物体。Unity处理嵌套Canvas的规则是——子Canvas作为一个整体参与父Canvas的排序但子Canvas内部元素之间的顺序由子Canvas内的Hierarchy顺序决定不会再跟父Canvas的兄弟节点混排。举一个实际案例一个主界面Canvas下有一个弹出窗口子物体它带一个自己的Canvas组件Sorting Order保持默认0。同时这个弹窗里又挂了很多子物体。你想通过修改主Canvas下其他物体的顺序来让弹窗显示到某些物体之上、某些物体之下——这是做不到的。因为子Canvas一旦存在它内部就是一个独立的渲染单元。你唯一能控制的就是把弹窗这个子物体的位置放在主Canvas兄弟节点的某个位置以及设置它自己Canvas的Sorting Order。所以如果设计良好弹窗完全不应该做成嵌套Canvas而应该独立出一个顶级Canvas用Sorting Order统一控制。嵌套Canvas更多是用来做局部特殊处理比如某个面板整体要加Mask、整体要做一个特殊shader效果才考虑用子Canvas包一层。3. Sprite的显示层级Sorting Layer、Order in Layer和Y轴排序3.1 Sprite Renderer的优先级顺序说完了UI体系再看Sprite体系。2D游戏里所有图片元件都靠Sprite Renderer组件显示它的排序规则相对清晰优先级从高到低是Sorting Layer在Tags Layers里配置Order in Layer同一个Layer内比较数值大的遮住数值小的Z轴到相机的距离如果Layer和Order都相同离相机近的渲染在前会盖住后面的Y轴位置2D游戏的伪3D排序需要通过Sprite Renderer的Sprite Sort Point 相机设置配合开启这里最容易犯的一个错误是把Order in Layer当成了全局优先级两个角色的Order都设置了5却忘了这两个物体可能不在同一个Sorting Layer。如果A角色在Character层、B角色在Effect层而Effect这个Layer在Tag列表中排得比Character靠后那B角色哪怕Order in Layer设置为10也会被排序靠前的Layer里的物体压住。Sorting Layer的排序规则就是在Tags Layers窗口中从上到下依次优先级从低到高最上面的Layer优先级最低越往下越靠前。这就意味着如果单位、特效、场景背景分别放到三个不同的Sorting Layer那么背景在最上面最低优先级单位在中间特效在最下面最高优先级。实际操作中记得这点就行上面垫底下面显示在最顶。3.2 Order in Layer的规则与代码控制同一个Sorting Layer下面Order in Layer是Int数值类型。数值越大显示越靠前。默认都是0。我见过很多团队为了省事直接把所有Sprite都放在同一个Default Layer靠修改Order in Layer来控制单位前后关系这种做法虽然简单但也会埋下隐患——因为Unity对Sprite的绘制顺序不是按Hierarchy来的而是先把所有物体按Sorting Layer排序再按Order in Layer排序再按Z/Y排序。所以如果你在一个角色A的脚下放了一个阴影Sprite阴影的Order设成了-1角色A本身是0阴影就永远垫底但如果地面的Order也是0且绘制顺序排在角色A之后地面就会跑上来盖住角色产生穿帮。代码控制很简单SpriteRenderer renderer GetComponentSpriteRenderer(); renderer.sortingLayerName Character; // 按名字切换Layer renderer.sortingOrder 10; // 同Layer内排序还有一个开发技巧把不同的游戏对象按重要程度预设Order区间比如对象类型Sorting LayerOrder in Layer 范围地形/背景Ground-100 ~ -1常规单位Character0 ~ 50弹道/技能特效Effect50 ~ 200漂浮文字/血条UI_Like1000 ~ 2000这样写入一个规范后所有美术和程序员都按统一区间来就很少出现层级打架的情况。3.3 2D横版游戏常见的Y轴伪深度排序2D游戏里还有一个区别于普通UI排序的特殊场景两个角色在地图上的不同Y轴位置靠下的角色理论上应该遮挡靠上的角色因为更接近画面下方视觉上更靠前。这是小时候玩《仙剑奇侠传》这种俯视角/45度视角游戏时最常见的遮挡逻辑。Unity的Sprite Renderer支持这个把Sprite Renderer的Sprite Sort Point属性改成Pivot或者Center视素材锚点而定然后在Camera上勾选一个自定义的Transparency Sort Axis。默认轴方向是(0, 0, 1)也就是按Z轴排序当两个Sprite距离相机不同时做Z排序用的。改成(0, 1, 0)之后Unity就会按Y轴坐标去排序Y值越小越靠下的Sprite越优先渲染、越遮挡别人。我在项目里遇到过一个情况一个角色站在墙后面墙的Y轴和角色的Y轴正好重叠导致角色的半身从墙里浮了出来。后来处理方式就是把墙的Sorting Layer设为Obstacle并且所有Unit的Sprite与墙的Y轴排序解析方式改成像素级Pivot对齐解决了一大批类似的问题。这类Y轴排序的经验很多2D游戏项目都会遇到建议独立做一个排序脚本监听单位Y坐标变化动态更新List里的渲染先后顺序。3.4 Sprite的包围盒与剔除对层级的间接影响热搜词里反复出现了Unity Renderer的包围盒其实这个跟层级控制也有间接关系。Sprite Renderer渲染时Unity会计算包围盒Bounds来做视锥剔除如果包围盒被裁掉这个Sprite就不会被渲染也就谈不上层级了。在层级调试中如果发现某个Sprite偶尔消失不一定是排序问题有可能是它的Bounds被相机裁剪逻辑误判了——尤其是动态生成的Sprite、从图集动态换Sprite时Bounds往往需要手动刷新。核心调试方法就是选中物体查看Scene视图右下角的Bounds轮廓线如果轮廓线跟实际显示区域不匹配就是Bounds计算有错。这个跟遮挡关系看似无关但在排查显示异常时经常是同一个现象的两个不同原因。4. UI与Sprite的混合遮挡跨体系排序的关键打通方案4.1 为什么UI放到最后依然被Sprite盖住前面反复强调UGUI和Sprite是两套体系。但实际场景里它们往往需要互相遮挡比如角色从UI背后走过、血条UI的半截被城墙挡住、技能特效在UI文字前面飞。要实现跨体系的遮挡控制需要找到两者共用的排序维度。最大的关键点在于不管哪个体系渲染时最终都汇入同一个渲染队列排序依据都可以归结为三个公共维度Sorting Layer优先级最高、Order in Layer次之、到相机的距离最后。UGUI的Canvas组件上也有Sorting Order字段这个字段实质上就是Canvas的Order in Layer同时也支持给Canvas设置Sorting Layer在Canvas组件最下方。所以打通方案就出来了把UI从Screen Space - Overlay改成Screen Space - Camera并指定一个专门渲染UI的相机比如叫UICamera。在Tags Layers里创建公共的Sorting Layer比如3DWorld、“UI让3D/Sprite的Sorting Layer和UI Canvas的Sorting Layer统一在一张排序表里。想让Sprite遮挡UI就把Sprite的Sorting Layer设置在UI之上想让UI永远最上就把UI Canvas的Sorting Layer排在所有Sprite Layer的下面最上面并把Sorting Order设大。4.2 粒子特效和3D模型如何参与排序很多项目里真正的痛点不是Sprite而是粒子特效和UI的遮挡。比如技能大招的特效要盖住UI的按钮或者UI文字要浮在特效之上。粒子特效的Renderer模块上有和Sprite Renderer一模一样的Sorting Layer和Order in Layer属性。如果你使用的是默认Screen Space - Overlay的UI那粒子特效无论如何都盖不住UI。如果你使用的是Screen Space - Camera的UI那么粒子的Sorting Layer一旦排到UI之上粒子就会盖住UI。对3D模型Mesh Renderer来说情况稍复杂一点不透明的3D模型不参与Sorting Layer排序它走深度缓冲。这意味着哪怕你把一个不透明箱子的Sorting Layer设得再高只要UI平面在空间上离相机近箱子还是会被UI挡住深度写入和深度测试的逻辑。但如果你把箱子的Shader换成Transparent类型部分半透明、粒子这类它就会转入Transparent渲染队列重新参与Sorting Layer排序。实战建议如果只是想让某个3D模型盖住UI简单粗暴的方法是给这个模型单独建一个透明材质版本设置它的Sorting Layer高于UI如果这个模型必须是不透明材质则用Screen Space - Camera 调整UI相机的深度/裁剪距离来配合。4.3 跨体系排序的实际工程配置一个典型的跨体系项目比如2.5D卡牌游戏的Sorting Layer配置可以参考这样Tags Layers 中的 Sorting Layers从上到下优先级递增 1. Background 地图背景 2. Obstacle 墙体、树 3. Character 角色 4. Effect 技能特效 5. UI 所有UGUI Canvas 6. UI_AboveEverything Toast、Loading等UI然后在每个Canvas上设置主界面CanvasSorting Layer UI, Sorting Order 0弹窗CanvasSorting Layer UI, Sorting Order 300Loading/Toast CanvasSorting Layer UI_AboveEverything, Sorting Order 2000这样整个项目的层级体系就统一在一套Sorting Layer之下了UGUI和Sprite之间的遮挡只剩看谁的Layer更靠后这一个判断标准再也不会有那种明明调了Order却无效的玄学问题。5. 动态控制层级实例化、弹窗、拖拽时最常用的代码方案5.1 动态修改UI的兄弟顺序实战项目中UI往往不是静态摆好的而是代码动态创建、动态切换的。最常见的需求是点击某个按钮弹出一个面板这个面板要出现在最上层。用上前面讲的原理最靠谱的写法是在Show弹窗时public static void ShowPanel(GameObject panel) { panel.SetActive(true); panel.transform.SetAsLastSibling(); // 放到兄弟列表最末尾 渲染最上面 }因为同一个Canvas内部Hierarchy顺序直接等于渲染顺序SetAsLastSibling()就能把面板提到这个Canvas内所有UI之上。同理如果要做一个新创建的角色血条永远显示在其它血条之上可以healthBar.transform.SetSiblingIndex(transform.parent.childCount - 1);这里有一个很容易犯的错如果你把UI放在同一个Canvas下但项目的Canvas用了多个Sorting Order不同的CanvasSetAsLastSibling()只对同一Canvas内的兄弟节点有效跨Canvas无效。之前有个同事没注意到主界面Canvas和弹窗Canvas是父子平级的关系在弹窗里调SetAsLastSibling()结果弹窗始终比主界面低一层最后排查半天才发现是Canvas的Sorting Order没设值。5.2 动态修改Sprite的OrderSprite的排序比UI更灵活因为它直接暴露了数值属性动态排序的代码写起来也直观// 单位被击杀后尸体要显示在普通单位之下 GetComponentSpriteRenderer().sortingOrder -10; // 单位死亡掉落道具要显示在角色脚下、地面之上 itemRenderer.sortingLayerName Character; itemRenderer.sortingOrder 1;另一个常见场景是玩家操控的角色移动时脚下阴影会跟着地图位置变化改变Y轴排序。如果多个游戏角色都快走到同一条线上Y轴重叠会导致排序来回跳变画面会闪。我的经验是做一个专门的排序管理器SortingManager把所有需要动态排序的单位注册进去每个单位维护一个SortingOrder权重值基础值Y偏移由管理器统一每帧更新一次sortingOrder而不是每个单位的脚本自己改自己的这样能避免多单位并发修改时谁覆盖谁的问题。5.3 一个自动处理弹窗层级的UI管理器封装分享一个我在项目里实际使用的UI层级管理器简化版思路。这个管理器负责所有动态面板的排序注册思路很简单public class UILayerManager : MonoBehaviour { public static UILayerManager Instance; [Header(Canvas层级配置)] public Canvas mainCanvas; // 主界面 public Canvas popupRoot; // 弹窗根Canvas public Canvas toastRoot; // Toast提示根Canvas public Canvas loadingRoot; // Loading根Canvas private DictionaryUILevel, Canvas levelCanvasMap; public enum UILevel { Main 0, // 主界面 Popup 300, // 弹窗 Toast 1000, // 提示 Loading 2000 // 加载 } public void ShowPanel(GameObject panel, UILevel level) { if (panel null) return; Canvas targetRoot levelCanvasMap[level]; panel.transform.SetParent(targetRoot.transform, false); panel.transform.SetAsLastSibling(); panel.SetActive(true); } }核心思想就是把UI按照层级类型拆到不同的根Canvas下每个根Canvas的Sorting Order各不相同并且留好了数值余量。这样无论以后加多少新面板只要指定一个UILevel就不会出现弹窗压在Toast上面、Loading盖不住弹窗这类乱七八糟的顺序问题。5.4 处理拖拽后的层级变化拖拽功能有个专门的需求拖拽中的物体必须显示在所有物体之上。处理办法有两种。第一种在OnBeginDrag时调用transform.SetAsLastSibling()拖拽结束再恢复第二种拖拽过程中临时修改Canvas的sortingOrder至一个高位值。推荐优先用第二种原因是如果Canvas内部有滚动列表、滑动区域你把拖拽Item在兄弟节点里移到最末尾可能会影响其它UI的布局计算比如ScrollRect对子Node的遍历。改成临时提高sortingOrder则完全不影响布局逻辑。public class DragSortingFix : MonoBehaviour, IBeginDragHandler, IEndDragHandler { private Canvas canvas; private void Awake() { canvas GetComponentInParentCanvas(); } public void OnBeginDrag(PointerEventData eventData) { if (canvas ! null) canvas.sortingOrder 9999; } public void OnEndDrag(PointerEventData eventData) { if (canvas ! null) canvas.sortingOrder 0; // 恢复原值实际项目要记录原始值 } }注意这里恢复排序值的时候要记录的是这个Canvas的原始sortingOrder而不是无脑写死0。尤其是全局UI层级管理器统一管理Canvas时写死0会把这个Canvas从弹窗Canvas直接降级成主界面Canvas级别引发一串连锁显示问题。6. 层级排查实战几个典型“显示不上来/盖不住”的谜之现象6.1 场景一新弹出的UI明明代码没问题却显示不出来现象是点击按钮后一个面板Show了SetActive(true)了console里也没报错但界面上就是看不到这个面板。排查思路先看Hierarchy里这个面板挂在哪个父节点下。如果挂到了主界面的某个被Mask裁剪的节点下可能直接被父级Mask裁掉了层级根本没机会参与。再看坐标如果面板的RectTransform跑到了屏幕外面照明条件正常也不会显示。再查Canvas的sortingOrder。如果这个面板属于弹窗Canvas但弹窗Canvas的sortingOrder0主界面的Canvas也是0而面板在Hierarchy顺序上排在主界面之前那它就被主界面压住了。最后查Raycast Target。如果面板上一个透明Image把射线全部挡住了视觉上可能显示但按钮点不到这其实是事件系统的层级问题和渲染层级无关但表现很像。6.2 场景二3D角色走到UI面前UI反而被角色遮住了之前遇到一个AR项目UI是Screen Space - Camera模式挂在AR相机前面3D角色一旦走到相机和UI平面之间角色就会把UI遮住。这是正常的物理遮挡不是bug。处理方式看需求而定如果要求UI永远不被3D物体遮住就别用Screen Space - Camera切回Overlay如果要求角色被UI半遮挡的视觉效果就把UI平面调整到离相机更近的距离调Plane Distance并给角色材质或者UI平面使用半透明Shader来动态混合。另外还有一个常见坑用了Screen Space - Camera后UI Canvas的Sorting Layer如果没设置默认是Default而3D角色可能也设了Default且渲染顺序靠前导致UI整体被角色压住。解决办法就是给UI Canvas单独设置一个高优先级的Sorting Layer如UI层保证在跨体系排序时UI优先。6.3 场景三粒子特效要么永远被UI盖住要么盖住所有UI粒子特效在UI前后的层级问题根源几乎都是粒子系统的Renderer模块的Sorting Order没跟UI的Canvas排序统一。最直接的做法第一确认UI Canvas是Screen Space - Camera模式不能用Overlay第二给粒子特效设置一个独立的高Sorting Layer第三如果同一个Sorting Layer里还要继续细致分级就用Order in Layer。粒子系统排序最终生效的其实是特效所在GameObject上的ParticleSystemRenderer组件的排序属性而不是特效下面某个Sprite的排序属性注意区分。6.4 场景四Hierarchy顺序明明调整了UI就是不动这通常发生在嵌套Canvas场景下。如果你的UI元素被一个子Canvas包裹着你修改父Canvas下的兄弟顺序是不会影响到子Canvas内部UI元素的显示顺序的。排查方式选中目标UI元素看它的祖先链上有没有带Canvas组件的节点。找到一个就得重新评估它的sortingOrder因为子Canvas会插队。这种嵌套设计我在游戏里见最多的就是让一整个聊天窗口可以整体旋转缩放然后把聊天窗口单独做成了一个带Canvas的节点。一旦出现弹窗和聊天窗口同时存在的情景就得额外小心它们的sortingOrder设置。6.5 从显示层级延伸到性能调优热搜词里还有一条UI界面卡顿其实层级设置和卡顿之间也有直接关系。UI的Overlay模式虽然方便但有一个隐藏代价Overlay模式下UGUI会为整个UI生成一张巨大的合并网格任何一个小改动都可能触发大范围的Rebuild造成卡顿。而Screen Space - Camera模式的UI在局部动态内容较多时性能通常更好。另一个常见问题是为了排序恨不得一个UI一个Canvas这样做非常消耗Draw CallUI界面卡顿往往就是这么来的。控制层级不等于无限拆分Canvas正确做法是静态UI放同一个Canvas动态面板用层级管理器分配到少数几个固定Canvas上每个Canvas的sortingOrder只做固定区段划分不要每实例化一个UI就新建一个Canvas。我见过有项目为了改一个按钮的显示顺序就给这个按钮单独挂一个Canvas并动态调整sortingOrder结果UI整体性能直线下降。移动端上Unity UI的Rebuild开销和Overdraw都要严格控制排序设计、Canvas拆分、粒子特效的Sorting Layer都要同时考虑进去才能做到既正确又流畅。6.6 特殊环境微信小游戏里的UI层级注意事项针对Unity微信小游戏(小程序)视频播放方案这类热度很高的需求顺带提一句跟层级相关的坑在微信小游戏环境里如果要播放全屏视频视频播放器本质是盖在Unity渲染画面之上的原生控件Unity里的层级控制完全管不到它。所以如果你在Unity里做了视频上要显示一个CloseButton的UI按钮无论如何也显示不到视频上面——因为视频是原生层。业内常规套路是不要在Unity里放关闭按钮而是视频全屏播放完之后自动关闭或者用榜单/分享等小游戏原生接口来做交互。这算是层级控制在小游戏环境下的一种特殊边界。7. 一套可控的层级设计方案从项目初始化时就定好规矩7.1 项目启动时先配好Sorting Layer清单层级问题最怕的不是不会调而是没有统一规范每个开发者各调各的最后一团乱麻。所以我的强烈建议是项目初始化阶段就建立一个Sorting Layer清单把整个项目用到的层级全部列出来后续所有开发都按这个清单执行不允许临时新增。一旦进入开发中期再去乱加Sorting Layer那就会引发连锁灾难——新增的Layer可能会把原本辛苦调好的各种显示关系全部打乱。一个通用模板Layer名称说明典型对象Background场景背景地图、天空盒贴图Ground地面层地面、地板Obstacle障碍物墙、柱子、树Shadow阴影层角色脚下阴影Character角色层玩家、NPC、怪物Effect特效层攻击特效、Buff特效Water水面层水面半透明区域UIUI层所有UGUI CanvasUIPopupUI弹窗层弹窗、ModalUITopUI顶层Toast、Loading、全屏过渡7.2 与美术/策划约定层级区间光有Layer名还不够还要跟美术和策划约定一个Order in Layer区段规范。比如区间含义示例-1000 ~ -101背景装饰远处的山、云-100 ~ -1地表草地、路面0 ~ 50普通单位小怪、玩家51 ~ 200高优先级单位BOSS、飞行单位201 ~ 500技能特效大招、暴击特效501 ~ 1000特殊表现战斗飘字、伤害数字这样策划在配置数值时心里也有数不会随手写一个很大的数字把别的层级的显示顺序搞乱。特别是伤害数字类UI既不能盖住角色脸又要在所有特效之上区间设在1000比较安全。7.3 实现一个全局排序检查工具最后分享一个提升幸福感的开发小工具思路因为层级问题排查非常费眼最好在编辑器下写一个临时检查脚本一键统计当前场景内所有Canvas和Sprite Renderer的排序配置输出成对照表。#if UNITY_EDITOR using UnityEngine; using UnityEditor; public class SortingDebugTool : EditorWindow { [MenuItem(Tools/Sorting Debugger)] public static void ShowWindow() { GetWindowSortingDebugTool(Sorting Debugger); } private void OnGUI() { if (GUILayout.Button(列出场景内所有Canvas)) { var allCanvases FindObjectsOfTypeCanvas(); foreach (var c in allCanvases) { Debug.Log($Canvas路径: {GetPath(c.transform)}, $RenderMode: {c.renderMode}, $SortingLayer: {c.sortingLayerName}, $SortingOrder: {c.sortingOrder}); } } if (GUILayout.Button(列出场景内所有SpriteRenderer)) { var allSprites FindObjectsOfTypeSpriteRenderer(); foreach (var s in allSprites) { Debug.Log($Sprite路径: {GetPath(s.transform)}, $Layer: {s.sortingLayerName}, Order: {s.sortingOrder}, $PosY: {s.transform.position.y}); } } } private string GetPath(Transform target) { string path target.name; while (target.parent ! null) { target target.parent; path target.name / path; } return path; } } #endif排查层级问题时最花时间的不是改代码而是找到那个视图里被挡住但Hierarchy里不显眼的物体。有这样一个工具把所有排序信息统一打印出来一眼就能看出哪个Canvas的Sorting Order比预期的低、哪个Sprite的Layer压住了另一个Layer。8. 一个综合案例2.5D卡牌战斗中所有元素如何排层级为了把前面所有的原理揉到一起最后用一个综合场景收尾做一个2.5D卡牌战斗游戏画面里有3D场景、2D卡牌Sprite、UI按钮、血条、技能特效、伤害飘字、战斗结算弹窗。我们按责任划分来一步步设计层级。3D场景地形、建筑用Geometry渲染队列的不透明材质深度缓冲负责普通遮挡不需要参与排序。卡牌单位用Sprite Renderer挂CharacterSorting LayerOrder in Layer 单位在阵营中的序号前排单位Order小后排单位Order大。这样两排单位重叠时后排显示在前排之上符合斜45度视角的视觉逻辑。脚下的攻击范围辅助线、范围提示器挂Effect层Order in Layer 100保证能盖住单位地面投影但又不会挡住单位本体。伤害飘字、战斗数字挂在专用的World Space Canvas上Sorting Layer UIImage或UI_Like并且这个Canvas使用World Space模式让它在3D空间中跟随角色头顶位置这样一旦角色走到建筑后面飘字也会跟着被建筑遮住表现非常自然。左上角返回按钮、英雄头像、能量条属于常驻UI用Screen Space - Camera模式挂UI层Sorting Order 100。战斗胜利/失败结算弹窗属于Modal弹窗挂在独立CanvasSorting Layer UISorting Order 300。Loading界面、Toast提示挂在Sorting Layer UITop的CanvasOrder 1000。这个方案跑起来后的实际效果是3D场景在最底下2D角色其次角色脚下特效再往上一层头像UI紧接着盖住角色弹窗压在头像UI之上而Toast永远在最顶——所有元素的遮挡关系一目了然任何一个新来的程序员看到这个配置也基本能立刻定位问题。这个方案里最关键的一个设计决策是伤害飘字、血条这类需要参与世界遮挡的UI必须用World Space Canvas挂到Sorting Layer的最上层而常驻按钮这类永远不能遮挡的UI用Screen Space - Camera但Sorting Layer设置更高。如果你把血条做成了Overlay模式的Canvas那角色走到障碍物后面时血条反而会从障碍物前面飘过穿帮效果非常明显。我在实际项目里把这个方案做成了一套预制体模板初始化项目时直接复制修改后面几乎再也没有出现过大面积的层级bug。这套方法的本质并不是某个单一技巧而是把层级从一种运行时临时修改的东西变成了一种项目初始设计时就要明确规划好的基础架构。多数人把层级问题当bug来修修一次坏一次真正稳妥的做法是把它当架构来设计让所有元素从出生那一刻就知道自己在哪个层级、跟谁是兄弟、要被谁遮挡。
返回列表