本篇文章是整理在做项目的过程中,遇到了各种各样的界面穿插问题,以及界面层级混乱,比如,手机卡了或点快了,就导致两个界面相互交叉。
这里有个unity学习交流小组
对于界面,这应该算是一个很严重的bug,很大部分原因是整个UI框架没有从整体上考虑这个,后来决心弄清楚层级的控制,并把一些对于目前项目可行的方法应用,界面穿插的问题少了很多,注意我只是在现有的框架打的补丁。如果是一个从头开始,在架构UI的时候,希望这篇文章能够帮到你们。
以前项目使用的NGUI插件,UI是有一个人摆放的,我们客户端就拿到这些prefab,添加相应的逻辑脚本。但恰恰是这个摆界面的人也没有注意界面的层级,各个panel、各个widget的depth没有统一管理,导致开发中后期,界面一多起来,很多界面同时出现的时候,panel和widget的层级就乱了。
一般都不用它来开发游戏,作为拓展unity编辑器开发比较多,unity自带的GUI效率非常低,每次渲染都是一个DrawCall,不好用,不能做到所见即所得。
UGUI是NGUI作者参与开发的,unity官方的新UI系统,感觉挺好用的,至于效率问题应该比NGUI好些。5.x以后ugui有很大的提升,以后ugui应该会逐步替代ngui。
很早之前就尝试过这套ugui系统,正是我在研究界面层级的时候,用NGUI显示模型粒子特效没有找到好的方法时,于是探索了ugui,具体怎么控制层级,后面会讨论。
是一个老牌的unity UI插件了,在ugui没有推出之前,大部分unity的游戏都使用的NGUI插件开发UI,它的好处很多,提供了常见的UI控件,实现几乎所有需要的功能,在效率上也是控制严谨,DrawCall合并极大提升了控件渲染效率,还支持3D GUI。但是也有很多不足,版本更新太频繁,版本bug很多,对于了解NGUI的开发者可以很好利用它,但对于初学者来说,NGUI固然容易上手好用,但是对DrawCall的重建规则不了解,仍然会效率低下(后面会整理一下分析NGUI的笔记)。
最近发现的一款跨平台UI编辑器,组合各种复杂UI组件,以及为UI设计动画效果,无需编写代码,可以一键导出Unity,Starling,Egret, LayaAir,Flash等多个主流应用和游戏平台。
特性:
当然不止这些,还有许多UI框架,比如:Daikon Forge GUI、EZ GUI、2dTookit等等,有时间可以看看,但目前主要用到的还是NGUI、UGUI啦。
1. 如果是2D游戏,渲染顺序关系着每个层次的显示先后,比如UI在游戏内容前面,游戏内容又有多层次。举一个简单的例子,在横版2D游戏中,经常会用到多层滚动的背景,把游戏物体分层管理起来,可以有效的减少出错几率,很好的控制显示效果。
2. 对于3D游戏,游戏内容是3D的,UI一般是2D,有很多时候需要把某个模型啊,粒子特效啊,放在界面上,这样就有一个问题,3D物体和2D界面的先后关系,比如有些界面是在模型之上的,有些在下面,尝试过很多种办法,都能实现需求,但不是每种办法都是那么舒服的。
Camera是unity中最优先的渲染顺序控制。depth越大,渲染顺序越靠后。
对于这个属性,我也是云里雾里的,不是太明白。按照字面意思是层的排序,Canvas和Renderer都有这个属性,unity官方就一句话带过。优先级仅次于Camera的depth。
看网上的博客,一般都写着sorting layer是比Camera低一层级的控制渲染顺序的属性。然后我做了很多尝试,发现并不是所有Renderer组件这个属性作用,根据尝试的结果,做如下总结:
Canvas中的sorting layer可以控制Canvas的层级,这是ugui中的东西,下面会讨论的;
GameObject中的renderer属性就是挂在该GameObject的Renderer组件,Renderer是渲染组件的基类,下面有多个派生类,经过测试,目前知道的只有SpriteRenderer的sorting layer和sorting order能控制渲染顺序。所以我也觉得,sorting layer和 sorting order就是unity 对2D游戏所支持的吧;
renderer组件有如下几类:
这是unity中的一个概念,(至少之前在opengl中没听过),大致意思就是渲染顺序,那无疑就是控制渲染顺序的嘛。
所以一般设置材质的renderQueue或直接在shader中设置。
在ShaderLab中,有4个提前定义好的render queue,你可以设置更多的在他们之间的值:
Background :表示渲染在任何物体之前
Geometry(default):渲染大多数几何物体所用的render queue
AlphaTest:用于alpha测试
Transparent:用于渲染半透明物体
Overlay:渲染所有物体之上
There are four pre-defined render queues, but there can be more queues in between the predefined ones. The predefined queues are:
Background - this render queue is rendered before any others. You’d typically use this for things that really need to be in the background.
Geometry (default) - this is used for most objects. Opaque geometry uses this queue.
AlphaTest - alpha tested geometry uses this queue. It’s a separate queue from Geometry one since it’s more efficient to render alpha-tested objects after all solid ones are drawn.
Transparent - this render queue is rendered after Geometry and AlphaTest, in back-to-front order. Anything alpha-blended (i.e. shaders that don’t write to depth buffer) should go here (glass, particle effects).
Overlay - this render queue is meant for overlay effects. Anything rendered last should go here (e.g. lens flares).
在摄像机坐标系下的Z轴,控制着该相机下的物体的深度,在fragment shader中进行深度测试,这样就控制了渲染到屏幕的顺序。(可以看看深度测试的具体原理,就明白怎么控制显示的先后顺序了)
好了,讲完了unity中的控制渲染顺序的方法后,接下来聊两个个UI系统独有的方法了。首先说明一下,NGUI中空间位置不会影响屏幕显示关系,因为在它内部处理顶点的时候,舍弃了Z坐标值。
首先讲到的就是UIPanel,用过NGUI插件的朋友应该非常熟悉这个组件,将来会整理一篇分析NGUI的笔记,里面会重点聊到UIPanel,所以这篇笔记就一笔带过了:UIPanel非常重要。哈哈。
1 depth:
NGUI中最正统的控制panel之间层级关系的就是 它的 depth 属性。depth越大,越靠后渲染。
sorting order:
panel的该属性也可以控制panel的顺序。
它的优先级在depth之前,内部还是通过设置render的sorting order来控制的。来看看一些源码:
> public int sortingOrder
{
get
{
return mSortingOrder;
}
set
{
if (mSortingOrder != value)
{
mSortingOrder = value;
#if UNITY_EDITOR
NGUITools.SetDirty(this);
#endif
UpdateDrawCalls();
}
}
}
然后查找了mSortingOrder的索引,发现在函数UpdateDrawCalls中使用了mSortingOrder
继续跟踪dc.sortingOrder
然后发现mRenderer是个 MeshRenderer类型的,在之前还说过,对render的sorting order不太明白,并不能控制渲染顺序。然而NGUI内部却使用了并且能够控制渲染顺序。我就纳闷了,然后在网上查了很久的资料,终于在雨松博客的评论区看到:unity中3D物体的Z的渲染区域 和UI的区域不一样,必须同样都是一个片才行。NGUI中生成的UIDrawCall里面的Mesh都是片,所以sorting order对它有用,而之前说的之间创建一个cube,它是一个3D物体,3D物体和UI没办法通过它来控制层级的。
是因为panel的depth控制着UIDrawCall的生成顺序,影响了RenderQueue的顺序,前面已经提到过了,sorting order和sorting layer比RenderQueue优先级更高。
在同一个panel下的各个widget,可以用widget的depth属性来控制它们的前后关系。depth控制着widget在panel顶点重建时传入的顶点序列,这个跟踪一下NGUI源码就知道了。(后面我会整理几篇分析NGUI的文章,里面包含这个)
在NGUI中使用这个属性,就得先看看UIPanel关于RenderQueue的三种方式:
Automatic:由NGUI自动生成
StartAt:手动设置一个值
Explicit:同一个panel下的RenderQueue的值相同
看代码可以理解一下,UIPanel.cs
不同Canvas之间可以用以下两个属性控制渲染层级
Sorting Layer
Order in Layer
Canvas和NGUI的UIPanel一样,这些计算都不会管两个之间的父子关系,有一个算一个。
在同一个Canvas中,Hierarchy的顺序决定了控件的层级关系。
对于3D物体的显示先后就是完全按空间的先后来的,当然可以在fragment shader中关闭深度测试,或进行其他影响帧缓冲区的操作。就跟opengl中一样了。
RenderQueue是对unity中所有可以渲染的物体都适用。
补充:用Sorting Order可以控制片模型的层级关系,NGUI中sorting order就是就是靠这种特性实现的。
和3D模型一致。
和3D模型一致。
粒子系统本身是一个Renderer组件,它渲染的是一个一个精灵,是一个一个片,该属性有效。
经过上面的整理,已经明白了各个地方是可以怎么控制渲染顺序了,接下来就来解决一些项目中遇到的问题。
NGUI中放3D模型,实现方法:
这种方法肯定可以实现,而且简单粗暴。但仔细想想,要做的决不仅仅加个相机这么简单,我之前做的项目就是这么搞得。然后……然后……在开发的中后期,各种bug就出来了,由于多相机没有管理好,各种问题真的很难缠,呵呵底层不是我写的哈,所以我们上层开发人员就补丁啊,整个项目代码有些地方面目全非。
多相机其实就是利用Camera的depth来控制渲染顺序的,一般来讲,模型一个相机,UI一个相机,等等……你以为两个相机就够了吗?有没有想过模型上可能还有界面呢?你可能说再加一个相机,但是有些需求每个界面的跳转都是多个的,并不能直接在做界面的时候就确定哪个界面在上面哪个界面在下面,所以这样的加多个相机并不可行。
后来想到一种办法,就是每个界面对应一个相机,一个相机照一个模型,或是在一起显示的模型,利用相机的depth来控制它们的遮挡关系,但是需要在客户端框架中管理好相机,做一个“池子”,让相机可以复用,并且。可能你会担心性能问题,但这不是问题,一个游戏中可能有很多界面,但是需要同时显示的最多不超过5、6个吧,加上模型需要的,每次同时工作的撑死了就10个了,而且只要让相机只照射到相应的界面,这就不会造成性能的损失,不同的只是变换矩阵而已。
相比于相机的管理,我觉得用RenderQueue来控制会更简单一些,毕竟NGUI中也大量使用了RenderQueue来控制前后关系,可以看看它们源码:
这是在UIPanel中的LateUpdate,可以看到三种模式下,RenderQueue具体值加的方式,一般我们都是用Automatic模式,这种模式下是根据每个UIPanel中生成的DrawCall自动计算RenderQueue的值。
不管3D模型、粒子特效、还是NGUI,都可以用RenderQueue来控制渲染顺序,所以我想到了一个办法。
修改NGUI的源码,让两个UIPanel的RenderQueue值间隔一些,空出几个值用来设置给模型或粒子特效,很简单:
看吧,是不是很简单。然后只需要设置显示在界面上的模型或粒子特效的RenderQueue为这些空出来的值就行了。
这个脚本挂在相应的模型上,target表示 该模型要显示在哪个panel下面。
A、B两个界面,我把渲染顺序调整后:A — 模型 — B
到这里只做好了一部分,我们只是利用RenderQueue控制了渲染顺序,但是空间的深度关系还是在影响着在屏幕上显示的先后关系。在fragment shader中会进行深度测试,不管你谁先渲染谁后渲染,只要所有物体渲染时都写入深度缓冲区,那么渲染顺序并不能真正影响最后屏幕显示的先后关系。
所以还需要一步,就是在渲染模型时关闭深度测试,例如A、B两个界面,模型在AB之间,那么渲染流程如下:
A — 关闭深度测试 — 模型 — 开启深度测试 — B
在shader lab中关闭深度测试的方法是:
ZWrite Off
这里提供一个简单的shader:
>Shader "Custom/DepthTest" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "Queue"="Transparent" }
ZWrite Off
LOD 200
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
补充:模型与模型之间也可以用上面的这种渲染流程控制屏幕显示的先后关系。
在同一时间,屏幕上只显示一个界面,每次打开新界面就隐藏下面的界面,连模型和粒子都隐藏掉。这样就不存在两个界面同时显示的情况了。
在其它控制条件都相同的情况下,NGUI和模型在一个相机下,受正常的渲染管线控制,也就是空间位置关系可以影响渲染到屏幕的层级关系。注意模型的“厚度”。
粒子特效也可以使用Camera的depth来控制,但是粒子特效会存在很多,所以用Camera并不可行,所以这里就直接排除掉了。
粒子特效本身是用“点精灵”渲染的,每个粒子就是一个点精灵,可以看做一个片模型,而片模型就可以受该属性影响。
可以做一个测试,把下面脚本挂在粒子特效上:
>public class SortUtil : MonoBehaviour {
void Awake()
{
renderer.sortingOrder = 1;
}
}
sorting Order默认值为0,现有A、B两个界面,把A的panel设置为0, B的panel设置2:
A:0
粒子特效:1
B:2
粒子特效刚好插在A、B之间,显示效果也是粒子特效穿插在A、B之间。
补充:unity5.3后粒子特效支持了直接设置sorting order
这里就没有模型和NGUI交叉麻烦了,只需要完成RenderQueue的设置就可以了,不需要改shader。
假设A、B两个界面,RenderQueue的值的大小关系:
A < 粒子特效 < B
参照上面模型的具体方法,让粒子特效的RenderQueue插在A、B两个界面之间就可以了。
同样的,可以用多个相机来做,但这种方式总归是不太好,不推荐。
在fragment shader中利用传入的Mask数据,去模型片段数据进行筛选,符合条件的留下,不符合的丢弃。这种办法其实也适用于NGUI。
UGUI中Image可以接受一个材质,可以把RenderTexture放在一个材质上。这样就可以按照UGUI本身的那些排序方式来控制了。
和NGUI一样,同样可以用sorting order来控制,Canvas组件支持这些属性。
写了这么多,大致总结了常见的方式,比较熟悉NGUI,所以对于NGUI中各种都比较清楚,写得比较详细。对UGUI可能总结的不是很完整,所以以后还会继续总结。在文章中提到了几处shader lab的地方,我在这里说明一下,利用shader lab也可以完成以上各种遮挡关系,层级关系,但这篇文章没有详细讲清楚,主要考虑到篇幅,所以后来会专门写shader lab的实现方法。