UGUI的點擊事件機制

UGUI的點擊事件機制

0x01點擊的出發點

反編譯EventSystem大概獲得一個這樣的流程圖。爲UGUI中各種點擊及拖動響應事件產生的一個大概的流程圖。其中黃色部分爲重點的分析區域。

這裏寫圖片描述

反編譯 PointerInputModule獲得實現,其中

 protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
    {
      PointerEventData data;
      bool pointerData = this.GetPointerData(input.fingerId, out data, true);
      data.Reset();
      pressed = pointerData || input.phase == TouchPhase.Began;
      released = input.phase == TouchPhase.Canceled || input.phase == TouchPhase.Ended;
      if (pointerData)
        data.position = input.position;
      data.delta = !pressed ? input.position - data.position : Vector2.zero;
      data.position = input.position;
      data.button = PointerEventData.InputButton.Left;
      this.eventSystem.RaycastAll(data, this.m_RaycastResultCache);
      RaycastResult firstRaycast = BaseInputModule.FindFirstRaycast(this.m_RaycastResultCache);
      data.pointerCurrentRaycast = firstRaycast;
      this.m_RaycastResultCache.Clear();
      return data;
    }

這個方法的調用主要是在ProcessTouchEvents中,對每個touch點進行處理。這裏是由Eventsystem的Update調用到Process然後調過來的。是每一幀輪詢的。

這裏面功能的主要實現是這幾句

this.eventSystem.RaycastAll(data, this.m_RaycastResultCache);
      RaycastResult firstRaycast = BaseInputModule.FindFirstRaycast(this.m_RaycastResultCache);
      data.pointerCurrentRaycast = firstRaycast;

先用this.eventSystem.RaycastAll獲得一個結果隊列,然後拿到首先響應的對象。

反編譯BaseInputModule

    protected static RaycastResult FindFirstRaycast(List<RaycastResult> candidates)
    {
      for (int index = 0; index < candidates.Count; ++index)
      {
        if (!((UnityEngine.Object) candidates[index].gameObject == (UnityEngine.Object) null))
          return candidates[index];
      }
      return new RaycastResult();
    }

這裏其實就是拿第一個出來。

0x02點擊觸發隊列的生成

EventSystem中的實現

public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
    {
      raycastResults.Clear();
      List<BaseRaycaster> raycasters = RaycasterManager.GetRaycasters();
      for (int index = 0; index < raycasters.Count; ++index)
      {
        BaseRaycaster baseRaycaster = raycasters[index];
        if (!((UnityEngine.Object) baseRaycaster == (UnityEngine.Object) null) && baseRaycaster.IsActive())
          baseRaycaster.Raycast(eventData, raycastResults);
      }
      raycastResults.Sort(EventSystem.s_RaycastComparer);
    }

private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
    {
      if ((UnityEngine.Object) lhs.module != (UnityEngine.Object) rhs.module)
      {
        if ((UnityEngine.Object) lhs.module.eventCamera != (UnityEngine.Object) null && (UnityEngine.Object) rhs.module.eventCamera != (UnityEngine.Object) null && (double) lhs.module.eventCamera.depth != (double) rhs.module.eventCamera.depth)
        {
          if ((double) lhs.module.eventCamera.depth < (double) rhs.module.eventCamera.depth)
            return 1;
          return (double) lhs.module.eventCamera.depth == (double) rhs.module.eventCamera.depth ? 0 : -1;
        }
        if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
          return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);
        if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
          return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
      }
      if (lhs.sortingLayer != rhs.sortingLayer)
        return SortingLayer.GetLayerValueFromID(rhs.sortingLayer).CompareTo(SortingLayer.GetLayerValueFromID(lhs.sortingLayer));
      if (lhs.sortingOrder != rhs.sortingOrder)
        return rhs.sortingOrder.CompareTo(lhs.sortingOrder);
      if (lhs.depth != rhs.depth)
        return rhs.depth.CompareTo(lhs.depth);
      if ((double) lhs.distance != (double) rhs.distance)
        return lhs.distance.CompareTo(rhs.distance);
      return lhs.index.CompareTo(rhs.index);
    }    

