NGUI所见即所得之UIRoot
UIRoot是NGUI最根本和最重要的脚本,在实际UI开发过程中都是以UIRoot为根的GameObject树,那他的作用到底是什么,先看下UIRoot的Inspection选项:
看到这个,大致可以猜到是跟UI界面缩放有关的,而且是基于高度放缩的。
Scaling Style参数
到底每个参数(Scaling Style, Manual Height ,Minimum Height 和MaximumHight)的作用和区别是什么,在没有其他先验知识的情况下,只有去看代码了。
public enum Scaling { PixelPerfect, FixedSize, FixedSizeOnMobiles, }
Scaling Style参数的作用是制定UIRoot的缩放类型,如果是PixelPerfect,Minimum Height和Maximum Height才起作用,换而言之,如果Scaling Style选择的是PixelPerfect就要对Minimum Height和Maximum Height进行设置。FixedSize和FixedSizeOnMobiles只跟Manual Height有关,区别在于后者(FixedSizeOnMobiles)只是针对IOS和Android上的判断,也就是说只有IOS和Android平台下FixedSizeOnMobiles才起作用。
那他们是怎么缩放的呢?如果是PixelPerfect缩放类型,当屏幕的分辨率大于Maximum Height,则以Maximum Height 为基础缩放,反之,如果屏幕分辨率小于Minimum Height 则以Minimum Height为基础进行缩放。例如,如果屏幕高度为1000,而设置的Maximum Height值为800,则UI界面整体放大为原来的1000/800=1.25倍。
如果Scaling Style指定为FixedSize或FixedSizeOnMobiles,则缩放只以Manual Height为参考,屏幕分辨率的高度值不同于此设置值时,则根据其比例(即Screen Height / Manual Height)对整棵UI树的进行“等比”缩放(宽度的缩放比也是此比例值)。
如果Scaling Style指定为FixedSize,UIWidget.height(以UIRoot默认进行高度缩放)是不会改变的(有关UIWidget的内容可以查看D.S.Qiu的另外一篇文章),不管实际屏幕分辨率的像素是多少,这看下Example 1 的 Anchor Stretch的背景图片,高度始终都是800,即设置的的manualHeight:
也就是相当于UIRoot下的UIWidget的height参数一直都是实际的值,虽然在实际在显示器显示的高度不是UIWidget.height这个值,所以才有了放缩的感觉。实际的放缩是根据Camera.pixelHeight(这个值和Screen.height的大小是一样的)来的,也就是放缩比 = Camera.pixelHeight/UIRoot.manualHeight,或者是Screen.height/UIRoot.manualHeight。
也就是说,当Scaling Style 指定为FixedSize,UIRoot的子对象高度的参数保持不变,至于显示的缩放是根据Camera自动进行的,程序无需额外控制。更多详细的分享可以参考另外一篇有关UIAnchor 和UIStretch的文章(猛点查看)。
增补于 2013/11/16 14:20
Scaling Style策略
(1)PixelPerfect和Minimum Height, Maximum Height
使用PixelPefect,只要是想达到UI图片尽可能不缩放,保持原尺寸大小,这种在PC端这种可以调节界面大小的会使用比较多。
(2)FixedSize和Manul Height
FixedSize只要是希望UI界面尽可能和屏幕分辨率适配,如移动设备上,特别是手机上,屏幕就那么小,UI界面一定要求全屏显示,就要进行缩放。
对于Unity实际开发中屏幕自适应问题,oneRain在①中有更详细的描述,这里只介绍下,D.S.Qiu想到的一种自适应策略——“花草”填充法。填充法(这个是D.S.Qiu命名的,哈哈,自恋下)指的是用其他图片区填充因为固定比例放缩而出现镂空黑边的区域,当然也可能已经有游戏是这么做的,当然oneRain说的增加一个宽度缩放比例,长宽分别以尽可能接近屏幕长宽比的比例去缩放。
Scale的实现
虽然我们已经了解了Scale的作用区别以及策略,那到底是怎么实现的呢?还是上代码:
void Update () { if (mTrans != null) { float calcActiveHeight = activeHeight; if (calcActiveHeight > 0f ) { float size = 2f / calcActiveHeight; Vector3 ls = mTrans.localScale; if (!(Mathf.Abs(ls.x - size) <= float.Epsilon) || !(Mathf.Abs(ls.y - size) <= float.Epsilon) || !(Mathf.Abs(ls.z - size) <= float.Epsilon)) { mTrans.localScale = new Vector3(size, size, size); } } } }
可以看出Update函数中是根据activeHeight来调整UIRoot的transform的localScale来实现的,哈,竟可以这么简单。那么,我们只要弄清楚activeHeight就可以了:
public int activeHeight { get { int height = Mathf.Max(2, Screen.height); if (scalingStyle == Scaling.FixedSize) return manualHeight; #if UNITY_IPHONE || UNITY_ANDROID if (scalingStyle == Scaling.FixedSizeOnMobiles) return manualHeight; #endif if (height < minimumHeight) return minimumHeight; if (height > maximumHeight) return maximumHeight; return height; } }
可以看出activeHeight就是前面Scale Style的不同参数的具体实现,即得到缩放参考高度。
Orthographic Size和分辨率
在上面Update和activeHeight的函数中都出现了 “2” 这个常数,这个常数到底是怎么来的。要想知道这个,就要明白Camera 设定为Orthographic类型中的Size(即Orthographic Size)的含义,查看Unity文档,就可以知道这个Size是Camera看到区域的一半,如果Size设置为1,则Camera可以看到高度为为2的区域,然后我们知道照相机看到的区域是全画在整个屏幕的,也就是说Size的值对应为屏幕分辨率的一半。
如果屏幕宽度为1000个像素,Size设置的值表示1000/2=500个像素。所以,我们通过整个关系计算UIRoot下的GameObject的实际对应屏幕的高度:从GameObject向上一直到UIRoot,将它们的loaclScal相乘得到的乘积除以Size乘以屏幕高度的一半,即(localScale*....localScale)/Size*Screen.height/2。
这可以解释UIRoot的localStyle为啥都是很小的小数,因为这样可以保证UIRoot的子节点都可以以原来的大小作为localScale,比如一张图片是20*20的,我们可以直接设置localScale为(20,20,1)不用进行换算,直观方便。(NGUI3.0(or 2.7)以后的版本已经不再使用localScale来表示UISprite ,UILable(UIWidget的子类)的大小了,而是在UIWidget的width和height来设置,这样做的好处就是一个gameObject节点可以挂多个UISprite或UILabel了,而不会受localScale的冲突影响 2013/11/16增补)。
UIRoot细节
前面说道Update函数中有一个常数 2 ,表示Size是设置为1的,这个可以从Start函数就可以知道:
protected virtual void Start () { UIOrthoCamera oc = GetComponentInChildren(); if (oc != null) { Debug.LogWarning("UIRoot should not be active at the same time as UIOrthoCamera. Disabling UIOrthoCamera.", oc); Camera cam = oc.gameObject.GetComponent (); oc.enabled = false; if (cam != null) cam.orthographicSize = 1f; } else Update(); }
但是这里似乎有点疏忽,这里只移除UIOrthoCamera这个脚本(UIRoot脚本开始就言明这两个脚本不能同时使用,所以要移除),并将cam的orthographicSize设置为1f,但是我想如果没有UIorthoCamera这个脚本的话,就不会重新设置Camera的orthographicSize值,这样如果orthographicSize不是1的话,效果就不一样了。刚开始我会以为这是NGUI developer的一个Bug,但是如果都在Start设置orthographicSize为1f,那这个参数就没有意义了,让使用者自己设置可以有更多效果,如“屏中屏”——将满屏的UI缩放为另外一个UI界面的一半大小,所以才会说“似乎有点疏忽”。
下面看下效果图:
orthographicSize=1
orthographicSize=2
很明显可以orthographicSize=2时,图片进行了缩小。当orthographicSize=1时背景图片用了UIStretch脚本来实现满屏效果,当orthographicSize为2时却没有满屏,这就说明代码中UIRoot是以2为屏幕宽度的,现在Camera的视野大小为4,那映射到屏幕当然不会有“满屏”的效果了(只会是屏幕宽度的一半),背景图片在左上角是因为使用UIAnchor脚本。
②和③中分别都介绍了如何设置Orthographic Size来达到像素和Unity中单位对应起来,都写的很不错,这也是我写这篇博客的一个触动。
UIRoot中还有两类函数:GetPixelSizeAdjustment 和 Broadcast,前者是获取当前分辨率的单个像素的大小,而后者其实就一个UIRoot的消息广播函数,还有一个当前激活状态下的UIRoot的队列。至此D.S.Qiu已经将UIRoot脚本解析的淋漓尽致了,自然就剩下小结了。
『基于宽度放缩
UIRoot是基于高度放缩的,即放缩的比例是以高度为参考的,所以UIRoot有一个manualHeight的参数。那么对于横版游戏显然不行,要是能实现基于宽度放缩。所以可以通过设置一个“manualWidth”的参数来做。比如我们项目中使用的是 1024 作为UI的宽屏尺寸,通过换算设置manualHeight的值:
int height = Mathf.Max(2, Screen.height); manualHeight = Screen.height * 1024 / Screen.width; //基于宽度的屏幕分辨率自适应
之前的同事用了很多方法都没有搞定,其实根本就没有那么复杂,所以一旦知道原理之后,很多事情就变得简单多了。
』
增补于 2013,12,23 22:14
小结:
一直都想把NGUI的内部机制彻底的弄明白,一直都没付诸实践,知道看②和③中的文章,D.S.Qiu发现还是有必要尽快整理下,切好最近项目没什么事情,也是周末。UIRoot这个脚本虽然很简单,但确实NGUI整个体系的基石。更多NGUI文章点击查看。
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件([email protected])交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在文首注明出处:http://dsqiu.iteye.com/blog/1964679
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
参考:
①oneRain: http://blog.csdn.net/onerain88/article/details/11713299
②风宇冲: http://blog.sina.com.cn/s/blog_471132920101fua3.html
③midashao:http://blog.csdn.net/midashao/article/details/8232341