您的位置:首页 > 产品设计 > UI/UE

NGUI的事件通知架构和源码剖析

2015-07-10 09:45 429 查看
NGUI的事件通知其实是由一个脚本UICamera来实现的,脚本的命名不是太好,其基本的原理很简单,在Update函数中检测用户输入,然后根据自己的策略分发到具体的物体。其定义了一些基本的通知回调函数,你可以查看具体的注释:

/// * OnHover (isOver) is sent when the mouse hovers over a collider or moves away.
/// * OnPress (isDown) is sent when a mouse button gets pressed on the collider.
/// * OnSelect (selected) is sent when a mouse button is first pressed on an object. Repeated presses won't result in an OnSelect(true).
/// * OnClick () is sent when a mouse is pressed and released on the same object.
///   UICamera.currentTouchID tells you which button was clicked.
/// * OnDoubleClick () is sent when the click happens twice within a fourth of a second.
///   UICamera.currentTouchID tells you which button was clicked.
///
/// * OnDragStart () is sent to a game object under the touch just before the OnDrag() notifications begin.
/// * OnDrag (delta) is sent to an object that's being dragged.
/// * OnDragOver (draggedObject) is sent to a game object when another object is dragged over its area.
/// * OnDragOut (draggedObject) is sent to a game object when another object is dragged out of its area.
/// * OnDragEnd () is sent to a dragged object when the drag event finishes.
///
/// * OnTooltip (show) is sent when the mouse hovers over a collider for some time without moving.
/// * OnScroll (float delta) is sent out when the mouse scroll wheel is moved.
/// * OnKey (KeyCode key) is sent when keyboard or controller input is used.


所以从字面上你就可以理解, 其提供了哪些事件通知,这些事件都是在在主线程中完成的。需要特别说明的是,NGUI有自己的事件通知,和MonoBehavior里面的函数OnMouseDown, OnMouseUp, OnMouseOver等消息处理函数重叠,所以只要我们使用了NGUI的处理框架以及NGUI的脚本,如UIButton,UISCrollView等,我们无需重载MonoBehavior的上述事件处理函数。 如果自己处理了,可能同一个用户输入会响应两次。

下面就简单介绍一下关键的函数或者结构体

通知函数, Notify

/// <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 (mNotifying) return;
mNotifying = true;

if (NGUITools.GetActive(go))
{
// 基本的Unity的GameObject函数.
go.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);

if (mGenericHandler != null && mGenericHandler != go)
{
mGenericHandler.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
}
}
mNotifying = false;
}


所以,消息通知是通过GameObject::SendMessage ()的方法来实现的,可以查看源码,等到funcName的名字都是上面的事件通知的名字,如: OnClick, OnHover, OnSelect等。

Raycast 帮助函数,如何从一个屏幕上的位置信息,找到点击,触摸,滑过的物体

/// <summary>
/// Returns the object under the specified position.
/// </summary>
static public bool Raycast (Vector3 inPos)
{
for (int i = 0; i < list.size; ++i)
{
// 当前的UI Camera
UICamera cam = list.buffer[i];

// Skip inactive scripts
if (!cam.enabled || !NGUITools.GetActive(cam.gameObject)) continue;

// Convert to view space
currentCamera = cam.cachedCamera;
// 将屏幕的位置信息,转换成ViewPort的信息,viewport的空间在0, 1范围内
Vector3 pos = currentCamera.ScreenToViewportPoint(inPos);
if (float.IsNaN(pos.x) || float.IsNaN(pos.y)) continue;

// If it's outside the camera's viewport, do nothing
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) continue;

// Cast a ray into the screen
Ray ray = currentCamera.ScreenPointToRay(inPos);

// Raycast into the screen
int mask = currentCamera.cullingMask & (int)cam.eventReceiverMask;
float dist = (cam.rangeDistance > 0f) ? cam.rangeDistance : currentCamera.farClipPlane - currentCamera.nearClipPlane;

if (cam.eventType == EventType.World_3D)
{
............
}
else if (cam.eventType == EventType.UI_3D)
{
// 获取当前的所有的RaycastHit的列表,所以在此,需要特别说明的是,任何的NGUI的控件都需要加上BoxColider物体。 disk和camera的距离,mask和层
RaycastHit[] hits = Physics.RaycastAll(ray, dist, mask);

if (hits.Length > 1)
{
// 下面所有的代码获取了点击到的物体的列表, 放入到全局的静态变量mhits中
// 基本的结构单元为:
//  struct DepthEntry
//{
//  public int depth; // Gameobject 的深度信息
//  public RaycastHit hit; // RaycastHit信息,可以查看具体的unity文档
//  public Vector3 point; // hit物体的世界空间的地址
//  public GameObject go; // hit 物体
//}

for (int b = 0; b < hits.Length; ++b)
{
GameObject go = hits[b].collider.gameObject;
UIWidget w = go.GetComponent<UIWidget>();

if (w != null)
{
if (!w.isVisible) continue;
if (w.hitCheck != null && !w.hitCheck(hits[b].point)) continue;
}
else
{
UIRect rect = NGUITools.FindInParents<UIRect>(go);
if (rect != null && rect.finalAlpha < 0.001f) continue;
}

mHit.depth = NGUITools.CalculateRaycastDepth(go);

if (mHit.depth != int.MaxValue)
{
mHit.hit = hits[b];
mHit.point = hits[b].point;
mHit.go = hits[b].collider.gameObject;
mHits.Add(mHit);
}
}

// 按照深度升序排序,
mHits.Sort(delegate(DepthEntry r1, DepthEntry r2) { return r2.depth.CompareTo(r1.depth); });
// 找到depth最高的可视的物体为当前的接触的物体
for (int b = 0; b < mHits.size; ++b)
{
#if UNITY_FLASH
if (IsVisible(mHits.buffer[b]))
#else
if (IsVisible(ref mHits.buffer[b]))
#endif
{
lastHit = mHits[b].hit;
hoveredObject = mHits[b].go;
lastWorldPosition = mHits[b].point;
mHits.Clear();
return true;
}
}
mHits.Clear();
}
else if (hits.Length == 1)
{
............
}
}
else if (cam.eventType == EventType.World_2D)
{
..............
}
else if (cam.eventType == EventType.UI_2D)
{
..............
}
}
return false;
}


