Unity UGUI鼠标穿透UI问题(Unity官方的解决方法)

  • 简述

    最近在用UGUI的时候遇到了鼠标穿透的问题,就是说在UGUI和3D场景混合的情况下,点击UI区域同时也会 触发3D中物体的鼠标事件。比如下图中

    \

    这里给Cube加了一个鼠标点击改变颜色的代码,如下

    1. void Update()
    2. {
    3. if(Input.GetMouseButtonDown(0))
    4. {
    5. GetComponent<Renderer>().material.color = new Color(Random.value, Random.value, Random.value, 1.0f);
    6. }
    7. }

    运行一下,会发现只要有鼠标点击(任何位置点击),Cube的颜色就会改变,根据代码我们知道这也是必然的,但是问题是如果Cube是一个3D世界中的mesh或者terrain,而button是UI的话也同样会出现同样的问题。

    在游戏开发中我们的UI是始终出现在屏幕的,如果在一个战斗场景中用户点了UI战斗场景中的物体也会作出响应肯定是有问题的!

    其实关于这个问题网上有不少解决方法了,但是总感觉没有一个是适合我的需求,或者说没有一个最好的答案。

    其中提到最多的是利用EventSystem.current.IsPointerOverGameObject()来判断,这个方法的意义是判断鼠标是否点到了GameObject上面,这个GameObject包括UI也包括3D世界中的任何物体,所以他只能判断用户是都点到了东西。对于本文中的问题意义不是很大。那么这个问题到底该怎么解决呢?

    原理

    解决方法最终还是离不开射线检测,不过UGUI中已经封装了针对UI部分的射线碰撞的功能,那就是GraphicRaycaster类。里面有个Raycast方法如下,最终就是将射线碰撞到的点添加进resultAppendList数组。

    001. public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    002. {
    003. if (canvas == null)
    004. return;
    005.  
    006. // Convert to view space
    007. Vector2 pos;
    008. if (eventCamera == null)
    009. pos = new Vector2(eventData.position.x / Screen.width, eventData.position.y / Screen.height);
    010. else
    011. pos = eventCamera.ScreenToViewportPoint(eventData.position);
    012.  
    013. // If it's outside the camera's viewport, do nothing
    014. if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
    015. return;
    016.  
    017. float hitDistance = float.MaxValue;
    018.  
    019. Ray ray = new Ray();
    020.  
    021. if (eventCamera != null)
    022. ray = eventCamera.ScreenPointToRay(eventData.position);
    023.  
    024. if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
    025. {
    026. float dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;
    027.  
    028. if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
    029. {
    030. RaycastHit hit;
    031. if (Physics.Raycast(ray, out hit, dist, m_BlockingMask))
    032. {
    033. hitDistance = hit.distance;
    034. }
    035. }
    036.  
    037. if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
    038. {
    039. RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction, dist, m_BlockingMask);
    040.  
    041. if (hit.collider != null)
    042. {
    043. hitDistance = hit.fraction * dist;
    044. }
    045. }
    046. }
    047.  
    048. m_RaycastResults.Clear();
    049. Raycast(canvas, eventCamera, eventData.position, m_RaycastResults);
    050.  
    051. for (var index = 0; index < m_RaycastResults.Count; index++)
    052. {
    053. var go = m_RaycastResults[index].gameObject;
    054. bool appendGraphic = true;
    055.  
    056. if (ignoreReversedGraphics)
    057. {
    058. if (eventCamera == null)
    059. {
    060. // If we dont have a camera we know that we should always be facing forward
    061. var dir = go.transform.rotation * Vector3.forward;
    062. appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
    063. }
    064. else
    065. {
    066. // If we have a camera compare the direction against the cameras forward.
    067. var cameraFoward = eventCamera.transform.rotation * Vector3.forward;
    068. var dir = go.transform.rotation * Vector3.forward;
    069. appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
    070. }
    071. }
    072.  
    073. if (appendGraphic)
    074. {
    075. float distance = 0;
    076.  
    077. if (eventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
    078. distance = 0;
    079. else
    080. {
    082. distance = (Vector3.Dot(go.transform.forward, go.transform.position - ray.origin) / Vector3.Dot(go.transform.forward, ray.direction));
    083.  
    084. // Check to see if the go is behind the camera.
    085. if (distance < 0)
    086. continue;
    087. }
    088.  
    089. if (distance >= hitDistance)
    090. continue;
    091.  
    092. var castResult = new RaycastResult
    093. {
    094. gameObject = go,
    095. module = this,
    096. distance = distance,
    097. index = resultAppendList.Count,
    098. depth = m_RaycastResults[index].depth,
    099. sortingLayer =  canvas.sortingLayerID,
    100. sortingOrder = canvas.sortingOrder
    101. };
    102. resultAppendList.Add(castResult);
    103. }
    104. }
    105. }

    从这个方法开始深入查看Unity UGUI源码你会发现,其实每个组件在创建的时候已经被添加进了一个公共列表,UGUI 源码中的GraphicRegistry类就是专门干这件事的。再看下Graphic类中的OnEnable方法

    01. protected override void OnEnable()
    02. {
    03. base.OnEnable();
    04. CacheCanvas();
    05. GraphicRegistry.RegisterGraphicForCanvas(canvas, this);
    06.  
    07. #if UNITY_EDITOR
    08. GraphicRebuildTracker.TrackGraphic(this);
    09. #endif
    10. if (s_WhiteTexture == null)
    11. s_WhiteTexture = Texture2D.whiteTexture;
    12.  
    13. SetAllDirty();
    14.  
    15. SendGraphicEnabledDisabled();
    16. }

    看这句GraphicRegistry.RegisterGraphicForCanvas(canvas, this);就是注册需要做射线检测的UI组件。再看他内部是如何工作的

    01. public static void RegisterGraphicForCanvas(Canvas c, Graphic graphic)
    02. {
    03. if (c == null)
    04. return;
    05.  
    06. IndexedSet<Graphic> graphics;
    07. instance.m_Graphics.TryGetValue(c, out graphics);
    08.  
    09. if (graphics != null)
    10. {
    11. graphics.Add(graphic);
    12. return;
    13. }
    14.  
    15. graphics = new IndexedSet<Graphic>();
    16. graphics.Add(graphic);
    17. instance.m_Graphics.Add(c, graphics);
    18. }

    不过,问题又来了,为什么是添加进列表的对象都是Graphic类型呢?这跟ScrollRect,Button,Slider这些有关吗?其实,这就跟UGUI的类继承关系有关了,其实我们使用的UGUI中的每个组件都是继承自Graphic或者依赖一个继承自Graphic的组件

    看一下UGUI的类层次结构就会一目了然,如下

    \

    看图就会更加清楚,在这我们可以把我们用到的UGUI的所有组件分为两类,1.是直接继承自Graphic的组件。2.是依赖于1的组件"[RequireComponent(typeof(Griphic))]",仔细想想会发现,所有组件都属于这两种中的某一种。

    所以对所有Graphic进行Raycast其实就相当于对所有UI组件进行Raycast。

    结合上面的知识所以,解决这个问题最好的方法是根据,UGUI的射线碰撞来做。这样会比较合理。

    解决方案

    这里我们直接在使用Input.GetMouseButtonDown(0)的地方加了一个检测函数,CheckGuiRaycastObjects,如下

    01. bool CheckGuiRaycastObjects()
    02. {
    03. PointerEventData eventData = new PointerEventData(Main.Instance.<strong>eventSystem</strong>);
    04. eventData.pressPosition = Input.mousePosition;
    05. eventData.position = Input.mousePosition;
    06.  
    07. List<RaycastResult> list = new List<RaycastResult>();
    08. Main.Instance.<strong>graphicRaycaster</strong>.Raycast(eventData, list);
    09. //Debug.Log(list.Count);
    10. return list.Count > 0;
    11. }

    不过在使用时需要先获取两个加粗显示的变量,graphicRaycaster和eventSystem。

    这两个变量分别对应的是Canvas中的GraphicRaycaster组件,和创建UI时自动生成的“EventSystem”中的EventSystem组件,用的是自己制定以下就可以。

    然后在使用的时候可以这样:

    01. void Update ()
    02. {
    03. if (CheckGuiRaycastObjects()) return;
    04. //Debug.Log(EventSystem.current.gameObject.name);
    05. if (Input.GetMouseButtonDown(0))
    06. {
    07. Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    08. RaycastHit hit;
    09.  
    10. if (Physics.Raycast(ray, out hit))
    11. {
    12. //do some thing
    13. }
    14. }
    15. }

    还有一个需要注意的地方就是,在做UI的时候一般会用一个Panel做跟目录,这个panel也会被添加到GraphicRegistry中的公共列表中,如果是这样的话记得把list.Count>0改成list.Count>1,或者直接删除Panel上的继承自Graphic的组件。

    这样在结合着EventSystem.current.IsPointerOverGameObject()来使用就比较好了。

    本文固定连接:http://www.cnblogs.com/fly-100/p/4570366.html

    ok了,现在舒服多啦!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值