簡述
最近在用UGUI的時候遇到了鼠標穿透的問題,就是說在UGUI和3D場景混合的情況下,點擊UI區域同時也會 觸發3D中物體的鼠標事件。比如下圖中

這里給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的話也同樣會出現同樣的問題。
在游戲開發中我們的UI是始終出現在屏幕的,如果在一個戰斗場景中用戶點了UI戰斗場景中的物體也會作出響應肯定是有問題的!
其實關於這個問題網上有不少解決方法了,但是總感覺沒有一個是適合我的需求,或者說沒有一個最好的答案。
其中提到最多的是利用EventSystem.current.IsPointerOverGameObject()來判斷,這個方法的意義是判斷鼠標是否點到了GameObject上面,這個GameObject包括UI也包括3D世界中的任何物體,所以他只能判斷用戶是都點到了東西。對於本文中的問題意義不是很大。那么這個問題到底該怎么解決呢?
原理
解決方法最終還是離不開射線檢測,不過UGUI中已經封裝了針對UI部分的射線碰撞的功能,那就是GraphicRaycaster類。里面有個Raycast方法如下,最終就是將射線碰撞到的點添加進resultAppendList數組。
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方法
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()來使用就比較好了。