ControlScheme 记录当前的输入的类型

public enum ControlScheme
{
Mouse,  // 鼠标事件
Touch,  // 触摸事件
Controller, // 控制器输入
}


MouseOrTouch 下面是记录鼠标事件和触摸事件的结构体,个人认为实现的不大合理,把鼠标和触摸需要的信息混合在一起了, NGUI在实现中也把touch,mouse事件合并进行了处理。

/// <summary>
/// Ambiguous mouse, touch, or controller event.
/// </summary>

public class MouseOrTouch
{
public Vector2 pos;             // Current position of the mouse or touch event
public Vector2 lastPos;         // Previous 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 last;         // Last object under the touch or mouse
public GameObject current;      // Current game object under the touch or mouse
public GameObject pressed;      // Last game object to receive OnPress
public GameObject dragged;      // Game object that's being dragged

public float pressTime = 0f;    // When the touch event started
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;

/// <summary>
/// Delta time since the touch operation started.
/// </summary>

public float deltaTime { get { return touchBegan ? RealTime.time - pressTime : 0f; } }

/// <summary>
/// Returns whether this touch is currently over a UI element.
/// </summary>
public bool isOverUI
{
get
{
return current != null && current != fallThrough && NGUITools.FindInParents<UIRoot>(current) != null;
}
}
}


处理输入的事件

// 处理鼠标按下或者触摸按下
void ProcessPress (bool pressed, float click, float drag)
{
// Send out the press message
if (pressed)
{
if (mTooltip != null) ShowTooltip(false);

currentTouch.pressStarted = true;
// 全局的OnPress的事件通知,前面的物体被释放
if (onPress != null && currentTouch.pressed)
onPress(currentTouch.pressed, false);

// OnPress 的事件通知, false,前面的物体
Notify(currentTouch.pressed, "OnPress", false);

currentTouch.pressed = currentTouch.current;
currentTouch.dragged = currentTouch.current;
currentTouch.clickNotification = ClickNotification.BasedOnDelta;
currentTouch.totalDelta = Vector2.zero;
currentTouch.dragStarted = false;

// 全局的OnPress的事件通知,当前的物体被按下
if (onPress != null && currentTouch.pressed)
onPress(currentTouch.pressed, true);

// OnPress 的事件通知, true,当前的物体
Notify(currentTouch.pressed, "OnPress", true);

// Update the selection
if (currentTouch.pressed != mCurrentSelection)
{
if (mTooltip != null) ShowTooltip(false);
currentScheme = ControlScheme.Touch;
selectedObject = currentTouch.pressed;
}
}
// 此处就是处理Drag的各项事务, 注意条件
else if (currentTouch.pressed != null && (currentTouch.delta.sqrMagnitude != 0f || currentTouch.current != currentTouch.last))
{
// Keep track of the total movement
currentTouch.totalDelta += currentTouch.delta;
float mag = currentTouch.totalDelta.sqrMagnitude;
bool justStarted = false;

// If the drag process hasn't started yet but we've already moved off the object, start it immediately
if (!currentTouch.dragStarted && currentTouch.last != currentTouch.current)
{
currentTouch.dragStarted = true;
currentTouch.delta = currentTouch.totalDelta;

// OnDragOver is sent for consistency, so that OnDragOut is always preceded by OnDragOver
isDragging = true;
// 通知OnDragStart 事件到全局函数和现在的物体
if (onDragStart != null) onDragStart(currentTouch.dragged);
Notify(currentTouch.dragged, "OnDragStart", null);

// 通知OnDragOver 事件到全局函数和上次Drag的物体
if (onDragOver != null) onDragOver(currentTouch.last, currentTouch.dragged);
Notify(currentTouch.last, "OnDragOver", currentTouch.dragged);

isDragging = false;
}
else if (!currentTouch.dragStarted && drag < mag)
{
// If the drag event has not yet started, see if we've dragged the touch far enough to start it
justStarted = true;
currentTouch.dragStarted = true;
currentTouch.delta = currentTouch.totalDelta;
}

// If we're dragging the touch, send out drag events
// 判断DragStarted 发送相关的事件,如
if (currentTouch.dragStarted)
{
if (mTooltip != null) ShowTooltip(false);

isDragging = true;
bool isDisabled = (currentTouch.clickNotification == ClickNotification.None);

if (justStarted)
{
if (onDragStart != null) onDragStart(currentTouch.dragged);
Notify(currentTouch.dragged, "OnDragStart", null);

if (onDragOver != null) onDragOver(currentTouch.last, currentTouch.dragged);
Notify(currentTouch.current, "OnDragOver", currentTouch.dragged);
}
else if (currentTouch.last != currentTouch.current)
{
if (onDragStart != null) onDragStart(currentTouch.dragged);
Notify(currentTouch.last, "OnDragOut", currentTouch.dragged);

if (onDragOver != null) onDragOver(currentTouch.last, currentTouch.dragged);
Notify(currentTouch.current, "OnDragOver", currentTouch.dragged);
}

if (onDrag != null) onDrag(currentTouch.dragged, currentTouch.delta);
Notify(currentTouch.dragged, "OnDrag", currentTouch.delta);

currentTouch.last = currentTouch.current;
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;
}
}
}
}