也就是說我去Raycast所有對象的過程,其實是從RaycasterManager中獲得註冊過的Raycaster並且逐個進行判斷是否可以被點到。整個過程是一個循環搜索,並不考慮遮擋關係等,生成一組觸控結果,然後根據相機的depth,點擊的sortingLayer,sortingOrder,depth,distance 等對碰撞結果信息進行排序。排序後的碰撞結果就已經有了優先順序,比如A控件遮擋B控件的操作,那麼結果隊列裏就是先A後B。

你會說,我靠這麼多控件的結果排序麼。但是彆着急,這裏的頂層結果排序並不是對所有控件排序,至於爲什麼,繼續往後看。

反編譯其中的BaseRaycaster

public abstract void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList);

發現其中的Raycast是一個虛方法,並無實現。而真正上用的其實是GraphicRaycast。創建一個Canvas,看看是不是自動生成了它!沒錯,這個東西就是跟畫布息息相關。結合上面提到的RaycastAll中的排序,其實是每一個畫布下都會產生的點擊結果隊列,依次被添加到了這個總的list上然後排序。

反編譯GraphicRaycast,可以看到實現。代碼很長淡定的一行行看下去。

public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
      if ((UnityEngine.Object) this.canvas == (UnityEngine.Object) null)
        return;
      Vector3 position = Display.RelativeMouseAt((Vector3) eventData.position);
      int targetDisplay = this.canvas.targetDisplay;
      if ((double) position.z != (double) targetDisplay)
        return;
      if ((double) position.z == 0.0)
        position = (Vector3) eventData.position;
      Vector2 vector2;
      if ((UnityEngine.Object) this.eventCamera == (UnityEngine.Object) null)
      {
        float num1 = (float) Screen.width;
        float num2 = (float) Screen.height;
        if (targetDisplay > 0 && targetDisplay < Display.displays.Length)
        {
          num1 = (float) Display.displays[targetDisplay].systemWidth;
          num2 = (float) Display.displays[targetDisplay].systemHeight;
        }
        vector2 = new Vector2(position.x / num1, position.y / num2);
      }
      else
        vector2 = (Vector2) this.eventCamera.ScreenToViewportPoint(position);
      if ((double) vector2.x < 0.0 || (double) vector2.x > 1.0 || ((double) vector2.y < 0.0 || (double) vector2.y > 1.0))
        return;
      float num3 = float.MaxValue;
      Ray r = new Ray();
      if ((UnityEngine.Object) this.eventCamera != (UnityEngine.Object) null)
        r = this.eventCamera.ScreenPointToRay(position);
      if (this.canvas.renderMode != RenderMode.ScreenSpaceOverlay && this.blockingObjects != GraphicRaycaster.BlockingObjects.None)
      {
        float f = 100f;
        if ((UnityEngine.Object) this.eventCamera != (UnityEngine.Object) null)
          f = this.eventCamera.farClipPlane - this.eventCamera.nearClipPlane;
        RaycastHit hit;
        if ((this.blockingObjects == GraphicRaycaster.BlockingObjects.ThreeD || this.blockingObjects == GraphicRaycaster.BlockingObjects.All) && (ReflectionMethodsCache.Singleton.raycast3D != null && ReflectionMethodsCache.Singleton.raycast3D(r, out hit, f, (int) this.m_BlockingMask)))
          num3 = hit.distance;
        if ((this.blockingObjects == GraphicRaycaster.BlockingObjects.TwoD || this.blockingObjects == GraphicRaycaster.BlockingObjects.All) && ReflectionMethodsCache.Singleton.raycast2D != null)
        {
          RaycastHit2D raycastHit2D = ReflectionMethodsCache.Singleton.raycast2D((Vector2) r.origin, (Vector2) r.direction, f, (int) this.m_BlockingMask);
          if ((bool) ((UnityEngine.Object) raycastHit2D.collider))
            num3 = raycastHit2D.fraction * f;
        }
      }
      this.m_RaycastResults.Clear();
      GraphicRaycaster.Raycast(this.canvas, this.eventCamera, (Vector2) position, this.m_RaycastResults);
      for (int index = 0; index < this.m_RaycastResults.Count; ++index)
      {
        GameObject gameObject = this.m_RaycastResults[index].gameObject;
        bool flag = true;
        if (this.ignoreReversedGraphics)
          flag = !((UnityEngine.Object) this.eventCamera == (UnityEngine.Object) null) ? (double) Vector3.Dot(this.eventCamera.transform.rotation * Vector3.forward, gameObject.transform.rotation * Vector3.forward) > 0.0 : (double) Vector3.Dot(Vector3.forward, gameObject.transform.rotation * Vector3.forward) > 0.0;
        if (flag)
        {
          float num1;
          if ((UnityEngine.Object) this.eventCamera == (UnityEngine.Object) null || this.canvas.renderMode == RenderMode.ScreenSpaceOverlay)
          {
            num1 = 0.0f;
          }
          else
          {
            Transform transform = gameObject.transform;
            Vector3 forward = transform.forward;
            num1 = Vector3.Dot(forward, transform.position - r.origin) / Vector3.Dot(forward, r.direction);
            if ((double) num1 < 0.0)
              continue;
          }
          if ((double) num1 < (double) num3)
          {
            RaycastResult raycastResult = new RaycastResult()
            {
              gameObject = gameObject,
              module = (BaseRaycaster) this,
              distance = num1,
              screenPosition = (Vector2) position,
              index = (float) resultAppendList.Count,
              depth = this.m_RaycastResults[index].depth,
              sortingLayer = this.canvas.sortingLayerID,
              sortingOrder = this.canvas.sortingOrder
            };
            resultAppendList.Add(raycastResult);
          }
        }
      }
    }

    private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
    {
      IList<Graphic> graphicsForCanvas = GraphicRegistry.GetGraphicsForCanvas(canvas);
      for (int index = 0; index < graphicsForCanvas.Count; ++index)
      {
        Graphic graphic = graphicsForCanvas[index];
        if (graphic.depth != -1 && graphic.raycastTarget && (RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera) && graphic.Raycast(pointerPosition, eventCamera)))
          GraphicRaycaster.s_SortedGraphics.Add(graphic);
      }
      GraphicRaycaster.s_SortedGraphics.Sort((Comparison<Graphic>) ((g1, g2) => g2.depth.CompareTo(g1.depth)));
      for (int index = 0; index < GraphicRaycaster.s_SortedGraphics.Count; ++index)
        results.Add(GraphicRaycaster.s_SortedGraphics[index]);
      GraphicRaycaster.s_SortedGraphics.Clear();
    }

