UI,除了界面的顯示,還有一個重要的元素:事件響應。MoneBehaviour提供一些事件提供了一些函數接口(OnMouseUp,OnMouseDown等),只要MonBehaviour的子類實現這相應的方法以及方法執行的條件達到,Unity底層就會分發調用執行這個函數。一般地,UI事件響應處理機制會有4個基本元素:
1.event object:事件對象,即當前事件的類型,如鼠標左鍵按下等。
2.event source:事件源,或事件的觸發器,比如說,鼠標左鍵單擊點擊一個button,那麼button就是event source,鼠標左鍵單擊就是event source。
3.event handle:事件處理方法。
4.event listener:事件的監聽,比如上面說的鼠標左鍵點擊一個button,event listener就是監聽打button的一個mouse click事件,然後分發調用對應的event handle進行處理。
一般event source不會單獨存在,經常會跟event handle綁定在一起,其實就是指定了不同的event handle就是不同event source,如Button就有單擊雙擊事件,Input就有輸入焦點事件,event listener就好像人的大腦,監控這個所有的事件,同時作出不同的響應。
NGUI自己組織了一套UI事件響應處理機制, 不是對MonoBehaviour的方法的封裝調用,UICamera就是NGUI框架中的event listener,原理很簡單:在Update中捕獲鼠標,鍵盤等設備的輸入(也可以狹義的認爲UICamera就是event listener),判斷不同event object 和event source,然後“廣播”分發執行event handle,下面附上分發的函數:
- /// <summary>
- /// Generic notification function. Used in place of SendMessage to shorten the code and allow for more than one receiver.
- /// </summary>
- static public void Notify (GameObject go, string funcName, object obj)
- {
- if (go != null)
- {
- go.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
- if (genericEventHandler != null && genericEventHandler != go)
- {
- genericEventHandler.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
- }
- }
- }
知道的誰是event listener,那要怎麼實現event handle。實現NGUI的事件的方法有很多種,③給了三種方式監聽NGUI的事件方法。文末貼了NGUI支持的event handle。
增補於:11/10/2013 8:45,感覺之前的沒頭沒尾的,就加了上面的鋪墊
記得剛開始用NGUI的時候,就有心思要去琢磨下UICamera,那個時候NGUI還是2.6的版本,現在已經到了3.0.3f,NGUI更新真的很強勁, 當然改動也挺大的,特性也越來越多了。之前本來研究下UICamera最後還是放棄了,因爲UICamera的代碼太複雜了,很凌亂,也就放下去了,這幾天重新翻看了下,發現UICamera的可讀性太強了,代碼的組織邏輯很強,完全可以當做文本來從上到下來閱讀,所以纔會有這篇文章。
UICamera做了很多有優化,新增了一些特性,之前可能覺得NGUI只是做一個工具,現在越來越完美了,少廢話,下面把看到的亮點呈上。
ClickNotification
- /// <summary>
- /// Whether the touch event will be sending out the OnClick notification at the end.
- /// </summary>
- public enum ClickNotification
- {
- None,
- Always,
- BasedOnDelta,
- }
ClickNotification定義了OnClick響應的條件,後面也定義了ClickNotification變量 public ClickNotification clickNotification = ClickNotification.Always;
ClickNotification.None: 不響應OnClick事件
ClickNotification.Always:總是響應OnClick事件
ClickNotification.BaseOnDelta:依據移動的delta的距離判斷是否響應OnClick函數,如果移動距離大於float click = isMouse ? mouseClickThreshold : touchClickThreshold;則不響應OnClick事件
下面這部分代碼是當響應了OnDrag事件就把currentTouch.clickNotification = ClickNotification.None;就不在會響應OnClick事件了。
- bool isDisabled = (currentTouch.clickNotification == ClickNotification.None);
- Notify(currentTouch.dragged, "OnDrag", currentTouch.delta);
- isDragging = false;
- if (isDisabled)
- {
- // If the notification status has already been disabled, keep it as such
- currentTouch.clickNotification = ClickNotification.None;
- }
- else if (currentTouch.clickNotification == ClickNotification.BasedOnDelta && click < mag)
- {
- // We've dragged far enough to cancel the click
- currentTouch.clickNotification = ClickNotification.None;
- }
然後再執行OnClick和OnDoubleClick事件先判斷條件currentTouch.clickNotification != ClickNotification.None 是否成立:
- // If the touch should consider clicks, send out an OnClick notification
- if (currentTouch.clickNotification != ClickNotification.None)
- {
- float time = Time.realtimeSinceStartup;
- Notify(currentTouch.pressed, "OnClick", null);
- if (currentTouch.clickTime + 0.35f > time)
- {
- Notify(currentTouch.pressed, "OnDoubleClick", null);
- }
- currentTouch.clickTime = time;
- }
EventType
- public enum EventType
- {
- World, // Perform a Physics.Raycast and sort by distance to the point that was hit.
- UI, // Perform a Physics.Raycast and sort by widget depth.
- }
這個很簡單就是定義當前射線和碰撞體碰撞的判斷標準,如果是UI則以Depth來判斷,如果是World是以實際距離來判斷。
List<UICamera> list
- /// <summary>
- /// List of all active cameras in the scene.
- /// </summary>
- static public List<UICamera> list = new List<UICamera>();
UICamera在初始化的時候會被加入 mList這個鏈表中,然後對鏈表進行排序,根據相機的深度,深度值越小的相機排位靠前,最靠前的相機爲場景的主UICamera,然後只有只有主UICamera纔會去監測場景中的事件,其他的UICamera並不執行監測任務。UICamera利用Unity的Raycast去監測事件發生的對象,因爲發射出去的Ray對象必須碰撞到Collider纔會有反應,所以NGUI中所有需要響應事件的控件均需要添加Collider,同時Ray只會碰撞到深度最小的Collider,Ray射線的最大深度爲rangeDistance,當這個值爲-1時則發射深度和相機深度一樣,主UICamera每一幀都會主動去發射Ray檢測鼠標此時觸碰到的對象並將其記錄在對應的鼠標按鍵事件中,這是能監測到OnHover這個動作的關鍵(當然只有在useMouse爲true時纔會有此操作)。
在遊戲場景初始化階段,每個UICamera都會根據平臺義useMouse、useTouch、useKeyboard和useController 這些屬性,分別對應的是能否在場景使用鼠標、觸摸屏、鍵盤以及搖桿。
- public bool useMouse = true;
- public bool useTouch = true;
- public bool allowMultiTouch = true;
- public bool useKeyboard = true;
- public bool useController = true;
MouseOrTouch
- /// <summary>
- /// Ambiguous mouse, touch, or controller event.
- /// </summary>
- public class MouseOrTouch
- {
- public Vector2 pos; // Current position of the mouse or touch event
- public Vector2 delta; // Delta since last update
- public Vector2 totalDelta; // Delta since the event started being tracked
- public Camera pressedCam; // Camera that the OnPress(true) was fired with
- public GameObject current; // The current game object under the touch or mouse
- public GameObject pressed; // The last game object to receive OnPress
- public GameObject dragged; // The last game object to receive OnDrag
- public float clickTime = 0f; // The last time a click event was sent out
- public ClickNotification clickNotification = ClickNotification.Always;
- public bool touchBegan = true;
- public bool pressStarted = false;
- public bool dragStarted = false;
- }
MouseOrTouch是一個很重要的類,是一個事件的結構體,然後就定義了不同平臺的事件,記錄Camera監測的事件:MouseOrTouch只是記錄“鼠標”等的移動的“物理”信息——位置,移動距離等,只有鼠標是否按下只有在Update中每幀監測。
下面定義不同平臺的事件,例如鼠標事件,mMouse記錄鼠標左鍵,右鍵和中鍵的事件(因爲鼠標這裏只記錄鼠標的三個按鍵,所以mMouse纔是有三個元素,現在明白爲啥了吧)。
- // Mouse events
- static MouseOrTouch[] mMouse = new MouseOrTouch[] { new MouseOrTouch(), new MouseOrTouch(), new MouseOrTouch() };
- // The last object to receive OnHover
- static GameObject mHover;
- // Joystick/controller/keyboard event
- static MouseOrTouch mController = new MouseOrTouch();
- // Used to ensure that joystick-based controls don't trigger that often
- static float mNextEvent = 0f;
- // List of currently active touches
- static Dictionary<int, MouseOrTouch> mTouches = new Dictionary<int, MouseOrTouch>();
currentTouch
- /// <summary>
- /// ID of the touch or mouse operation prior to sending out the event. Mouse ID is '-1' for left, '-2' for right mouse button, '-3' for middle.
- /// </summary>
- static public int currentTouchID = -1;
- /// <summary>
- /// Current touch, set before any event function gets called.
- /// </summary>
- static public MouseOrTouch currentTouch = null;
currentTouch這個變量是整個UICamera中控制事件監測的關鍵所在,記錄了當前事件的觸發對象和一些其他諸如position位置、dealta時間、totaldealta總時間等屬性,然後用currentTouchID記錄當前事件的類型,這些類型包括鼠標事件、鍵盤控制器事件以及觸摸屏事件。
ProcessTouch
ProcessTouch這個函數就是根據currentTouch來針對不同的情況響應不同的函數,被ProcessMouse,ProcessTouch和ProcessOthers調用,如ProcessMouse,分別捕獲鼠標三個按鍵的狀態,然後調用ProcessTouch來響應:
- // Process all 3 mouse buttons as individual touches
- if (useMouse)
- {
- for (int i = 0; i < 3; ++i)
- {
- bool pressed = Input.GetMouseButtonDown(i);
- bool unpressed = Input.GetMouseButtonUp(i);
- currentTouch = mMouse[i];
- currentTouchID = -1 - i;
- // We don't want to update the last camera while there is a touch happening
- if (pressed) currentTouch.pressedCam = currentCamera;
- else if (currentTouch.pressed != null) currentCamera = currentTouch.pressedCam;
- // Process the mouse events
- ProcessTouch(pressed, unpressed);
- }
『ProcessMouse分析
因爲之前版本升級到NGUI3.0.6時,UICamera出現了一個Bug:當Time.ScaleTime != 1f 的時候,事件響應有問題,當時由於時間關係,只是和之前的版本進行比對,增加了些代碼解決的。但是還是感覺沒有能對UICamera具體細節沒能完全掌握,挺蹩腳的,還不能達到“自主”的處理目的,所以一直都想有時間好好把UICamera的事件分發流程細節清理下。
- /// <summary>
- /// Update mouse input.
- /// </summary>
- public void ProcessMouse ()
- {
- // No need to perform raycasts every frame
- if (mNextRaycast < RealTime.time) //更新鼠標current爲當前的 hoveredObject,如果時間間隔小於 20毫秒,就不更新
- {
- mNextRaycast = RealTime.time + 0.02f;
- if (!Raycast(Input.mousePosition, out lastHit)) hoveredObject = fallThrough;
- if (hoveredObject == null) hoveredObject = genericEventHandler;
- for (int i = 0; i < 3; ++i) mMouse[i].current = hoveredObject;
- }
- lastTouchPosition = Input.mousePosition;
- bool highlightChanged = (mMouse[0].last != mMouse[0].current);
- if (highlightChanged) currentScheme = ControlScheme.Mouse;
- // Update the position and delta 更新三個鼠標按鍵的位置 delta 和pos ,
- mMouse[0].delta = lastTouchPosition - mMouse[0].pos;
- mMouse[0].pos = lastTouchPosition;
- bool posChanged = mMouse[0].delta.sqrMagnitude > 0.001f;
- // Propagate the updates to the other mouse buttons
- for (int i = 1; i < 3; ++i)
- {
- mMouse[i].pos = mMouse[0].pos;
- mMouse[i].delta = mMouse[0].delta;
- }
- // Is any button currently pressed?
- bool isPressed = false;
- for (int i = 0; i < 3; ++i)
- {
- if (Input.GetMouseButton(i))
- {
- currentScheme = ControlScheme.Mouse;
- isPressed = true;
- break;
- }
- }
- if (isPressed)
- {
- // A button was pressed -- cancel the tooltip
- mTooltipTime = 0f;
- }
- else if (posChanged && (!stickyTooltip || highlightChanged)) //更新Tip顯示
- {
- if (mTooltipTime != 0f)
- {
- // Delay the tooltip
- mTooltipTime = RealTime.time + tooltipDelay;
- }
- else if (mTooltip != null)
- {
- // Hide the tooltip
- ShowTooltip(false);
- }
- }
- // The button was released over a different object -- remove the highlight from the previous
- if (!isPressed && mHover != null && highlightChanged) //更新hover GameObject ,並分發 OnHover事件
- {
- currentScheme = ControlScheme.Mouse;
- if (mTooltip != null) ShowTooltip(false);
- Notify(mHover, "OnHover", false);
- mHover = null;
- }
- // Process all 3 mouse buttons as individual touches 分別處理鼠標的三個按鍵,獲取按鍵狀態,進行事件分發
- for (int i = 0; i < 3; ++i)
- {
- bool pressed = Input.GetMouseButtonDown(i);
- bool unpressed = Input.GetMouseButtonUp(i);
- if (pressed || unpressed) currentScheme = ControlScheme.Mouse;
- currentTouch = mMouse[i];
- currentTouchID = -1 - i;
- currentKey = KeyCode.Mouse0 + i;
- // We don't want to update the last camera while there is a touch happening
- if (pressed) currentTouch.pressedCam = currentCamera;
- else if (currentTouch.pressed != null) currentCamera = currentTouch.pressedCam;
- // Process the mouse events
- ProcessTouch(pressed, unpressed);
- currentKey = KeyCode.None;
- }
- currentTouch = null;
- // If nothing is pressed and there is an object under the touch, highlight it
- if (!isPressed && highlightChanged)
- {
- currentScheme = ControlScheme.Mouse;
- mTooltipTime = RealTime.time + tooltipDelay;
- mHover = mMouse[0].current;
- Notify(mHover, "OnHover", true);
- }
- // Update the last value
- mMouse[0].last = mMouse[0].current;
- for (int i = 1; i < 3; ++i) mMouse[i].last = mMouse[0].last;
- }
這次回看UICamera的代碼,更加UICamera優化了很多,代碼邏輯清晰簡單了,之前的一直感覺很亂(一堆條件判斷)才一直沒有細看。雖然上面代碼還是有加點註釋,其實已經完全沒必要了。然後在NGUI3.0.7版本還增加了 在Editor下用鼠標做屏幕Touch的操作的功能:
/// Process fake touch events where the mouse acts as a touch device.
/// Useful for testing mobile functionality in the editor.』
增補於 2013,12,29 15:15
其他
UICamera還提供其他一些“特性”,能夠讓開發者實現更多的功能(就不解釋了吧, 有註釋):
- /// <summary>
- /// If 'true', once a press event is started on some object, that object will be the only one that will be
- /// receiving future events until the press event is finally released, regardless of where that happens.
- /// If 'false', the press event won't be locked to the original object, and other objects will be receiving
- /// OnPress(true) and OnPress(false) events as the touch enters and leaves their area.
- /// </summary>
- public bool stickyPress = true;
- /// <summary>
- /// If set, this game object will receive all events regardless of whether they were handled or not.
- /// </summary>
- static public GameObject genericEventHandler;
- /// <summary>
- /// If events don't get handled, they will be forwarded to this game object.
- /// </summary>
- static public GameObject fallThrough;
最後,NGUI一共支持一下事件:
- void OnHover (bool isOver) – Sent out when the mouse hovers over the collider or moves away from it. Not sent on touch-based devices.
- void OnPress (bool isDown) – Sent when a mouse button (or touch event) gets pressed over the collider (with ‘true’) and when it gets released (with ‘false’, sent to the same collider even if it’s released elsewhere).
- void OnClick() — Sent to a mouse button or touch event gets released on the same collider as OnPress. UICamera.currentTouchID tells you which button was clicked.
- void OnDoubleClick () — Sent when the click happens twice within a fourth of a second. UICamera.currentTouchID tells you which button was clicked.
- void OnSelect (bool selected) – Same as OnClick, but once a collider is selected it will not receive any further OnSelect events until you select some other collider.
- void OnDrag (Vector2 delta) – Sent when the mouse or touch is moving in between of OnPress(true) and OnPress(false).
- void OnDrop (GameObject drag) – Sent out to the collider under the mouse or touch when OnPress(false) is called over a different collider than triggered the OnPress(true) event. The passed parameter is the game object of the collider that received the OnPress(true) event.
- void OnInput (string text) – Sent to the same collider that received OnSelect(true) message after typing something. You likely won’t need this, but it’s used by UIInput
- void OnTooltip (bool show) – Sent after the mouse hovers over a collider without moving for longer than tooltipDelay, and when the tooltip should be hidden. Not sent on touch-based devices.
- void OnScroll (float delta) is sent out when the mouse scroll wheel is moved.
- void OnKey (KeyCode key) is sent when keyboard or controller input is used.
小結:
最近由於項目要用到FastGUI,然後手上的FastGUI不支持NGUI(NGUI變動太大了),然後自己要升級下FastGUI,就要更多的掌握NGUI的原理,所以纔會一直不斷的寫一些文章。寫文章主要是記錄下自己從中看到的東西,當然D.S.Qiu最喜歡和大家分享,希望能對讀者有幫助,哪怕只有一個人,D.S.Qiu也會很興奮的,因爲很多次D.S.Qiu都不打算寫的(文章寫的太爛,沒有深度,邏輯差,每次都要熬夜等),但當我看到別人文章的亮點時,我就覺得自己還是可以分享些的。
今天把FastGUI 兼容到了NGUI3.0.3f,還增加一些功能,然後要寫一個文檔給美術的同事,我感覺頭就大了,感覺如果要我口述一定能讓聽者完全明白,但是寫起來就完全不着調,所以覺得D.S.Qiu的文字很渣,馬上就是凌晨1:30,睡覺,晚安!
如果您對D.S.Qiu有任何建議或意見可以在文章後面評論,或者發郵件([email protected])交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在文首註明出處:http://dsqiu.iteye.com/blog/1971866
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
參考:
①2B青年: http://blog.sina.com.cn/s/blog_6f16aba701017mgz.html
②tasharen: http://www.tasharen.com/?page_id=160