我們知道,如今的移動端設備分辨率五花八門,而開發過程中往往只取一種分辨率作為設計參考,例如采用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 }