簡述html
最近在用UGUI的時候遇到了鼠標穿透的問題,就是說在UGUI和3D場景混合的狀況下,點擊UI區域同時也會 觸發3D中物體的鼠標事件。好比下圖中canvas
這裏給Cube加了一個鼠標點擊改變顏色的代碼,以下數組
void Update() { if(Input.GetMouseButtonDown(0)) { GetComponent<Renderer>().material.color = new Color(Random.value, Random.value, Random.value, 1.0f); } }
運行一下,會發現只要有鼠標點擊(任何位置點擊),Cube的顏色就會改變,根據代碼咱們知道這也是必然的,可是問題是若是Cube是一個3D世界中的mesh或者terrain,而button是UI的話也一樣會出現一樣的問題。app
在遊戲開發中咱們的UI是始終出如今屏幕的,若是在一個戰鬥場景中用戶點了UI戰鬥場景中的物體也會做出響應確定是有問題的!dom
其實關於這個問題網上有很多解決方法了,可是總感受沒有一個是適合個人需求,或者說沒有一個最好的答案。ide
其中提到最多的是利用EventSystem.current.IsPointerOverGameObject()來判斷,這個方法的意義是判斷鼠標是否點到了GameObject上面,這個GameObject包括UI也包括3D世界中的任何物體,因此他只能判斷用戶是都點到了東西。對於本文中的問題意義不是很大。那麼這個問題到底該怎麼解決呢?函數
原理ui
解決方法最終仍是離不開射線檢測,不過UGUI中已經封裝了針對UI部分的射線碰撞的功能,那就是GraphicRaycaster類。裏面有個Raycast方法以下,最終就是將射線碰撞到的點添加進resultAppendList數組。this
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList) { if (canvas == null) return; // Convert to view space Vector2 pos; if (eventCamera == null) pos = new Vector2(eventData.position.x / Screen.width, eventData.position.y / Screen.height); else pos = eventCamera.ScreenToViewportPoint(eventData.position); // If it's outside the camera's viewport, do nothing if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) return; float hitDistance = float.MaxValue; Ray ray = new Ray(); if (eventCamera != null) ray = eventCamera.ScreenPointToRay(eventData.position); if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None) { float dist = eventCamera.farClipPlane - eventCamera.nearClipPlane; if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All) { RaycastHit hit; if (Physics.Raycast(ray, out hit, dist, m_BlockingMask)) { hitDistance = hit.distance; } } if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All) { RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction, dist, m_BlockingMask); if (hit.collider != null) { hitDistance = hit.fraction * dist; } } } m_RaycastResults.Clear(); Raycast(canvas, eventCamera, eventData.position, m_RaycastResults); for (var index = 0; index < m_RaycastResults.Count; index++) { var go = m_RaycastResults[index].gameObject; bool appendGraphic = true; if (ignoreReversedGraphics) { if (eventCamera == null) { // If we dont have a camera we know that we should always be facing forward var dir = go.transform.rotation * Vector3.forward; appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0; } else { // If we have a camera compare the direction against the cameras forward. var cameraFoward = eventCamera.transform.rotation * Vector3.forward; var dir = go.transform.rotation * Vector3.forward; appendGraphic = Vector3.Dot(cameraFoward, dir) > 0; } } if (appendGraphic) { float distance = 0; if (eventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay) distance = 0; else { // http://geomalgorithms.com/a06-_intersect-2.html distance = (Vector3.Dot(go.transform.forward, go.transform.position - ray.origin) / Vector3.Dot(go.transform.forward, ray.direction)); // Check to see if the go is behind the camera. if (distance < 0) continue; } if (distance >= hitDistance) continue; var castResult = new RaycastResult { gameObject = go, module = this, distance = distance, index = resultAppendList.Count, depth = m_RaycastResults[index].depth, sortingLayer = canvas.sortingLayerID, sortingOrder = canvas.sortingOrder }; resultAppendList.Add(castResult); } } }
從這個方法開始深刻查看Unity UGUI源碼你會發現,其實每一個組件在建立的時候已經被添加進了一個公共列表,UGUI 源碼中的GraphicRegistry類就是專門幹這件事的。再看下Graphic類中的OnEnable方法spa
protected override void OnEnable() { base.OnEnable(); CacheCanvas(); GraphicRegistry.RegisterGraphicForCanvas(canvas, this); #if UNITY_EDITOR GraphicRebuildTracker.TrackGraphic(this); #endif if (s_WhiteTexture == null) s_WhiteTexture = Texture2D.whiteTexture; SetAllDirty(); SendGraphicEnabledDisabled(); }
看這句GraphicRegistry.RegisterGraphicForCanvas(canvas, this);就是註冊須要作射線檢測的UI組件。再看他內部是如何工做的
public static void RegisterGraphicForCanvas(Canvas c, Graphic graphic) { if (c == null) return; IndexedSet<Graphic> graphics; instance.m_Graphics.TryGetValue(c, out graphics); if (graphics != null) { graphics.Add(graphic); return; } graphics = new IndexedSet<Graphic>(); graphics.Add(graphic); instance.m_Graphics.Add(c, graphics); }
不過,問題又來了,爲何是添加進列表的對象都是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,以下
bool CheckGuiRaycastObjects() { PointerEventData eventData = new PointerEventData(Main.Instance.eventSystem); eventData.pressPosition = Input.mousePosition; eventData.position = Input.mousePosition; List<RaycastResult> list = new List<RaycastResult>(); Main.Instance.graphicRaycaster.Raycast(eventData, list); //Debug.Log(list.Count); return list.Count > 0; }
不過在使用時須要先獲取兩個加粗顯示的變量,graphicRaycaster和eventSystem。
這兩個變量分別對應的是Canvas中的GraphicRaycaster組件,和建立UI時自動生成的「EventSystem」中的EventSystem組件,用的是本身指定一下就能夠。
而後在使用的時候能夠這樣:
void Update () { if (CheckGuiRaycastObjects()) return; //Debug.Log(EventSystem.current.gameObject.name); if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { //do some thing } } }
還有一個須要注意的地方就是,在作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了,如今舒服多啦!