首先可以看到在this.canvas.renderMode不通的時候使用了不通的方式來進行判定。

隨後 resultAppendList.Add(raycastResult);這一塊就是加入最終的結果列表。在這之前它判斷了(double) num1 < (double) num3)纔會寫入這個隊列。其實就是從下面這個靜態方法產生的隊列中篩選出結果放入最終隊列。這個num3是被其他什麼東西擋住了的深度,使用射線檢測來檢測的。如果Canvas是SpaceOverlayer的話這裏直接num3變爲最大浮點數。那麼下面這個測試就都能通過,否則就是沒被擋住的控件才能通過。這塊其實就是空間中非UI的物體對UI遮擋事件屏蔽的實現。

然後看靜態方法的實現,這裏實現了另一層就是每個控件是否能被點擊到的操作,如果能夠點擊到就加入隊列,最後排下序,最後再加入到resultAppendList中。

這裏的每個控件是否能被點擊到的操作判斷

首先graphic.raycastTarget這個就是我們通常在Unity中設置控件點擊是否可用的那個checkbox的值。

然後graphic是會判斷點擊點是否是在矩形區域內部,也就是說你只要超過這個區域,就一定是沒有點擊事件的。

最後是調用graphic自己的Raycast判斷是否能夠被點到。

三者全部滿足會進入後續的點擊排序。

反編譯Graphics看到

public virtual bool Raycast(Vector2 sp, Camera eventCamera)
    {
      if (!this.isActiveAndEnabled)
        return false;
      Transform transform = this.transform;
      List<Component> componentList = ListPool<Component>.Get();
      bool flag1 = false;
      bool flag2 = true;
      for (; (UnityEngine.Object) transform != (UnityEngine.Object) null; transform = !flag2 ? (Transform) null : transform.parent)
      {
        transform.GetComponents<Component>(componentList);
        for (int index = 0; index < componentList.Count; ++index)
        {
          Canvas canvas = componentList[index] as Canvas;
          if ((UnityEngine.Object) canvas != (UnityEngine.Object) null && canvas.overrideSorting)
            flag2 = false;
          ICanvasRaycastFilter canvasRaycastFilter = componentList[index] as ICanvasRaycastFilter;
          if (canvasRaycastFilter != null)
          {
            bool flag3 = true;
            CanvasGroup canvasGroup = componentList[index] as CanvasGroup;
            if ((UnityEngine.Object) canvasGroup != (UnityEngine.Object) null)
            {
              if (!flag1 && canvasGroup.ignoreParentGroups)
              {
                flag1 = true;
                flag3 = canvasRaycastFilter.IsRaycastLocationValid(sp, eventCamera);
              }
              else if (!flag1)
                flag3 = canvasRaycastFilter.IsRaycastLocationValid(sp, eventCamera);
            }
            else
              flag3 = canvasRaycastFilter.IsRaycastLocationValid(sp, eventCamera);
            if (!flag3)
            {
              ListPool<Component>.Release(componentList);
              return false;
            }
          }
        }
      }
      ListPool<Component>.Release(componentList);
      return true;
    }

