全面理解 Unity UI 系統

原地址:http://www.cnblogs.com/whitecat/p/4159815.html

隨着 Unity 4.6 發佈,新 UI 系統終於與大家見面了。這篇文章將不會介紹如何使用按鈕、滾動條之類的UI控件,這些內容可以參考 Unity Manual;這篇文章的重點是,如何理解 UI 系統的設計,以便更好的在實際中使用它。

 

RectTransform

Unity UI 系統使用 RectTransform 實現基本的佈局和層次控制。RectTransform 繼承於 Transform,所以 Transform 的所有特徵 RectTransform 同樣擁有。在 Transform 基礎上,RectTransform 增加了 軸心(pivot)、錨點(實際上是用 anchorMin、anchorMax 兩個點定義的矩形區域)、和 尺寸變化量(sizeDelta)。

軸心:表示UI元素的中心,使用相對於自身矩形範圍的百分比表示的點位置,這會影響定位、縮放和旋轉。

錨點:相對於父級矩形的子矩形區域,這個矩形各個邊界值使用百分比表示。

尺寸變化量:相對錨點定義的子矩形的大小變化量,與錨點定義的子矩形合併後的區域纔是最終的UI矩形。

在 Inspector 界面上,爲了更方便的調節 RectTransform 的屬性,錨點的兩個點重合時會顯示位置和寬高(直接調節位置和sizeDelta),否則顯示相對錨點矩形邊界的偏移量(通過計算後再賦值給位置和sizeDelta)。在程序中,RectTransform 添加了 anchoredPosition 和 rect 屬性來更方便的編程。

RectTransform 組件同樣負責組織 GameObject 的層級關係。在 UI 系統中,子級 UI 對象總是覆蓋顯示在父級 UI 對象上;層級相同的 UI 對象,下方的 UI 對象總是覆蓋顯示在上方的 UI 對象上。這樣的設計避免了繁瑣的深度設置。在程序中,Transform 添加了 SetSiblingIndex、GetSiblingIndex、SetAsFirstSibling、SetAsLastSibling 這些方法來方便的修改物體的層級順序。

 

EventSystem

如果你使用 UI 系統,那麼 EventSystem 對象會自動創建。這個對象負責監聽用戶輸入。默認情況下,在電腦上可以使用鍵盤和鼠標輸入,在移動設備上可以使用觸摸輸入。但是如果你要爲surface這樣的設備開發,你也可以同時啓用兩種輸入。當需要屏蔽用戶輸入時,將此對象關閉即可。UnityEngine.EventSystems.EventSystem.current 保存了當前活動的 EventSystem 對象。

 

Canvas

Canvas 是其他所有 UI 對象的根。在一個場景裏 Canvas 數量和層級都沒有限制。子 Canvas 使用與父 Canvas 相同的渲染模式。一個 Canvas 有三種渲染模式:

Screen Space - Overlay:UI元素相對於屏幕空間,以2D方式顯示在任何相機畫面的上面。這是非常標準的 UI 風格。典型例子:大量窗口、文本和按鈕的策略遊戲。

Screen Space - Camera:UI元素相對於屏幕空間,由指定的相機負責顯示,相機的參數影響顯示的結果。你可以把 Canvas 理解爲相機的子物體。典型例子:射擊遊戲屏幕上的 3D HUD。

World Space:UI元素相對於世界空間,和其他場景裏的物體一樣有世界位置、遮擋關係。通常用來做非常創新的 UI 設計。例子:遊戲內的手機屏幕、與場景綁定的遊戲指導等。

 

CanvasScaler

這個組件負責屏幕適配。UI 系統使用 RectTransform 來計算 UI 的位置和大小,但這還不夠。如何讓設計的 UI 可以適配不同的分辨率、寬高比和 DPI?這個組件給出了以下3種適配方法,注意任何一種適配方法都不會改變UI的寬高比和相對定位。

