关闭

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

标签: 架构
1485人阅读 评论(0) 收藏 举报
分类:

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等。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    最新评论