1.前言
在上一篇中梳理了一下整個事件系統的流程,包括Workflow,此文則詳細講解一下InputModule本身,並對一些方法做一下解釋。
2.綜述
InputModule的結構如下所示:
BaseInputModule為原始基類,只包含最基本的功能。PointerInputModule主要功能是獲取當前touch或者鼠標位置坐標、狀態以及對應游戲物體信息。最終Standalone/TouchInputModule則進行事件判斷與處理。當然頁包括一些VR sdk中自定義的InputModule。
2.1 BaseInputModule
此類包含基本的功能包括模塊啟動管理、獲取當前input模塊、處理Enter/Exit以及其他輔助功能:
1)啟動管理
即在OnEnable和OnDisable方法中處理的事件。即將此模塊添加到添加到EventSystem的inputModule列表中。
2)獲取當前Input模塊
此input模塊是指最基本的輸入模塊,基本上就是unity最基本的Input類的簡單封裝。用戶可以自定義Input類,但是目前基本沒有此類需求。
3)處理Enter/Exit
此功能對應代碼如下所示,是比較重要的一個方法,但是也是最讓人費解的方法。即使有注釋也比較費解為什么要這么處理。如果只是處理Enter和Exit方法,則此段代碼從 “if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget) return;”開始即可,后來從此方法的使用知道,最開始的一段代碼是為了一些特定功能添加的。
// walk up the tree till a common root between the last entered and the current entered is foung // send exit events up to (but not inluding) the common root. Then send enter events up to // (but not including the common root). protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget) { // if we have no target / pointerEnter has been deleted // just send exit events to anything we are tracking // then exit if (newEnterTarget == null || currentPointerData.pointerEnter == null) { for (var i = 0; i < currentPointerData.hovered.Count; ++i) ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler); currentPointerData.hovered.Clear(); if (newEnterTarget == null) { currentPointerData.pointerEnter = null; return; } } // if we have not changed hover target if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget) return; GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget); // and we already an entered object from last time if (currentPointerData.pointerEnter != null) { // send exit handler call to all elements in the chain // until we reach the new target, or null! Transform t = currentPointerData.pointerEnter.transform; while (t != null) { // if we reach the common root break out! if (commonRoot != null && commonRoot.transform == t) break; ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler); currentPointerData.hovered.Remove(t.gameObject); t = t.parent; } } // now issue the enter call up to but not including the common root currentPointerData.pointerEnter = newEnterTarget; if (newEnterTarget != null) { Transform t = newEnterTarget.transform; while (t != null && t.gameObject != commonRoot) { ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler); currentPointerData.hovered.Add(t.gameObject); t = t.parent; } } }
4)其他
其他均為輔助方法,但是有個方法可能以后會用到,即尋找兩個游戲物體的公共節點,如下
protected static GameObject FindCommonRoot(GameObject g1, GameObject g2) { if (g1 == null || g2 == null) return null; var t1 = g1.transform; while (t1 != null) { var t2 = g2.transform; while (t2 != null) { if (t1 == t2) return t1.gameObject; t2 = t2.parent; } t1 = t1.parent; } return null; }
2.2 PointerInputModule
此類繼承BaseInputModule模塊,作用很簡單即獲取當前touch或者鼠標的點信息,但是處理確立邏輯卻比較繁瑣,這是因為涉及到鼠標事件的左中右三個按鍵問題。此模塊通過維護m_PointerData列表來處理各種功能。如下所示。
2.2.1 PointerData字典
通過一個問題來解釋這個字典的作用。如何判斷鼠標滑動時的delta?解決此問題需要把上一幀的鼠標位置記錄下來,然后用當前幀鼠標的坐標去減上一幀的值。PointerData字典就是此作用,將當前幀Pointer信息記錄下來,只是此字典記錄的不止一個點。包括鼠標左右中三個按鍵(對應key值為-1,-2,-3)、虛擬按鍵值(key為-4)以及touch(key為touch的touchID)。
2.2.2 獲取Touch事件PointerData
TouchPointerData的獲取比較簡單。即獲取到當前touch數據,針對不同手指不同id去處理,但是大部分只有一個手指,獲取當前點擊狀態。然后射線檢測,獲取當前檢測的游戲物體。
代碼如下:
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released) { PointerEventData pointerData; var created = GetPointerData(input.fingerId, out pointerData, true); pointerData.Reset(); pressed = created || (input.phase == TouchPhase.Began); released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended); if (created) pointerData.position = input.position; if (pressed) pointerData.delta = Vector2.zero; else pointerData.delta = input.position - pointerData.position; pointerData.position = input.position; pointerData.button = PointerEventData.InputButton.Left; if (input.phase == TouchPhase.Canceled) { pointerData.pointerCurrentRaycast = new RaycastResult(); } else { eventSystem.RaycastAll(pointerData, m_RaycastResultCache); var raycast = FindFirstRaycast(m_RaycastResultCache); pointerData.pointerCurrentRaycast = raycast; m_RaycastResultCache.Clear(); } return pointerData; }
2.2.3 獲取Mouse事件的數據
此部分比較麻煩,因為鼠標事件包含左右中三個,但是他們的位置以及射線檢測到的游戲物體是相同的,唯一不同的是點擊狀態不同。所以針對這些問題定義了三個類:
1)MouseButtonEventData
此類只是在PointerData的基礎上進行封裝,增加了點擊狀態處理,即是否進行了點擊。鼠標左右中三個按鍵都是由MouseButtonEventData表現。
2)ButtonState
此類是在MouseButtonEventData基礎上的進一步封裝。添加了按鍵位置參數,即表示此點擊位置是左右中哪個按鍵進行了點擊。個人認為可以完全跟上一個類合並。
3)MouseState
此類是在ButtonState的基礎上繼續進行的封裝。通過m_TrackedButtons列表來維護三個按鍵的信息,所以列表中中最多只有三個元素,且除了剛開始運行時,有且只有三個元素。
所以獲取mouse事件的數據時,返回值是一個MouseState,流程與Touch相同,只是增加了Copy數據的工程。因為左中右三個按鍵的位置與對應的游戲物體相同。
2.2.4 其他方法
還定義了其他一些方法,比如processMove、ProcessDrag等方法,其實是比較簡單的。但是需要注意的一點是PointerEventData存儲的數據包含上一幀的信息,所以處理ProcessMove時,只需要傳入PointerEventData就可以。等處理結束后,下一幀未開始的時候一些數據才會統一,比如PointerEnter才會與當前射線檢測的物體統一(此時不一定相同,因為接收PointerEnter的游戲物體有可能是當前游戲物體的父物體)。
2.3 StandaloneInputModule
此類才是真正去處理事件的類,是由Process處理的,首先處理Update/Move/Submit事件,然后處理ProcessTouchEvent和ProcessMouseEvent事件,而他們中的核心方法是ProcessTouchPress和ProcessMousePress。
2.3.1 Update/Move/Submit事件
這些事件比較處理比較簡單,只是對當前選擇是游戲物體,發送update、move以及submit事件。這些事件是pc端的事件,對應一些特殊的按鍵,比如Enter、esc以及ased字母鍵。
2.3.2 常規事件處理
常規事件是指常用到的事件比如drag、pointerEnter/exit以及click等事件,移動端和pc端分別是由ProcessTouchEvent和ProcessMouseEvent分別處理。但是不管是Touch還是Mouse,都是按照Press、Move和Drag進行處理。這里的Move與2.3.1中的move事件不同。此處的Move是處理PointerEnter/Exit事件,同理Press處理PointerDown、PointerClick以及PointerUp事件。
ProcessTouchEvent
此方法處理Touch事件,由於移動端不止一個手指觸摸,所以所有的手指都要處理。思路是這樣的:
(一)首先,如果在當前幀Pressed即手機觸碰到觸摸板,則處理PointerDown事件,同時記錄當前游戲物體可能的Drag對象。
(二)如果當前幀沒有Released,則根據上述的結果去處理Move和Drag(如果拖動距離大於EventSystem規定的距離才去執行)。Move處理的是PointerEnter/Exit事件,所以移動端Touch和pc端Mouse處理方式有所不同。
(三)如果當前幀Released(即手離開屏幕)則處理PointerUp事件(如果之前Pressed狀態時如果沒有處理PointerDown事件,且無click事件,則不會處理PointerUp事件),同時處理EndDrag和Drop事件(如果條件允許)。
此處存在一個問題,即不會單獨處理PointerUp事件。不管是Mouse還是Touch,如果一個游戲物體既沒有down事件也沒有click事件,是不會觸發PointerUp事件的。
ProcessMouseEvent
由於mouse事件不存在鼠標離開的問題,所以會一直處理ProcessMove。而且由於鼠標有左右中三個按鍵,所以針對Press和Drag都會進行判斷。Press的處理流程與Touch基本相同(如上文所示)。區別則有兩點,一個是touch按下時會判斷是否處理Enter/Exit事件;另一個則是當鼠標Release時,mouse會判斷是否處理Enter/Exit事件。
3.結語
以上是InputModule模塊整個的事件處理流程。