Constant Pixel Size:通過調節 Canvas 像素大小來維持縮放不變。它的意思是在任何屏幕上不改變 Canvas 的縮放係數(Scale Factor),而是調節 Canvas 的像素大小與屏幕保持一致。你可以手動或通過代碼調節 Canvas 的縮放係數。這是 UI 系統默認的適配方案。如下圖兩種分辨率下相同的UI顯示的不同之處,雖然不同屏幕下UI元素定位、大小沒有發生變化(圖中兩個白色元素定位分別爲屏幕左上角和右下角),但是較小的屏幕上UI元素佔用了大部分屏幕空間,顯得更擁擠。這就是這種適配方式的缺點,小屏幕太擁擠、大屏幕太空曠,沒有考慮到屏幕的分辨率和DPI。但是這種模式的好處是UI元素可以保持設計時的細節(因爲沒有縮放)。這種模式可能適用於:你希望UI在一定範圍內按原始大小顯示,這樣既可以讓UI顯示的儘可能清晰、又可以讓屏幕較大的玩家擁有更廣闊的視野,但是在太小或太大的屏幕上,你可以通過程序來調節縮放係數,不至於小屏幕被UI佔滿、大屏幕找不到UI。

Scale With Screen Size:根據屏幕分辨率縮放。這可能是大部分遊戲最方便的適配方法。在這種模式下,你需要指定一種設計分辨率,然後指定縮放的算法。無論哪種縮放算法,如果實際寬高比與設計寬高比相同,UI 都會被等比縮放。實際上,Canvas 只是保持自己的大小和設計分辨率一致。如果實際寬高比與設計寬高比不同,這時縮放算法纔會影響顯示結果。縮放算法有三種:擴展、收縮 和 匹配寬高。擴展算法的邏輯是,擴大 Canvas (在寬高比上)較短的一邊,使得 Canvas 寬高比與屏幕一致。如下左圖,設計分辨率寬高比爲1:1(紅色線框),實際屏幕更寬所以 Canvas 的 width 增加以匹配屏幕。這樣的算法在寬高比不同的屏幕上將始終導致UI更“開闊”。收縮算法的邏輯是,收縮 Canvas (在寬高比上)較長的一邊,使得 Canvas 寬高比與屏幕一致。如下中圖,設計分辨率寬高比爲1:1(紅色線框),實際屏幕更窄所以 Canvas 的 height 減小以匹配屏幕。這樣的算法在寬高比不同的屏幕上將始終導致UI更“緊湊”。匹配寬高的算法邏輯是,根據指定的權重,同時調節 Canvas 的寬和高,使得 Canvas 寬高比與屏幕一致。如下右圖,設計分辨率爲紅色線框,設定寬度和高度的權重相等(0.5),實際屏幕上 Canvas 的寬和高都被調整以匹配屏幕。這樣的算法目的是,通過可調節的寬高權重,儘可能的保持UI的原始設計。

Constant Physical Size:通過調節 Canvas 物理大小來維持縮放不變。它的意思是在任何屏幕上不改變 Canvas 的 DPI,而是調節 Canvas 的物理大小總是與屏幕保持一致。這種說法可能比 Constant Pixel Size 更難以理解,實際上他們本質是一樣的,只不過 Constant Pixel Size 通過邏輯像素大小調節來維持縮放,而 Constant Physical Size 通過物理大小調節來維持縮放。使用這種模式必須指定一個像素轉換物理大小的因數(填寫96方便在windows上進行開發)。運行時通過具體設備報告的dpi計算 Canvas 像素大小和縮放係數。這種模式從設計的意圖來看,是爲了在開發時使用物理單位而非像素單位,這隻會讓程序和美術的工作變得複雜,實際使用價值並不高。因爲開發人員更關心設計的像素分辨率,他們需要繪製明確的像素大小的圖片!如果未來開發人員和玩家都使用了超高DPI的顯示器,那時或許會更注重物理尺寸。

 

Selectable

可交互UI組件的基類。它負責響應用戶的輸入,產生視覺變化、切換導航目標 以及 處理通用的UI事件。