/// <summary>
/// 处理Touch,和Mouse release事件。
/// </summary>
void ProcessRelease (bool isMouse, float drag)
{
// Send out the unpress message
currentTouch.pressStarted = false;
if (mTooltip != null) ShowTooltip(false);

if (currentTouch.pressed != null)
{
// If there was a drag event in progress, make sure OnDragOut gets sent
if (currentTouch.dragStarted)
{
if (onDragOut != null) onDragOut(currentTouch.last, currentTouch.dragged);
Notify(currentTouch.last, "OnDragOut", currentTouch.dragged);

if (onDragEnd != null) onDragEnd(currentTouch.dragged);
Notify(currentTouch.dragged, "OnDragEnd", null);
}

// Send the notification of a touch ending
if (onPress != null) onPress(currentTouch.pressed, false);
Notify(currentTouch.pressed, "OnPress", false);

// Send a hover message to the object
if (isMouse)
{
if (onHover != null) onHover(currentTouch.current, true);
Notify(currentTouch.current, "OnHover", true);
}
mHover = currentTouch.current;

// If the button/touch was released on the same object, consider it a click and select it
if (currentTouch.dragged == currentTouch.current ||
(currentScheme != ControlScheme.Controller &&
currentTouch.clickNotification != ClickNotification.None &&
currentTouch.totalDelta.sqrMagnitude < drag))
{
if (currentTouch.pressed != mCurrentSelection)
{
mNextSelection = null;
mCurrentSelection = currentTouch.pressed;
if (onSelect != null) onSelect(currentTouch.pressed, true);
Notify(currentTouch.pressed, "OnSelect", true);
}
else
{
mNextSelection = null;
mCurrentSelection = currentTouch.pressed;
}

// If the touch should consider clicks, send out an OnClick notification
if (currentTouch.clickNotification != ClickNotification.None && currentTouch.pressed == currentTouch.current)
{
float time = RealTime.time;

if (onClick != null) onClick(currentTouch.pressed);
Notify(currentTouch.pressed, "OnClick", null);

if (currentTouch.clickTime + 0.35f > time)
{
if (onDoubleClick != null) onDoubleClick(currentTouch.pressed);
Notify(currentTouch.pressed, "OnDoubleClick", null);
}
currentTouch.clickTime = time;
}
}
else if (currentTouch.dragStarted) // The button/touch was released on a different object
{
// Send a drop notification (for drag & drop)
if (onDrop != null) onDrop(currentTouch.current, currentTouch.dragged);
Notify(currentTouch.current, "OnDrop", currentTouch.dragged);
}
}
currentTouch.dragStarted = false;
currentTouch.pressed = null;
currentTouch.dragged = null;
}


Update 函数,其处理UI 事件,并且发送给具体的物体和回调函数。

void Update ()
{
// Only the first UI layer should be processing events
#if UNITY_EDITOR
if (!Application.isPlaying || !handlesEvents) return;
#else
if (!handlesEvents) return;
#endif
current = this;

// Process touch events first
// 处理触摸和鼠标事件,可以review源代码,都是复用了ProcessPress和ProcessRelease
// 函数
if (useTouch) ProcessTouches ();
else if (useMouse) ProcessMouse();
..................
}


总结: 所有整个NGUI的事件处理通知都是由UICamera的update函数,分析当前帧的用户输入,然后发送时间给NGUI的UI控件,如UIButton,UIScrollView等。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  架构