Unity3D事件系統和EventSystem

你的財神爺發表於2019-01-11

Unity的事件系統提供了多種使用方式,又和物理碰撞結合在一起,所以同樣使用Unity事件處理,就能寫出各種各樣的風格。很多專案還會自己對事件在進行一次封裝,有的還會使用第三方外掛。無論是手勢外掛還是UI外掛,都是要建立在事件系統之上的,這些外掛都會各自針對事件進行封裝。所以,混亂,未知,衝突在所難免。

本文針對Unity2017的版本,對事件系統進行梳理和解讀,然後對EventSystem的使用和最佳實踐給出一套方案。

Unity事件處理的種類

1. 系統回撥OnMouse事件

首當其衝的就是MonoBehavior上的事件回撥,可以參看MonoBehaviour文件。這是一系列的OnMouse開頭的回撥函式。

OnMouseDown
OnMouseDrag
OnMouseEnter
OnMouseExit
OnMouseOver
OnMouseUp
這個處理方式有以下幾個特點:

MonoBehavior所在的GameObject需要有Collider碰撞元件,並且Physics.queriesHitTriggers設定為True,這個在Edit -> Physics Settings -> Physics or Physics2D中設定。

或者MonoBehavior所在的GameObject存在GUIElement。

OnMouse處理函式可以是協程。

GameObject所有MonoBehavior實現OnMouse的函式都會呼叫。

Collider或GUIElement的層級順序,會遮擋事件的傳遞。

按照官方的解釋,這是GUI事件的一部分,參看EventFunctions。設計的初衷也是為了GUI服務的。參看ExecutionOrder最後的unity執行流程圖,會發現OnMouse事件是一個獨立的Input Event。

可以看到,OnMouse事件在,Physics事件之後,Update之前,記住這個順序,後面會用到。並且,這是引擎本身回撥的,就引擎使用而言可以看成是,訊息驅動。至於引擎的實現,可是輪詢也可以是訊息驅動。

2. 在Update中輪詢Input物件

public class ExampleClass : MonoBehaviour

    public void Update() 
    { 
       if (Input.GetButtonDown("Fire1")) 
       { 
            Debug.Log(Input.mousePosition); 
       } 
    }
}
這是官方的例子,Input擁有各種輸入裝置的資料資訊。每一幀不斷的檢測,檢視有沒有需要處理的輸入資訊,利用GameObject本身的層級順序來控制Update的呼叫順序,從而控制了Input的處理順序。

Input的資訊由引擎自己設定的,明顯Unity需要實現不同平臺的事件處理,然後對Input進行設定。另外有一個InputManager皮膚用來配置Input相關屬性的,在Edit -> Physics Settings -> Input中。

由前面的執行流程圖可知,OnMouse事件會在Update之前呼叫,當然我們也可以在OnMouse中使用Input,這樣就變成了訊息驅動,而不是輪詢了。但這樣的缺點是,事件必須由touch或pointer碰撞觸發,比如鍵盤或控制器按鈕的事件就沒有辦法捕獲了。

3. EventSystem

最常見的是在UGUI中,用來進行UI的事件處理和分發。但看其命名,就知道這並不是一個僅僅針對UI的事件系統。參看文件介紹,EventSystem,可以看到:

The Event System is a way of sending events to objects in the application based on input, be it keyboard, mouse, touch, or custom input. The Event System consists of a few components that work together to send events.
EventSystem基於Input,可以對鍵盤,滑鼠,觸控,以及自定義輸入進行處理。EventSystem本身是一個管理控制器,核心功能依賴InputModule和Raycaster模組。

Input Module

用來處理Input資料,管理事件狀態,和傳送事件給GameObject。

這是一個可替換模組,比如引擎自帶了,StandaloneInputModule和TouchInputModule,也可以自定義。

Raycaster

用來捕獲哪些GameObject需要執行事件處理。一共有3個種類。

Graphic Raycaster 用於UI元素就是繼承自Graphic的物件。所以button這樣的Selectable物件需要一個Target Graphic物件。
Physics 2D Raycaster 用於2D物理碰撞元素,依賴於Collider2D。
Physics Raycaster 用於3D物理碰撞元素,依賴於Collider。


通常,canvas只用了Graphic Raycaster,用來處理UI的事件。所以只要是繼承Graphic物件都會自動獲得EventSystem事件監聽。但官方文件有這樣的說明:

If you have a 2d / 3d Raycaster configured in your scene it is easily possible to have non UI elements receive messages from the Input Module. Simply attach a script that implements one of the event interfaces.
也就是說,場景如果新增了2d / 3d Raycaster的射線檢測,那麼EventSystem也會檢測相應的物理元素。(後面會詳細介紹這種混合的使用模式)

SupportedEvents

這是EventSystem預設支援的事件處理回撥,當然也可以自定義,就需要擴充套件自己的Input Module來實現。這裡需要強調幾點:

IMoveHandler,ISubmitHandler 這樣的回撥事件可以接受鍵盤輸入,可以在InputManager皮膚裡配置自定義的值,不然就會使用預設值。

鍵盤事件需要Selectable物件,比如button就是繼承自Selectable。所以當button被選中的時候,就會響應鍵盤事件,比如回車和上下左右方向鍵,還有空格鍵。這時候,在button所在GameObject繫結一個實現了ISubmitHandler或IMoveHandler介面的指令碼,也會同時觸發。

另外,如果我想使用Collider來觸發這個鍵盤事件,就需要使用一個Selectable物件。Collider與Selectable放在一起,並且掛載一個實現了實現了ISubmitHandler或IMoveHandler介面的指令碼,當Collider被選中的時候,就可以觸發鍵盤事件了。