Transition:可交互組件有4種視覺狀態:正常(normal), 高亮(highlighted), 按下(pressed)和 禁用(disabled)。Selectable 根據用戶的輸入和自己當前的狀態執行狀態切換,切換狀態的視覺效果有4種類型:none、color tint、sprite swap 和 animation。使用 animation 效果必須再添加一個 animator 組件,此動畫控制器含有上述4種狀態。可以通過Auto Generate Animation 按鈕自動添加組件並創建動畫控制器。

Navigation:可以使用鍵盤和遊戲控制器切換導航目標,如果你要開發一個僅使用遊戲控制器就可以玩的遊戲(主機遊戲),那麼這個功能非常重要,因爲玩家沒有鼠標也無法使用觸摸屏,只能通過按鈕來切換導航目標。這個功能被設計的非常完善,以至於你幾乎什麼都不用做就可以處理的很好。一共有5種導航選項:不使用(None)、水平(Horizontal)、垂直(Vertical)、自動(Automatic)、顯式指定(Explicit)。在非顯式指定的情況下,導航系統根據每個UI元素的矩形位置和大小,自動查找4個方向上是否存在最合適的切換目標。如果選擇顯式指定,需要爲4個方向指定切換目標(Selectable)。在Inspector 界面點擊 Visualize 按鈕可以查看導航路徑(下圖中的黃色線條),在 EventSystems 中可以設置默認選中的對象。

通用事件:OnSelect, OnDeselect, OnPointEnter, OnPointExit, OnPointDown, OnPointUp, ... 。重寫這些方法來定義自己的可交互組件。文末將通過一個例子來說明如何實現自定義控件。

 

Auto Layout

自動佈局用於簡化UI的佈局工作。自動佈局基於 RectTransform 的佈局系統,包含 佈局元素(Layout Elements) 和 佈局控制器(Layout Controllers)兩個概念。

佈局元素含有 最小尺寸、首選尺寸 和 可選尺寸 這些參數,佈局控制器根據這些參數來調整佈局元素的大小和位置。佈局控制器調整的基本原則是:首先分配最小尺寸,然後如果還有足夠空間就分配首選尺寸,最後如果還有空間則分配可選尺寸。一個含有 RectTransform 組件的遊戲對象就是一個佈局元素。添加某些組件會修改佈局元素的參數。LayoutElement 是一個用來修改默認佈局參數的組件。佈局控制器以多種組件的形式存在,它們控制自身或子級的佈局元素的大小和位置。關於各種佈局控制器組件的功能和使用方法請參考 Unity 文檔。

 

Rich Text

默認情況下一個 Text 組件以單一樣式顯示所有文本,使用富文本可以讓顯示樣式更豐富,比如高亮部分文本。實際上,富文本功能不僅可以用在 UI 系統,還可以用在 Legacy GUI 系統 和 Debug 中。

富文本使用方法類似 html 標籤,比如 "<b>Hello</b>" 將顯示爲加粗的 "Hello"。這些標籤還可以嵌套使用。可用的標籤有 b(加粗)、i(傾斜)、size(大小)和 color (顏色)。其中 size 和 color 必須指定屬性值,如:"<size=50>Hello</size>" "<color=#FF0080FF>Hello</color>"。size 屬性的單位是像素,color 屬性使用RGBA格式的16進製表示顏色或直接填寫常用顏色名稱。下圖爲使用示例。

如果使用 TextMesh,還可以使用 material 和 quad 標籤。material 需要指定 material 數組中的 material 下標,如 "<material=1>Cool</material>";quad 標籤沒有結束標籤,通常用來在文本中顯示一個圖片,如 "This is me: <quad material=2 size=24 x=0 y=0 width=1 height=1 />"。

 

UnityEvent

