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。
记得刚开始用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.』
其他
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])交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。