最後,系統提供了一個EventTrigger元件。這僅僅是針對SupportedEvents的視覺化封裝。在皮膚上拖放配置就用EventTrigger,用程式碼繫結就用實現介面的方法。這就像UnityEvent和C# event的關係。
MessagingSystem

這是EventSystem的訊息傳遞系統,UGUI就是使用了這個機制來傳送事件訊息的。文件寫的比較清楚,我們可以自定義自己訊息傳遞。值得注意的有兩句話:

The new UI system uses a messaging system designed to replace SendMessage. The messaging system is generic and designed for use not just by the UI system but also by general game code.
這個訊息系統是用來替換SendMessage,實際專案估計也很少會用SendMessage,因為效率不高。另外,這是一個通用的訊息系統,不僅僅是針對UI的,而是通用的機制。

不過,我仍然覺得這種搜尋GameObject查詢介面型別呼叫的方式,沒有Action直接訂閱呼叫來的高效。

EventSystem 與 射線檢測的衝突問題

如果EventSystem僅僅用來處理UI事件的時候,就會與我們自己手動的射線檢測產生衝突,Physics.Raycast(ray, out hit),原因是顯而易見的,因為PhysicsGraphic只會過濾Graphic物件並且有自己的Raycast呼叫。我們自己手動的Raycast就會穿透過去。

那為什麼我們需要自己呼叫Raycast呢 ?其原因在於,我們使用了Collider碰撞檢測,UI系統並不會處理。這時候,我們就需要使用EventSystem的IsPointerOverGameObject()方法來判斷,有沒有選中了UI元素。具體的解決方案參看我的上一篇文章。

但現在我們知道EventSystem也是可以處理Physics元素的,那麼我們就可以放棄手動Raycast,轉而讓EventSystem統一處理。

EventSystem混合處理Physics

首先,我們看一個官方文件的說明 Raycasters。

If multiple Raycasters are used then they will all have casting happen against them and the results will be sorted based on distance to the elements.
當多個Raycaster被使用的時候,結果會按照元素之間的距離排序,然後事件就會按照這個順序被傳遞。

第一步

在相機上新增Physics2DRaycaster,我這裡只需要對Physics2D檢測,如果是3D就用Physics3DRaycaster。Physics Raycaster 依賴一個相機,如果沒有會自動新增。我掛載在相機上,射線檢測就會依賴這個相機。

這裡我用在GameCamera上面,當然也可以放在UICamera上面,Physics Raycaster掛載在哪個相機上面,射線就依賴這個相機的Culling Mask。

另外需要注意的是,Physics Raycaster所在的相機層級,也就是Depth,會影響到事件傳遞的順序。比如,UI Camera層級高於Game Camera,就會永遠先出發UI上的事件。同樣,OnMouse事件會預設依賴Main Camera的層級。

第二步

給需要碰撞檢測的GameObject,新增Collider和EventSystem的事件處理回撥介面。注意GameObject的Layer也要與Camera和Raycaster一致,才能正確被檢測到。

事件介面實現指令碼(圖中的Test)需要Collider,事件才能正確回撥,並且GameObject和相機的距離決定了Collider的層級,也就是事件阻擋關係。

第三步

這樣一來,EventSystem的SupportEvents的介面全部被應用到了Physics上面。也就不再需要自己手動去呼叫射線去檢測Physics碰撞了。那麼,還隱含著一個事情就是,EventSystem的IsPointerOverGameObject()就無法在判斷對UI的點選了。因為現在點選到Physics也會讓這個函式返回True。

EventSystem與OnMouse的區別

OnMouse 會先於 EventSystem 觸發。因為EventSystem的原始碼顯示,其在Update中去輪詢檢測處理Input的輸入。而OnMouse事件先於Update呼叫。

OnMouse指令碼需要在同一個GameObject上掛載Collider才能檢測。EventSystem的指令碼會根據子節點的Collider來觸發(平行節點不行)。

Rigidbody有個特點,會把子節點所有的Collider統一檢測和處理。也就是說,OnMouse指令碼與RigidBody在一起就可以檢測所有的子節點Collider,而不再需要同級的Collider。而EventSystem的指令碼則不依賴於Rigidbody,都可以檢測子節點的Collider。

OnMouse依賴於Tag為MainCamera相機的Culling Mask來過濾射線。EventSystem則是依賴掛載Physics Raycaster的相機。

另外,當在有Collider的子節點都掛載OnMouse或EventSystem事件的時候,只會觸發一次事件。但在同一個GameObject上掛載多個指令碼,就會觸發多次。

訊息輪詢 VS 訊息驅動

奇怪的是Unity好像比較推薦訊息輪詢的方式,就是在Update裡面每一幀去檢測Input的變化,來處理事件。從引擎的實現方式來看,完全可以採用訊息驅動,來暴露API。因為不同的平臺肯定都會提供,事件的回撥函式。平臺自身的事件有些是啟動執行緒輪詢的,有些是從底層作業系統拿到的事件回撥。當然,訊息驅動往往回撥函式會在獨立的執行緒裡,不在渲染執行緒就無法呼叫渲染的API。

不過Unity引擎完全可以提供一組事件的回撥,就像OnMouse事件一樣。但Input的設計就已經是基於輪詢的事件查詢機制了。我們可以看到在EventSystem的原始碼實現裡,也是在Update裡去輪詢Input Module的狀態。

protected virtual void Update()
{
    // ...
    TickModules();

    // ....
    if (!changedModule && m_CurrentInputModule != null)
        m_CurrentInputModule.Process();
}
輪詢需要每一幀都去檢測判斷Input的狀態,如果這樣的檢測散落在程式碼的各處是非常不好的。難道Unity的本意就是實現一個輪詢的外掛,在用訊息驅動去分發事件 ?於是EventSystem就出現了。

相關文章