也就是說這個繪製的Graphic上你如果實現過ICanvasRaycastFilter這個接口那麼就會調用這個接口的IsRaycastLocationValid來實現點擊是否觸發的判定。Image控件用到了次功能來實現Alpha的點擊穿透。

0x03Image的Alpah穿透的實現

反編譯Image可以看到

public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter

其中的最後一個接口ICanvasRaycastFilter就是畫布點擊遮罩的接口

繼續反編譯ICanvasRaycastFilter看到

namespace UnityEngine
{
  public interface ICanvasRaycastFilter
  {
    bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);
  }
}

其中IsRaycastLocationValid就是判斷當前點擊點是否能被Raycast的實現

在Image的反編譯代碼中有其實現

public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
      if ((double) this.alphaHitTestMinimumThreshold <= 0.0)
        return true;
      if ((double) this.alphaHitTestMinimumThreshold > 1.0)
        return false;
      if ((UnityEngine.Object) this.activeSprite == (UnityEngine.Object) null)
        return true;
      Vector2 localPoint;
      if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(this.rectTransform, screenPoint, eventCamera, out localPoint))
        return false;
      Rect pixelAdjustedRect = this.GetPixelAdjustedRect();
      localPoint.x += this.rectTransform.pivot.x * pixelAdjustedRect.width;
      localPoint.y += this.rectTransform.pivot.y * pixelAdjustedRect.height;
      localPoint = this.MapCoordinate(localPoint, pixelAdjustedRect);
      Rect textureRect = this.activeSprite.textureRect;
      Vector2 vector2 = new Vector2(localPoint.x / textureRect.width, localPoint.y / textureRect.height);
      float u = Mathf.Lerp(textureRect.x, textureRect.xMax, vector2.x) / (float) this.activeSprite.texture.width;
      float v = Mathf.Lerp(textureRect.y, textureRect.yMax, vector2.y) / (float) this.activeSprite.texture.height;
      try
      {
        return (double) this.activeSprite.texture.GetPixelBilinear(u, v).a >= (double) this.alphaHitTestMinimumThreshold;
      }
      catch (UnityException ex)
      {
        Debug.LogError((object) ("Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + ex.Message + " Also make sure to disable sprite packing for this sprite."), (UnityEngine.Object) this);
        return true;
      }
    }

可以發現UGUI的Image控件已經提供了透明區域是否可以點穿的功能實現,這裏的方式是將點擊點對應到圖片的UV座標,獲取UV座標的像素alpha的值。這裏可以通過外部設置image。alphaHitTestMinimumThreshold來實現alpha擊穿的控制。

如果我們自己做了一個繼承自Image的控件,那麼它的點擊狀態就是一個控件的Rect區域,跟你控件裏自己畫的的mesh沒有關係。 這時就需要重寫此方法,覆蓋之,然後實現自己的點擊判定。

0x04整個的大概總結

經過上面的分析過程。大概可以看出Unity的事件響應式如何一步步完成的。每一個tick的每一個點擊點,首先判斷對Canvas上的所有Graphic進行篩選可點擊的控件出來,排序,順序是哪個控件可以先被點到。然後把每個canvas的可點擊控件放在一起進行排序。順序是哪個canvas先被點到。最終再進行輪詢派發。

0x05優化

瞭解了整個過程,可以針對的想一想優化。比如如果一個畫布下所有控件都沒有響應,那麼直接關閉掉GraphicsRayCast要比設置每個控件上的RayCastIgnore效率好的多,諸如此類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章