博客内容基于 NGUI.3.11.2,讲述 NGUI 2D UI 。
设备无关坐标
以 OpenGL 为例,经过各种图形阶段(stage)后,几何形状最终被转换成像素显示在屏幕上。设备屏幕坐标以像素为单位,是设备相关的。不同设备分辨率不一样,即使同一设备,不同窗口的大小也不一样。在不关注具体设备的情况下绘制几何形状,则需要设备无关坐标系统。OpenGL 标准化设备坐标空间(NDC)在 X 轴、Y 轴,Z 轴上是 [-1, 1] 。举个例子,不做任何坐标变换(transform)和视口变换(viewport),对于顶点坐标(-1.0f, -1.0f, 0.0f)、(1.0f, -1.0f, 0.0f)、(1.0f, 1.0f, 0.0f)、(-1.0f, 1.0f, 0.0f),调用 glDrawArrays
并指定 GL_TRIANGLE_FAN
绘制的长方形覆盖整个窗口。
Unity 提供的坐标单位(设置 Transform
组件各属性值,如 position
)就是设备无关的。设置 Camera.orthographic = true
使用 2D 正交摄像机,假设屏幕宽高比为 aspect
,则正交摄像机 XY 平面矩形宽高分别为 cam.orthographicSize * 2.0f * apsect
和 cam.orthographicSize * 2.0f
。
NGUI 2D UI 像素单位
使用菜单 NGUI|Create|2D UI 创建 2D UI 。具体的创建函数 NGUITools.CreateUI
有如下设置 2D UI 部分代码,并且摄像机处于(0, 0, 0)。则可以计算渲染矩形的范围(先忽略 Z 轴)。
cam.orthographic = true;
cam.orthographicSize = 1;
cam.nearClipPlane = -10;
cam.farClipPlane = 10;
假设屏幕宽高比 aspect = 1.778f
。由于摄像机处于中心,则渲染矩形的左下角世界坐标是(-1.778f, -1.0f),右上角世界坐标是(1.778f, 1.0f)。这个坐标范围与设备无关,处于这个范围内便可以被渲染。
再想一下,如果在这个坐标范围内再封装一层并且把 GameObject 的 Transform.localScale
属性设置为(1.0f, 1.0f, 1.0f)(避免受到被此属性影响),然后坐标轴都缩小 360 倍,为了使得 GameObject 仍能被渲染,则 GameObject 的坐标值要被放大 360 倍。则现在左下角坐标变成了(-640.0f, -360.0f),右上角为(640.0f, 360.0f),现在坐标范围变成了 1280 x 720,此时一张 1280 x 720 的图片则可以显示在整个窗口中。于是形成了以像素为单位的新的坐标系统,这样会比之前的设备无关坐标范围更容易理解和使用。
其实 NGUI 像素单位的本质就是选择一个缩放因子(factor)对坐标进行缩小,这样 GameObject 实际的坐标值和大小值便可以放大相应的倍数,便于使用。最终渲染时会再乘以 factor 得到实际的坐标值或者大小值。NGUI 是根据高度计算缩放因子的,上例中高度是 2.0f 且设置 aspect = 1.778f
,缩放因子是 factor = 2.0f / 720
,于是构造了 1280 x 720 虚拟分辨率。若设备屏幕大一些,则 2.0f 对应的高度便会大一些,图像也就显示的大一些,反之便显示的小一些。
屏幕适配
UIRoot 与缩放因子
前面介绍了缩放因子,那么 NGUI 中是如何处理的呢?使用菜单创建 UI 时,会创建两个 GameObject ,UIRoot
和 UIPanel
添加到 UI Root 而 UICamera
添加到 Camera 。UIRoot
处理缩放逻辑,UIPanel
管理渲染,UICamera
管理消息处理。一个 NGUI 渲染实例在 Unity Inspector 面板中以树形结构具体表现,UIRoot
所在的 GameObject 为根节点(是其余所有 GameObject 直接或间接的 parent),其余 GameObject 都作为此根节点的直接或者间接子节点。多个 NGUI 渲染示例(UIRoot
)不能有交集,不然其中一个作为另一个的 child 会导致缩放逻辑会出错。
缩放因子的计算与应用在 UIRoot.UpdateScale
函数中。其中两行如下。
float size = 2f / calcActiveHeight;
mTrans.localScale = new Vector3(size, size, size);
size
就是前面提到的缩放因子。calcActiveHeight
是考虑屏幕适配后计算的高度值(以像素为单位)。然后把 size
赋值给 localScale
。由于 UIRoot
附加到根节点,所以其他的所有 GameObject 都会受到此缩放因子的影响。
这里 2f 其实就是 2 * Camara.orthographicSize
,由上面可知 orthographicSize
被设置成了 1f 。
不同的分辨率
实际开发时会选择一个标准分辨率,如 1280 x 720(aspect 为 16:9),并以此为标准设计美术资源,比如游戏背景图片为 1280 x 720 。对于手游,不同设备的分辨率也不同,屏幕适配就是为了把以开发时选择的标准分辨率为基准创建的游戏画面,更好的在不同的设备上显示。
假设 calcActiveHeight
返回的值就是 720(Scaling.Constrained 和 Constraint.FitHeight)。
- 若设备屏幕宽高比是 16:9 则能完毕适配。实际高度是 2.0f,则根据宽高比计算宽度为 3.556f 。假如有一张显示在中心的图片,大小为 1280 x 720 像素,乘以缩放因子(2.0f / 720)后刚好与实际的宽度和高度一致,图片正常全屏显示。注意只要是宽高比是 16:9,不管设备是 1280 x 720 还是 640 x 320 或其他分辨率,都是能完美适配的。因为 Unity 会自动把宽度 3.556f 和 高度 2.0f 与设备窗口的宽和高映射,这就是设备无关坐标。
- 若设备屏幕宽高比不是 16:9 比如 1024 x 768(4:3),则一张 1280 x 720 显示在中心的图片不能完全适配。实际高度是 2.0f, 宽度为 2.667f 。而缩放因子为 2.0f / 720,图片的宽和高乘以缩放因子得到宽和高分别为 3.556f 和 2.0f,和实际的宽度 2.667f 相比,图片更宽,所以图片会超出部分屏幕。如下图两种分辨率对比。
- NGUI 提供了多种屏幕适配策略由
UIRoot.scalingStyle
和UIRoot.constraint
指定,这里就不深入分析了。基本的策略有如下。 - 简单的完全适配宽度或者高度。当不能完美适配时,图片会在垂直或者水平方向超出。
- 比较标准宽高比和设备的宽高比,来选择在水平还是垂直方向完全匹配。若不能完美适配时,图片会全部显示,只是会有一个方向会多出间隙。
- NGUI 也提供了锚点功能。通过结合屏幕适配测试和锚点,来使图像更好的跨设备显示。
2D UI 还是 3D UI
假设标准分辨率是 1280 x 720,不使用锚点且 UIRoot
使用 Scaling.Constrained
和 Constraint.FitHeight
。有上图可知在 1024 x 768 的设备上,图片在水平方向会超出。若选择 Constraint.FitWidth
则 2D UI 可以完全显示图片,垂直方向会有一些间隙,但是 3D UI 不能。
具体的原因先不分析,简要介绍一下,2D UI 中缩放因子影响渲染矩形平面的缩放。而 3D UI 中缩放因子除了影响渲染平面,还影响渲染平面到摄像机的距离,所以会造成 UIRoot
选择不同的缩放策略而没有效果。
所以我觉得,当需要 3D 效果时才使用 NGUI 3D UI,默认就使用 2D UI 即可。