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有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在文首注明出处:http://dsqiu.iteye.com/blog/1971866
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)