NGUI脚本 UIRoot
在NGUI中,不仅有众多插件,还有许多实用的脚本。今天,我们将深入学习其中一个重要的脚本——UIRoot。
UIRoot简介
UIRoot是NGUI中最基础且关键的脚本。在实际的UI开发过程中,所有的UI元素都构建在以UIRoot为根的GameObject树之上。那么,UIRoot具体有什么作用呢?我们先来看看UIRoot的Inspector选项。从这些选项中,我们大致可以推测出它与UI界面的缩放相关,并且是基于高度进行缩放的。
Scaling Style参数
参数枚举与作用
public enum Scaling
{
PixelPerfect,
FixedSize,
FixedSizeOnMobiles,
}
Scaling Style参数用于指定UIRoot的缩放类型。不同的缩放类型对其他参数的依赖有所不同:
- PixelPerfect:当选择此类型时,Minimum Height和Maximum Height参数才会生效。也就是说,如果Scaling Style设置为PixelPerfect,就需要对这两个参数进行合理设置。
- FixedSize和FixedSizeOnMobiles:这两种类型只与Manual Height参数有关。它们的区别在于,FixedSizeOnMobiles仅在IOS和Android平台下生效。
缩放规则
PixelPerfect缩放类型
当屏幕分辨率大于Maximum Height时,以Maximum Height为基础进行缩放。反之,当屏幕分辨率小于Minimum Height时,则以Minimum Height为基础进行缩放。例如,如果屏幕高度为1000,而设置的Maximum Height值为800,则UI界面整体放大为原来的1000 / 800 = 1.25倍。
FixedSize或FixedSizeOnMobiles缩放类型
若Scaling Style指定为这两种类型,缩放将仅以Manual Height为参考。当屏幕分辨率的高度值与Manual Height设置值不同时,会根据比例(即Screen Height / Manual Height)对整棵UI树进行“等比”缩放,宽度的缩放比也是此比例值。
当Scaling Style指定为FixedSize时,UIWidget.height(以UIRoot默认进行高度缩放)不会改变。例如,查看Example 1的Anchor Stretch的背景图片,无论实际屏幕分辨率的像素是多少,其高度始终为设置的manualHeight值(如800)。这意味着UIRoot下的UIWidget的height参数一直保持实际的值,虽然在显示器上显示的高度并非UIWidget.height这个值,但会有缩放的视觉效果。实际的缩放是根据Camera.pixelHeight(该值与Screen.height大小相同)来实现的,缩放比 = Camera.pixelHeight / UIRoot.manualHeight,或者是Screen.height / UIRoot.manualHeight。也就是说,当Scaling Style指定为FixedSize时,UIRoot的子对象高度参数保持不变,显示的缩放由Camera自动完成,程序无需额外控制。更多详细内容可参考另外一篇有关UIAnchor和UIStretch的文章(猛点查看)。
Scaling Style策略
PixelPerfect和Minimum Height, Maximum Height
使用PixelPerfect策略,主要是为了使UI图片尽可能不进行缩放,保持原尺寸大小。这种策略在PC端较为常用,因为PC端界面大小可以调节。
FixedSize和Manual Height
FixedSize策略主要用于使UI界面尽可能与屏幕分辨率适配,在移动设备(特别是手机)上应用广泛。由于手机屏幕尺寸有限,为了实现UI界面的全屏显示,需要进行缩放。
对于Unity实际开发中的屏幕自适应问题,oneRain在①中有更详细的描述。这里介绍一种由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是根据不同的Scaling 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,将它们的localScale相乘得到的乘积除以Size乘以屏幕高度的一半,即(localScale ....localScale)/ Size Screen.height / 2。
这也解释了为什么UIRoot的localScale通常是很小的小数。这样可以保证UIRoot的子节点能够以原来的大小作为localScale,例如一张20 * 20的图片,我们可以直接将localScale设置为(20, 20, 1),无需进行换算,使用起来更加直观方便。需要注意的是,NGUI 3.0(或2.7)以后的版本不再使用localScale来表示UISprite、UILabel(UIWidget的子类)的大小,而是通过UIWidget的width和height进行设置。这样做的好处是一个GameObject节点可以挂载多个UISprite或UILabel,而不会受到localScale的冲突影响。
UIRoot细节
前面提到Update函数中的常数“2”,这表示Size设置为1。我们可以从Start函数中得到验证:
protected virtual void Start ()
{
UIOrthoCamera oc = GetComponentInChildren<UIOrthoCamera>();
if (oc != null)
{
Debug.LogWarning("UIRoot should not be active at the same time as UIOrthoCamera. Disabling UIOrthoCamera.", oc);
Camera cam = oc.gameObject.GetComponent<Camera>();
oc.enabled = false;
if (cam != null) cam.orthographicSize = 1f;
}
else Update();
}
这里似乎存在一个小问题,代码只移除了UIOrthoCamera脚本(UIRoot脚本开始就说明这两个脚本不能同时使用),并将cam的orthographicSize设置为1f。但如果没有UIOrthoCamera脚本,就不会重新设置Camera的orthographicSize值。如果orthographicSize不是1,效果就会不同。起初,我们可能会认为这是NGUI开发者的一个Bug,但实际上让使用者自己设置orthographicSize可以实现更多效果,例如“屏中屏”——将满屏的UI缩放为另外一个UI界面的一半大小。所以这里说“似乎有点疏忽”。
下面是效果图: 很明显,当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默认是基于高度进行缩放的,即缩放比例以高度为参考,因此有一个manualHeight参数。对于横版游戏来说,这种方式显然不太适用。为了实现基于宽度的缩放,可以设置一个“manualWidth”参数。例如,在我们的项目中,使用1024作为UI的宽屏尺寸,通过以下代码换算设置manualHeight的值:
int height = Mathf.Max(2, Screen.height);
manualHeight = Screen.height * 1024 / Screen.width;
//基于宽度的屏幕分辨率自适应
之前的同事尝试了很多方法都未能解决这个问题,实际上原理并不复杂。一旦了解了原理,很多事情就会变得简单。
小结
一直以来,我都希望深入了解NGUI的内部机制,但一直未能付诸实践。直到阅读了②和③中的文章,我才意识到有必要尽快进行整理。恰逢最近项目不忙,又是周末,于是完成了这篇文章。UIRoot脚本虽然看似简单,但却是NGUI整个体系的基石。更多NGUI文章点击查看。
注释: ①:[具体链接1] ②:[具体链接2] ③:[具体链接3]