zoukankan      html  css  js  c++  java
  • 【Unity游戏开发】你真的了解UGUI中的IPointerClickHandler吗?

    一、引子

      马三在最近的开发工作中遇到了一个比较有意思的bug:“TableViewCell上面的某些自定义UI组件不能响应点击事件,并且它的父容器TableView也不能响应点击事件,但是TableViewCell上面的Button等组件却可以接受点击事件,并且如果单独把自定义UI控件放在一个UI上面也可以接受点击事件”。最后马三通过仔细地分析,发现是某些自定义的UI组件实现方法的问题。通常情况下,如果想要一个UI响应点击事件的话,我们只需要实现IPointerClickHandler这个接口就可以了,但是在我们项目中的TableView继承自MonoBehavior,并且实现了IPointerClickHandler, IPointerDownHandler, IPointerUpHandler,IDragHandler等UI接口,此时如果我们的自定义UI组件只实现了IPointerClickHandler接口,而没有实现 IPointerDownHandler 接口,然后又作为TableViewCell里面的一个Child的话,就会出现TableViewCell接收不到点击事件,TableView也接收不到点击事件。点击事件被诡异地“吞没了”!下面我们简单地设计三个不同情况下的模拟测试来复现一下这个bug。

    二、进行测试

    情况1:没有父节点,自己身上挂载的脚本只实现IPointerClickHandler接口:

      场景中只有一个类型为Image的普通节点,它身上挂载了一个名为ChildHandler的脚本,该脚本只实现IPointerClickHandler接口

     1 using System.Collections;
     2 using System.Collections.Generic;
     3 using UnityEngine;
     4 using UnityEngine.EventSystems;
     5 
     6 public class ChildHandler : MonoBehaviour, IPointerClickHandler
     7 {
     8     public void OnPointerClick(PointerEventData eventData)
     9     {
    10         Debug.Log("Child OnPointerClick" + eventData.ToString());
    11     }
    12 }

      运行游戏,点击Image组件,观察控制台输出结果如下,这种情况下,我们只实现了IPointerClickHandler接口便接收到了点击事件。

    情况2:有父节点,父节点挂载的脚本实现了IPointerClickHandler, IPointerDownHandler, IPointerUpHandler接口,自己身上挂载的脚本亦实现同样的接口:

      然后我们再建立一个名为Parent的父节点,将Child子节点移动到Parent节点的内部。Parent节点挂载ParentHandler脚本,该脚本实现IPointerClickHandler, IPointerDownHandler, IPointerUpHandler接口。Child子节点挂载ChildHandler脚本,该脚本跟ParentHandler脚本实现相同的接口。

     1 using System.Collections;
     2 using System.Collections.Generic;
     3 using UnityEngine;
     4 using UnityEngine.EventSystems;
     5 
     6 public class ParentHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
     7 {
     8     public void OnPointerClick(PointerEventData eventData)
     9     {
    10         Debug.Log("Parent OnPointerClick" + eventData.ToString());
    11     }
    12 
    13     public void OnPointerDown(PointerEventData eventData)
    14     {
    15         Debug.Log("Parent OnPointerDown" + eventData.ToString());
    16     }
    17 
    18     public void OnPointerUp(PointerEventData eventData)
    19     {
    20         Debug.Log("Parent OnPointerUp" + eventData.ToString());
    21     }
    22 }
     1 using System.Collections;
     2 using System.Collections.Generic;
     3 using UnityEngine;
     4 using UnityEngine.EventSystems;
     5 
     6 public class ChildHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
     7 {
     8     public void OnPointerClick(PointerEventData eventData)
     9     {
    10         Debug.Log("Child OnPointerClick" + eventData.ToString());
    11     }
    12 
    13     public void OnPointerDown(PointerEventData eventData)
    14     {
    15         Debug.Log("Child OnPointerDown" + eventData.ToString());
    16     }
    17 
    18     public void OnPointerUp(PointerEventData eventData)
    19     {
    20         Debug.Log("Child OnPointerUp" + eventData.ToString());
    21     }
    22 }

      运行游戏,分别点击Child区域和Parent区域,观察控制台输出结果,可以发现子节点和父节点都可以分别接收到到点击事件。

    情况3:有父节点,父节点挂载的脚本实现了IPointerClickHandler, IPointerDownHandler, IPointerUpHandler接口,自己身上挂载的脚本只实现IPointerClickHandler接口:

      接着我们再来看最后一种情况,它跟上面的情况差不多,不同的是ChildHandler只实现了IPointerClickHandler接口,而没有实现 IPointerDownHandler, IPointerUpHandler另外两个接口:

     1 using System.Collections;
     2 using System.Collections.Generic;
     3 using UnityEngine;
     4 using UnityEngine.EventSystems;
     5 
     6 public class ChildHandler : MonoBehaviour, IPointerClickHandler
     7 {
     8     public void OnPointerClick(PointerEventData eventData)
     9     {
    10         Debug.Log("Child OnPointerClick" + eventData.ToString());
    11     }
    12 }

      运行游戏,分别点击Child区域和Parent区域,观察控制台输出结果,可以发现无论我们如何点击Child区域都无法接收到Click事件,并且这个Click事件也没有传递到父节点中。正如我们开篇所说的一样,父节点只接收到了Down和Up的事件,Click事件被“吞没了”。点击子节点没有和父节点重叠的地方,父节点正常地接收到了点击事件和Down、Up的事件。

       那么我们的Click事件去哪里了呢?到底是被谁给偷偷吃掉了呢?我们不妨从分析UGUI的源码入手,分析一下问题所在,再次贴上UGUI的源码传送门

    三、分析原因与源码

      因为我们是在Windows平台进行测试的,所以我们打开StandaloneInputModule.cs这个脚本进行观察,我们直接来到第431行ProcessMouseEvent函数,这个函数负责处理鼠标的事件。

      里面就一行调用,调用了ProcessMouseEvent这个函数,那么我们再继续观察ProcessMouseEvent的内容:

      重点关注一下453行的ProcessMousePress方法,它处理了鼠标的左键点击,那么我们就以鼠标左键点击来继续往下分析一下,完整的ProcessMousePress函数代码如下:

      1         /// <summary>
      2         /// Process the current mouse press.
      3         /// </summary>
      4         protected void ProcessMousePress(MouseButtonEventData data)
      5         {
      6             var pointerEvent = data.buttonData;
      7             var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
      8 
      9             // PointerDown notification
     10             if (data.PressedThisFrame())
     11             {
     12                 pointerEvent.eligibleForClick = true;
     13                 pointerEvent.delta = Vector2.zero;
     14                 pointerEvent.dragging = false;
     15                 pointerEvent.useDragThreshold = true;
     16                 pointerEvent.pressPosition = pointerEvent.position;
     17                 pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
     18 
     19                 DeselectIfSelectionChanged(currentOverGo, pointerEvent);
     20 
     21                 // search for the control that will receive the press
     22                 // if we can't find a press handler set the press
     23                 // handler to be what would receive a click.
     24                 var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
     25 
     26                 // didnt find a press handler... search for a click handler
     27                 if (newPressed == null)
     28                     newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
     29 
     30                 // Debug.Log("Pressed: " + newPressed);
     31 
     32                 float time = Time.unscaledTime;
     33 
     34                 if (newPressed == pointerEvent.lastPress)
     35                 {
     36                     var diffTime = time - pointerEvent.clickTime;
     37                     if (diffTime < 0.3f)
     38                         ++pointerEvent.clickCount;
     39                     else
     40                         pointerEvent.clickCount = 1;
     41 
     42                     pointerEvent.clickTime = time;
     43                 }
     44                 else
     45                 {
     46                     pointerEvent.clickCount = 1;
     47                 }
     48 
     49                 pointerEvent.pointerPress = newPressed;
     50                 pointerEvent.rawPointerPress = currentOverGo;
     51 
     52                 pointerEvent.clickTime = time;
     53 
     54                 // Save the drag handler as well
     55                 pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
     56 
     57                 if (pointerEvent.pointerDrag != null)
     58                     ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
     59             }
     60 
     61             // PointerUp notification
     62             if (data.ReleasedThisFrame())
     63             {
     64                 // Debug.Log("Executing pressup on: " + pointer.pointerPress);
     65                 ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
     66 
     67                 // Debug.Log("KeyCode: " + pointer.eventData.keyCode);
     68 
     69                 // see if we mouse up on the same element that we clicked on...
     70                 var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
     71 
     72                 // PointerClick and Drop events
     73                 if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
     74                 {
     75                     ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
     76                 }
     77                 else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
     78                 {
     79                     ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
     80                 }
     81 
     82                 pointerEvent.eligibleForClick = false;
     83                 pointerEvent.pointerPress = null;
     84                 pointerEvent.rawPointerPress = null;
     85 
     86                 if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
     87                     ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);
     88 
     89                 pointerEvent.dragging = false;
     90                 pointerEvent.pointerDrag = null;
     91 
     92                 // redo pointer enter / exit to refresh state
     93                 // so that if we moused over somethign that ignored it before
     94                 // due to having pressed on something else
     95                 // it now gets it.
     96                 if (currentOverGo != pointerEvent.pointerEnter)
     97                 {
     98                     HandlePointerExitAndEnter(pointerEvent, null);
     99                     HandlePointerExitAndEnter(pointerEvent, currentOverGo);
    100                 }
    101             }
    102         }

      在这个函数中首先会拿到射线检测返回的gameobject,然后搜索当前的gameobejct以及其父节点上面是否有实现了IPointerDownHandler的接口的控件,如果有实现了的就把newPressed赋值为这个控件的gameobject,如果没有,就去搜索实现了IPointerClickHandler这个接口的控件,如果没有在自身上找到的话,会依次地向父节点层层搜索,直到找到为止,然后依然是把newPressed赋值为这个控件的gameobject。接着会按照类似的方式去搜索自身以及父节点上是否有实现了IDragHandler的组件,如果有的话紧接着便会去触发OnPointerDown和OnDrag方法。

      当鼠标按下并抬起的时候,首先会触发IPointerUpHandler接口中的函数OnPointerUp(),然后会再次搜索当前gameobject以及其父节点上是否有实现了IPointerClickHandler接口的控件,如果有的的话,会和之前存下来的newPressd进行比较,看两者是否为同一个gameobject。如果两者为同一个gameobject的话就会触发Click事件。那么问题就出现在这里了,Unity原本想用这段代码判断鼠标按下和抬起的时候,鼠标指向的物体有没有变化。如果有变化,前后指向的不是同一个gameobject的话就不触发Click事件了。虽然原本是想实现这个功能,但是当我们的父节点实现了IPointerDownHandler和IPointerClickHandler接口,而子节点只实现了IPointerClickHandler接口的时候,就会造成两次获取的gameobject不匹配,那么也就不会触发任何的Click事件了,所以无论是父节点亦或者子节点脚本中的OnPointerClick方法也不会被调用到了,看来Click事件就是被这里“吃掉了”。虽然在这里我们只分析了Windows平台下的鼠标点击实现,但是在Mobile平台上,在触摸事件的处理上也是使用了类似的手段,也就是说这个bug也会在Android或者iOS平台上出现。

      因此我们需要注意,如果一个物体没有父节点的话,那么只实现IPointerClickHandler接口便是可以接收到点击事件的。如果他有父节点,父节点挂载的脚本也是只实现IPointerClickHandler接口的话,点击事件也是可以接收到的。但是如果父节点实现了IPointerDownHandler和IPointerClickHandler接口,子节点只实现IPointerClickHandler接口的话,两者便会都接收不到点击事件,需要子节点也实现IPointerDownHandler这个接口才行。

    三、总结

      通过一系列的试验和对UGUI源码地分析,我们弄明白了Click事件为什么消失不见了,以及UGUI接口使用中的一些需要注意的小细节和坑。看来只顾闷头写业务逻辑是完全不够的啊,在必要的时候,我们需要“沉下去”,去阅读更底层的源码,去分析bug出现的根本原因,这样才能起到“标本兼治”的效果,这样我们写起代码来才能更加安心。同时通过阅读源码,对源码进行分析和思考,也可以提升我们的编码水平、深化编程思想。因此马三决定会在接下来的博客计划中开辟出一个系统分析UGUI源码的系列文章,让我们一起来“扒开UGUI的祖坟”。

      本篇博客中的项目代码已经同步至Github,欢迎Fork!https://github.com/XINCGer/Unity3DTraining/tree/master/SomeTest/About_IPointerClickHandler

    如果觉得本篇博客对您有帮助,可以扫码小小地鼓励下马三,马三会写出更多的好文章,支持微信和支付宝哟!

           

    作者:马三小伙儿
    出处:https://www.cnblogs.com/msxh/p/10588783.html 
    请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

  • 相关阅读:
    如何在iTerm2中配置oh my zsh?
    sublime中格式化jsx文件
    ES6 new syntax of Literal
    ES6 new syntax of Rest and Spread Operators
    How to preview html file in our browser at sublime text?
    ES6 new syntax of Default Function Parameters
    ES6 new syntax of Arrow Function
    七牛云2018春招笔试题
    Spring-使用注解开发(十二)
    Spring-声明式事物(十一)
  • 原文地址:https://www.cnblogs.com/msxh/p/10588783.html
Copyright © 2011-2022 走看看