一、前言
寫完《Unity4.6新UI系統初探》后,我模仿手機上的UI分別用uGui和NGUI做了一個僅用作演示的ToggleSlider,我認為這個小小的控件已能體現自定義控件的開發過程。由於手頭上沒有mac版,暫時未能真機測試,PC上的效果如下:
二、制作過程
完整工程托管於github,分為uGui和NGUI兩個project。考慮到版權問題,工程里不含NGUI,同學們需自行將NGUI導進工程。NGUI需要Unity 4.5,uGui需要Unity 4.6。
三、功能點
- 滑塊可以拖動,從一邊拖到另一邊將改變控件值。
- 用戶停止操作時,滑塊如果居中,會自動滑向最近的一邊。
- 點擊滑塊或整個控件,控件值將被改變,滑塊自動滑向另一邊。
- 控件值被其它腳本修改時,滑塊自動滑向另一邊。
- 滑塊移動的過程中,如果值發生變化,滑塊會以當前位置為起點滑回去。
下面以uGui為例簡述制作方法,NGUI的方法也差不多,兩者的區別可參考下文[和NGUI對比]。
四、Hierarchy
上圖是用uGui制作好的層級結構。其中,
- Canvas負責渲染UI。
- Padding沒什么用,只是畫了一個邊框。
- Toggle Slider是控件的父物體。
- BackgroundAndMask使用ImageMask組件作為SymbolOff的遮罩,同時渲染灰色底圖。
- SymbolOff是灰色的twitter小鳥,坐標受動畫控制。
- Background_On使用Image組件渲染藍色高亮底圖,Color.alpha受動畫控制。
- BackgroundMaskOnly使用ImageMask組件作為SymbolOn的遮罩,並不渲染。
- SymbolOn是藍色的twitter小鳥,坐標受動畫控制。(不用Background_On作遮罩是因為藍色底圖的邊緣是半透明的。)
- Handle是正方形滑塊,坐標只受動畫控制。
- BackgroundAndMask使用ImageMask組件作為SymbolOff的遮罩,同時渲染灰色底圖。
- Current Value是下面那個可選框,用於測試Toggle Slider。
- Toggle Slider是控件的父物體。
- Padding沒什么用,只是畫了一個邊框。
- EventSystem可參考上篇文章。
五、Toggle Slider GO
Toggle Slider對象包含的Toggle Slider組件是唯一一個直接和控件有關的腳本。代碼可在github查閱,編寫起來很簡單。
六、Animation
所有效果都使用Animation組件實現,全部用動畫是為了偷懶,畢竟效果怎么實現都可以,這里僅作演示。動畫包含四條曲線,分別用於控制兩只twitter小鳥、藍色背景透明度和滑塊左右移動。這里簡單提幾個要點。
- 動畫的反向播放只需要將AnimationState.speed設為-1。
- 拖拽滑塊時,動畫暫停,根據鼠標位移逐幀設置動畫時間,然后Sample動畫。拖拽停止時恢復動畫。
- 在動畫里改變透明度時,Image組件不會自動更新,需要添加一個ColorWatcher組件,自己觸發Image.color的setter。
- 動畫設為ClampForever,因為Once無法在AnimationState中保留最后一幀的狀態。
七、Event
事件使用兩個Event Trigger組件進行響應。一個在Toggle Slider對象里,負責響應OnPointerUp,實現當點擊控件時,調用ToggleSlider.Toggle()。另一個在Handle對象里 (如圖),負責響應Drag事件,實現當拖動時調用ToggleSlider.OnDrag()。
在此遇到了一個蛋疼的問題,Event Trigger的Drop事件在這里無效,又沒有單獨的DragEnd事件,因此只好在Handle上增加OnPointerUp事件來監聽拖動是否結束。如此一來,Handle的OnPointerUp就會把上層控件的OnPointerUp事件攔截掉……我希望Unity能提供類似冒泡的機制,這樣一來我就能在Handle上添加一個腳本,只對拖拽結束進行響應,如果是單擊事件就冒泡到上層控件進行處理。
最終我的做法是,Handle的OnPointerUp事件也由ToggleSlider.OnPointerUp()響應,OnPointerUp內部通過dragging標記來判斷是拖動結束還是單擊。
八、不足
- Event Trigger沒有冒泡機制,子控件如果不處理事件,沒辦法拋給父控件處理。
- ImageMask沒能選擇alpha threshold。
九、存在的Bug
這段時間的測試遇到過幾個問題:
- 經常警告"Material uGUI/Stencil Mask doesn't have stencil ... SendWillRenderCanvases()"。有時會導致Image無法顯示,要換過一次Sprite之后才正常。
- 兩個Hierarchy內平級並且相鄰的ImageMask,都選中DrawImage,結果上面一個會擋住下面一個。需要在兩個中間插入一包含CanvasRenderer的GO才行,GO可以deactivated。
- 當我制作NGUI版本時,從uGui復制了一份出來再做修改。做到一半時我發現Hierarchy多了一個不含子物體的副本,當我選中控件時副本會同時被選中。於是我重啟Unity,發現Unity已經死鎖無法關閉,強制結束后項目損壞,只要一打開就crash,手動刪除scene后才恢復正常。估計是我在繼承樹上混用NGUI/uGui,或者uGui未剔除干凈引起的,已向官方反饋。
十、和NGUI對比
作為對比,我也用NGUI的測試版(3.6.4b)做了一樣的demo,花了不少時間。uGui的事件問題也在NGUI里遇到了,甚至更嚴重,此外還有其它問題。
-
NGUI的padding設置挺繁瑣的,uGui只要Rect Transform點下stretch,Left/Top/Right/Bottom全寫20就行。
添加padding時,我試着創建一個UIWidget,然后設置Anchors為Unified,然后依次設置Left/Top/Right/Bottom為Target's Left/Top/Right/Bottom,然后數值填入20/-20/20/-20才行。
-
NGUI添加Toggle有點復雜,uGui只要Hierarchy里Create一個就完事了。
創建調試用的Current Value時,找不到NGUI的Toggle組件,后來輸入名稱才找到,但還是不太會用。后來想到Examples里有toggle的prefab可以用,拖進Scene后對比了下發現NGUI的實現方式比uGui復雜了些,難以手工創建出來。看來Project里要把NGUI這些常見庫都備好才行。
-
NGUI設置Anchor有點失敗
將Toggle的prefab實例化到scene里后,設置了很久都沒能讓Toggle自動居中。難道這個Toggle的尺寸如果是動態的,NGUI就沒辦法自動居中?或許是我對NGUI還不是很了解,最終我只能根據Toggle寬度算出坐標偏移。
-
NGUI沒有Image Mask
所以這個版本沒能加入那兩個twitter的logo。這個怪不了NGUI,因為Unity的free版不提供訪問stencil buffer的功能,因此第三方UI插件沒辦法實現比較好的clipping機制。
-
NGUI的UIEventTrigger無法獲得事件參數
UIEventTrigger和uGui的EventTrigger類似,能夠觸發遠程方法。但是NGUI不能傳入動態事件參數,雖然能用 UIEventTrigger.current獲得當前事件,可UIEventTrigger對象其實沒定義任何參數。要獲得參數,只能自己寫一個帶有 OnDrag的組件,附加到GameObject上,或者使用UIEventListener,總之就不支持可視化編輯,只能用代碼動態綁定事件。
-
NGUI的UIEventListener無法響應停止拖動事件
為了解決前一個問題,我使用了UIEventListener來獲得拖動參數。然而當我想響應停止拖動事件時,我發現還是要用回UIEventTrigger才行。如果用戶不希望混用這兩個腳本,那么只能自己寫一個腳本。
十一、小結
uGui功能和用戶體驗方面都做的不錯,是我看到過最貼近Unity風格的UI系統。穩定性方面有小問題,不過作為測試版可以理解,已經超過了我的預期(之前以為會和4.0的剛推出Mecanim一樣bug一堆)。
性能方面,兩個工程我都實現了相同的PackedBenchMark場景,里面各包含了30個ToggleSlider,為了公平uGUI版本去掉了所有ImageMask,兩邊實測drawcall一致。從幀率上看在編輯器下NGUI性能優於uGUI大約20%!估計是因為NGUI在最近幾個版本中完善了batching機制,而uGUI並沒有采用前一篇文章所說的"更優的"batch算法,而是把batching粗暴的交給了顯卡驅動完成。如果有pro版的話使用profiler查看一下兩邊的CPU/GPU占用就能知道答案。
文獻資料
本文轉載自https://github.com/jaredoc/unity-ugui/tree/master/toggle_demo