我们知道,如今的移动端设备分辨率五花八门,而开发过程中往往只取一种分辨率作为设计参考,例如采用1920*1080分辨率作为参考分辨率。
选定了一种参考分辨率后,美术设计人员就会固定以这样的分辨率来设计整个游戏的UI概念图;而这时就需要程序尽可能精准的匹配各种不同屏幕的分辨率。
好在Unity ugui中自带Canvas适配:
例如,我们要在手机上采用竖屏设计,可能就会用到如上这样的参考分辨率,这时Canvas画布会自动检测当前的屏幕分辨率并进行缩放。
为了更直观的了解ugui的缩放原则,我们可以直接通过实验测试数据来观察:
如上所示,此时我设置的测试分辨率为1440*2960,因为设置的是按照参考分辨率的宽度进行匹配,所以整个画布的高度就会变为2960*1080/1440=2220;同样的,画布的宽度是这样计算的1440*1080/1440=1080。
同时,画布也按照相应的比例进行了缩放1440/1080=1.333333...
通过上面的观察我们可以发现,当以宽度进行适配时,只与参考分辨率的宽度和屏幕分辨率的宽度有关,是以这两个数值的比例进行的画布缩放;
同样的道理,如果我们设置为以高度进行匹配,就与屏幕的宽度和参考分辨率的宽度无关了,而只与对应高度的比值有关。
上面这一点非常重要,一定要非常清楚的,不然很可能会在适配和坐标转换时踩坑。(例如很多人是宽度按宽度适配和缩放,高度按高度适配和缩放,最后计算的结果可想而知!)
现在的问题就在于,什么时候应该适配参考分辨率的宽度,什么时候应该适配高度呢。
最好的方法是以最小的缩放幅度来达到适配UI的目的,也就是说,我们需要比较当前屏幕的宽高比与参考分辨率的宽高比之间的大小,最理想的情况当然是双方宽高比相同,那就无论匹配宽还是高都一样,也无需进行任何比例的缩放就能完美适配。
但事实上这种可能性几乎为零,当参考分辨率的宽高比大于屏幕分辨率的宽高比时,此时屏幕分辨率看上去会比参考分辨率显得更高,所以此时应该以参考分辨率的宽度进行匹配,将高度进行对应比例的压缩,宽度则保持不变。
如果此时还以高度进行匹配,则缩放幅度明显会比之前大,此时宽度的改变值会比高度的改变值更大,这样就无法达到最低限度的画布缩放。
1 using UnityEngine; 2 using UnityEngine.UI; 3 4 [RequireComponent(typeof(CanvasScaler))] 5 public class FixCanvasTool : MonoBehaviour 6 { 7 void Awake() 8 { 9 FixResolution(); 10 } 11 12 public void FixResolution() 13 { 14 CanvasScaler scaler = GetComponent<CanvasScaler>(); 15 16 float sWToH = scaler.referenceResolution.x * 1.0f / scaler.referenceResolution.y; 17 float vWToH = Screen.width * 1.0f / Screen.height; 18 if (sWToH > vWToH) 19 { 20 //匹配宽 21 scaler.matchWidthOrHeight = 0; 22 } 23 else 24 { 25 //匹配高 26 scaler.matchWidthOrHeight = 1; 27 } 28 } 29 }
上面的脚本实现了前面所说的原理,将它挂载到Canvas的根节点上就可以自动按照屏幕分辨率以最优化的缩放方式适配不同分辨率的屏幕。
下面来讨论进行过缩放后的ugui中如何显示指定三维世界坐标位置的点。
这种功能是十分常见的,例如我们在场景中打一个怪物,怪物在三维空间的世界坐标系中,但击中它后我希望在Canvas画布上对应的位置(例如就在怪物头上)显示当前怪物受到的伤害数值。
当然了,如果你坚持再创建一个基于场景中三维空间的画布,那我无话可说,但更好的做法显然是统一在一个二维画布的对应屏幕位置正确显示,这样你每个场景只需要统一管理一个Canvas即可。
如果你很熟悉GPU渲染管线的知识,那这里的坐标系转化对你来说应该就再简单不过了。
我们知道,一个点要在屏幕当中显示,需要经历以下坐标系的转换,首先转化为场景空间的世界坐标,然后转化为观察空间的坐标(摄像机坐标),此时Z轴的值代表摄像机的深度值。
得到观察空间的坐标后,就可以很方便的按照屏幕分辨率的值进行转化了,从而得到屏幕空间的坐标。如果是在写Shader的话中间还包括裁剪空间。
得到屏幕坐标后,此时的坐标并不能直接就按照该值点在画布上,因为屏幕坐标值和画布所给的参考分辨率的值一般是不相同的,所以这个值还要按照一定的缩放比例点在画布正确的位置。
需要注意的是,网上很多的转化方式都是有问题的,很多都是屏幕宽度按照参考参考分辨率的宽度缩放,屏幕高度按照参考分辨率的高度缩放,看上去好像没有任何问题。
但如果你的UI已经进行过适配,并且只按照其中一种模式进行匹配,那计算出来的结果就会完全不同。
下面给出参考:
1 public Vector2 WorldPosToUIPos(Vector3 worldPos,Canvas canvas) 2 { 3 //摄像机空间值域[0,1],z轴值代表深度 4 var viewPos = Camera.main.WorldToViewportPoint(worldPos); 5 //按照值域进行裁剪 6 if (viewPos.x >= 0 && viewPos.x <= 1 && viewPos.y >= 0 && viewPos.y <= 1) 7 { 8 //屏幕空间高度值 9 float sheight = viewPos.y * Screen.height; 10 //屏幕空间宽度值 11 float swidth = viewPos.x * Screen.width; 12 //适配转化 13 return new Vector2(swidth.GetFixed(canvas), sheight.GetFixed(canvas)); 14 } 15 //返回一个固定值-1代表不在屏幕当中 16 return -Vector2.one; 17 }
GetFixed为float类型的扩展方法:
1 //Screen坐标值适配Canvas画布 2 public static float GetFixed(this float value, Canvas canvas) 3 { 4 var cs = canvas.GetComponent<CanvasScaler>(); 5 if (cs.matchWidthOrHeight == 0) 6 //匹配宽度时仅按照宽度计算 7 return value * cs.referenceResolution.x / Screen.width; 8 else 9 //匹配高度时仅按照高度计算 10 return value * cs.referenceResolution.y / Screen.height; 11 }
需要注意的是,这里只进行高度或宽度的单一匹配,不进行混合计算。返回的值是以屏幕左下角为坐标原点得到的UIPos,因为默认情况下二维屏幕计算坐标轴就是以左下为原点的。(当然这是因为Unity内部对不同平台例如OpenGL和Direct3D进行了统一)
如果锚点(Anchor,注意和Pivot轴心区分)正好在左下:
则可以直接设置上面函数的返回值:
ShowUI.GetComponent<RectTransform>().anchoredPosition=WorldPosToUIPos(moster.transform.position+offse,canvas);
即使锚点不在左下,也只需要按照锚点的位置再进行简单的坐标转换即可。
注意在Canvas下不要用transfrom.localPosition设置元素的位置,最好采用anchoredPosition来设置以保证无论怎么改分辨率该值都不发生变化。
anchoredPosition显示的就是在Inspector面板中根据锚点计算后显示的Pos X,Pos Y的值。
2019年12月26日更新:
更新一个刘海屏的适配方案:
在游戏的全局系统设置中增加可以压缩canvas左右边缘的设置滑条,类似于这样:
通过该滑条的设置向左或向右来滑动场景中的canvas画布边缘向左或向右偏移。
下面是具体功能实现的脚本:
1 using System.Collections.Generic; 2 using UnityEngine; 3 using AGrail; 4 5 6 //这个脚本用于执行调整UI的边缘 7 public class UIEdgeFix : MonoBehaviour 8 { 9 [SerializeField] 10 private List<RectTransform> roots = new List<RectTransform>(); 11 12 private void Start() 13 { 14 FixEdge(GameManager.UIInstance.UIEdge); 15 } 16 17 public void FixEdge(float value) 18 { 19 foreach (var root in roots) 20 { 21 //判断为四周扩展类型的锚点预设 22 if (root.anchorMin == Vector2.zero && root.anchorMax == Vector2.one) 23 { 24 if (value > .5f) 25 { 26 //设置左下 27 root.offsetMin = new Vector2((value - .5f) * 200, 0); 28 root.offsetMax = new Vector2(0, 0); 29 } 30 else31 { 32 //设置右上 33 root.offsetMax = new Vector2(-(.5f - value) * 200, 0); 34 root.offsetMin = new Vector2(0, 0); 35 } 36 } 37 } 38 } 39 }
原理非常简单,根据滑条传入的值来判断是那一边的画布需要被压缩移动。
需要注意的是,在canvas下的根节点处必须将锚点预设为四周扩展的类型,然后canvas下的其他元素全部位于根节点下作为子物体。(背景图元素除外)
一般来说,规范的canvas布局也理应是如此。
这样做的好处是随时可以很方便的调整整个canvas窗口距离屏幕边缘的距离。
当滑条的值改变时更新调用所有canvas上的UIEdgeFix 脚本:
1 public void OnUIEdgeChange(float vol) 2 { 3 GameManager.UIInstance.UIEdge = vol; 4 var fixList = FindObjectsOfType<UIEdgeFix>(); 5 foreach (var item in fixList) 6 { 7 item.FixEdge(vol); 8 } 9 }
将改变的值随时记录到本地:
1 public float UIEdge 2 { 3 set 4 { 5 PlayerPrefs.SetFloat("UIEdge", value); 6 } 7 get 8 { 9 if (!PlayerPrefs.HasKey("UIEdge")) 10 PlayerPrefs.SetFloat("UIEdge", 0.5f); 11 return PlayerPrefs.GetFloat("UIEdge"); 12 } 13 }