一個可序列化的、可顯示在 Inspector 上的事件類型。典型的用途就是 Button 的 OnClick 事件。拖拽一個對象或組件到方框中,就可以選擇事件觸發時調用的方法。可選方法必須是公開的、無返回值的、含有0個或1個可序列化的參數。也可以調用 set 類型的屬性。你也可以在自己的腳本里使用 UnityEvent,只要定義一個序列化的字段,就可以和 OnClick 看起來一樣了!

UnityEvent 可以通過代碼添加、移除或調用方法。和 C# 的 delegate 相似。另外還有泛型版本的 UnityEvent,最多支持 4 個參數,但是實用價值不大,因爲你需要自己實現編輯器來保存參數。

 

自定義控件

通過此自定義控件的例子,來說明如何靈活運用 UI 系統各種功能實現各種奇葩需求。

需求:實現一種角色點數分配的控件,角色有一定數量的點數,可以分配給多種屬性,每種屬性都可以分配一定範圍的點數,各屬性點數之和不能超過角色擁有的點數。一般的實現方法是,爲每個角色顯示剩餘點數和多條屬性滑塊,拖動每個滑塊會改變剩餘點數。這裏我們要求把所有滑塊放到一個大滑動條中,可以直接拖動每個滑塊。這樣的好處是可以直觀的看到每個角色總能力對比以及剩餘可分配點數。如下爲設計圖。

 我們先分析這個需求:整個滑動條代表點數總量;所有滑塊是左對齊拼接的;每個滑塊具有自身最大值、最小值限制;所有滑塊總長度不超過整個滑動條;每個滑塊是可交互的。可以得到初步的設想是,每個滑塊擁有一個繼承 Selectable 的組件,重寫按下和彈起的事件,在這個過程中,控制滑塊的長度。但是如何保持所有滑塊始終左對齊呢?一種直接的想法是使用水平自動佈局。但是這裏我們不這麼做(你可以試試這樣來實現),而是充分利用 RectTransform 錨點的功能,讓每一個滑塊對其到前一個滑塊的右端。這樣要求每個滑塊是前一個滑塊的子級。解決方案有了,再考慮一下讓這件事情更簡單、更通用一些,所有初始參數填寫在一個根部的組件裏,這個組件根據這些參數來自動創建所有滑塊。那麼一個滑塊需要的參數大概就是這樣的吧:

複製代碼
// 屬性[Serializable]class Attribute{    public string name;    public Sprite image;    public Color color;    public int min;    public int max;    public int value;    [NonSerialized]    public ValueSlider valueSlider;}
複製代碼

min、max、value 分別爲 最小點數、最大點數 和 初始點數。最後一個 ValueSlider 就是我們自定義的 Selectable 組件,在屬性裏保存對應的組件引用方便對其進行修改。ValueSlider 定義如下:

複製代碼
// 滑塊class ValueSlider : Selectable{    MultiAttributesSlider _multiAttributesSlider;    Attribute _attribute;    public void Init(MultiAttributesSlider multiAttributesSlider, Attribute attribute)    {        _multiAttributesSlider = multiAttributesSlider;        _attribute = attribute;    }    public override void OnPointerDown(PointerEventData eventData)    {        base.OnPointerDown(eventData);        _multiAttributesSlider.BeginSlide(_attribute, eventData);    }    public override void OnPointerUp(PointerEventData eventData)    {        base.OnPointerUp(eventData);        _multiAttributesSlider.EndSlide();    }}
複製代碼

初始化的時候保存根部組件(起個名字叫“多屬性滑動條”)和對應的屬性引用,然後按下和彈起時分別調用根部組件的開始滑動、結束滑動方法。沒有什麼實際的內容,主要的操作都在根部組件上。根部組件定義如下:

複製代碼
class MultiAttributesSlider : MonoBehaviour{    // 總點數    [SerializeField]    int _totalValue;    // 屬性數組    [SerializeField]    Attribute[] _attributes;    //剩餘點數    int _restValue;    // 一個點數對應的像素大小    float pixelsPerPoint;    // 保存滑塊按下時的信息    Attribute _currentAttribute = null;    PointerEventData _eventData;    int _beginValue;    int _beginRestValue;    // 當鼠標按下任何一個滑塊時調用    public void BeginSlide(Attribute currentAttribute, PointerEventData eventData)    {        _currentAttribute = currentAttribute;        _eventData = eventData;        _beginValue = currentAttribute.value;        _beginRestValue = _restValue;    }    // 當鼠標從任何一個滑塊釋放時調用    public void EndSlide()    {        _currentAttribute = null;    }    // 初始化    void Awake()    {        // 需要通過自定義編輯器來保證 Inspector 填寫的參數完全合理。這個例子忽略這一步。        // 統計已使用的點數        int valueCount = 0;        for(int i=0; i<_attributes.Length; i++)        {            valueCount += _attributes[i].value;        }        // 計算剩餘點數        _restValue = _totalValue - valueCount;        RectTransform lastParent = transform as RectTransform;        // 計算一個點數對應的像素大小        pixelsPerPoint = lastParent.sizeDelta.x / _totalValue;        // 創建每個滑塊;更好的做法是,在自定義編輯器中使用一個按鈕來生成所有滑塊        for(int i=0; i<_attributes.Length; i++)        {            GameObject slider = new GameObject(_attributes[i].name);            // 初始化 RectTransform            RectTransform rect = slider.AddComponent<RectTransform>();            rect.SetParent(lastParent, false);            rect.localScale = Vector3.one;            rect.localRotation = Quaternion.identity;            rect.pivot = new Vector2(0, 0.5f);            rect.anchoredPosition = Vector2.zero;            if (i == 0)            {                rect.anchorMin = Vector2.zero;                rect.anchorMax = new Vector2(0, 1);            }            else            {                rect.anchorMin = new Vector2(1, 0);                rect.anchorMax = Vector2.one;            }            rect.sizeDelta = new Vector2(pixelsPerPoint * _attributes[i].value, 0);            // 初始化 Image            Image image = slider.AddComponent<Image>();            image.sprite = _attributes[i].image;            image.color = _attributes[i].color;            image.type = Image.Type.Sliced;            image.fillCenter = true;            // 初始化 ValueSlider            _attributes[i].valueSlider = slider.AddComponent<ValueSlider>();            _attributes[i].valueSlider.Init(this, _attributes[i]);            // 將當前 RectTransform 作爲下一個滑塊的父級            lastParent = rect;        }    }    // 更新滑塊的值    void Update()    {        if(_currentAttribute != null)        {            // 計算滑動距離對應的點數變化            int deltaValue = Mathf.RoundToInt((_eventData.position.x - _eventData.pressPosition.x) / pixelsPerPoint);            // 受最小、最大值限制的點數變化            deltaValue = Mathf.Clamp(_beginValue + deltaValue, _currentAttribute.min, _currentAttribute.max) - _beginValue;            // 更新剩餘點數            _restValue = _beginRestValue - deltaValue;            // 如果剩餘點數用完,需要減少點數變化            if(_restValue < 0)            {                deltaValue += _restValue;                _restValue = 0;            }            // 更新當前點數            _currentAttribute.value = _beginValue + deltaValue;            // 更新滑塊大小            (_currentAttribute.valueSlider.transform as RectTransform).sizeDelta                = new Vector2(pixelsPerPoint * _currentAttribute.value, 0);        }    }}
複製代碼

代碼的含義在上面的分析和註釋裏寫的很清楚了,不再贅述。這個組件最好再配合一個自定義的編輯器,但是這裏就不想寫了,如果你感興趣可以試試。

下面就是測試。創建一個滑動條背景,添加此腳本,填寫參數,最後看起來這樣的:

然後運行起來吧。圖就不截了,就是上面的設計圖......最後再來張合影。

 

如果你覺得我的文章有價值,點個“推薦”、加個“關注”什麼的我也不會介意的......
【白貓,博客園首頁:http://www.cnblogs.com/whitecat/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章