這次的專案遇到了一個遊走在畫面上的3D可點擊物件,這個3D還夾在UI和UI中間,這個就會出現穿透問題與防穿透問題。
這個案例蠻特殊的,會在UI上方移動的3D物件案例不多,所以在網路的文章大部分都只寫到防止UI穿透後方3D物件,不然就是防止同一攝影機底下的UI是否被遮擋,兩者都沒有達到我完整的需求,只能想其他方法了
網路上很多都是UI防止遮蔽的後方3D物件同時被點擊,作法也蠻多種的
1.EventSystem
public void OnMouseUpAsButton() { if(!EventSystem.current.IsPointerOverGameObject()) { //沒有UI遮蔽 } }
但這個在IOS和Android實機上會沒有作用,所以可以改成
public void OnMouseUpAsButton() { #if IPHONE || ANDROID if(!EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId)) #else if(!EventSystem.current.IsPointerOverGameObject()) #endif { //沒有UI遮蔽 } }
2.EventTrigger或者實作IPointerClickHandler介面
using UnityEngine; using UnityEngine.EventSystems; public class EventTest : MonoBehaviour, IPointerClickHandler { public void OnPointerClick(PointerEventData eventData) { //點擊 } }
這個方式要注意:
Hierachy要有 EventSystem 物件
Camera 要有 Physics Raycaster Component
3D物件 要有Collider Component
3.RaycastAll
public void OnMouseUpAsButton() { PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current); eventDataCurrentPosition.position = new Vector2(screenPosition.x, screenPosition.y); List<RaycastResult> results = new List<RaycastResult>(); EventSystem.current.RaycastAll(eventDataCurrentPosition, results); if(results.Count > 0){ // 點擊 } }
上方做法基本能應付基礎需求,避免點擊UI時點擊到3D物件的問題,再來就是反過來處理3D物件遮擋UI點擊問題
3D物件能遮擋UI點擊
基本上就直接把Canvas底下的Graphic Raycaster的Blocking Objects改成3D就可以了
正常情況下,這樣的確能解決基礎需求,不過如果場景上有多台攝影機呢?
如果是多台攝影機的情況下只是改Graphic Raycaster的Blocking Objects並沒辦法真正解決遮擋問題
假設場上有兩台攝影機,一台是渲染UI的放在原點位置,一台渲染3D的在其他位置,這樣就算最後畫面上3D物件是在UI上方,但也無法真正達到遮蔽效果,因為實際位置的不同,3D物件遮蔽不到Graphic Raycaster
最後只好自己找方法來解決這個問題了
這部分以MonoBehaviour.OnMouseDown系列的function為例,分為幾個步驟
- UI擋3D物件_比對遮擋Layer
gameObject.layer和LayerMask並不一樣
在unity中Layer設定上是一個 Int32,再分別把每個 bit 拿來當作儲存的開關,所以最多只能設定32個
而呈現方式也分成兩種:
LayerMask :是二進位數值做32個開關 0000 0000 0000 0000 0000 0000 0000 0000 (由左至右 31 ~ 0)
GameObject.layer :則會得到 Layer 的排序(0,1,2,3,4,5,6……)
要轉二進為可以直接用位移運算子
1<<4:1左移4位 =>0001 0000
// 0001 0000 Layer為UI,使用 << 運算子之後的結果
// 0001 1000 LayerMask勾選UI、Water
// ─────────
// 0001 1000 取聯集之後的結果 (聯集用OR運算子)
public LayerMask m_BlockLayer; private BaseRaycaster m_Raycaster; private bool m_IsBlocking = false; private bool CompareLayer(int layer) { return (1 << layer | m_BlockLayer) == m_BlockLayer; }
- UI擋3D物件_偵測是否有UI
EventSystem.current.RaycastAll()是以深度檢測,且3D物件深度不能比UI還要前面,這樣射線將會被3D物件阻斷
這裡用EventSystem.current.RaycastAll檢測是否有UI
點擊有多個物件時,檢查第一個物件是否為可遮蔽UI物件
如果有:就把m_IsBlocking設為true (上層的遮蔽UI)
如果沒有:代表底下還有其他UI時,就把UI的Raycaster直接關掉,讓UI不受點擊 (下層的被遮蔽UI)
這邊一定要特別檢查3D物件下方是否還有UI,剛剛雖然說射線會被3D物件阻斷,照道理不用特別判斷下方是否還有其他UI,但剛剛說EventSystem.current.RaycastAll()的射線阻斷其實是實際深度的部分,所以如果場景上有多個Canvas,而Canvas實際位置又與3D物件錯開,最後畫面的渲染雖然是重疊的,射線也不會因此阻斷,所回傳的UI陣列也會大於0
EventSystem.current.RaycastAll()會針對每個Canvas做射線偵測,這也就是為什麼還要再次判斷如果有偵測到UI,且UI不屬於阻擋層的情況下要關閉其BaseRaycaster的原因,主要以防此UI被點擊
private void OverlapUI() { PointerEventData data = new PointerEventData(EventSystem.current); data.position = Input.mousePosition; List<RaycastResult> raycastResult = new List<RaycastResult>(); EventSystem.current.RaycastAll(data, raycastResult); if (raycastResult.Count > 0 && CompareLayer(raycastResult[0].gameObject.layer)) { m_IsBlocking = true; } else if (raycastResult.Count > 0) { m_Raycaster = raycastResult[0].module; m_Raycaster.enabled = false; } }
這時還會產生一個問題
如果把UI的Raycaster直接關掉,其他同層UI也會因此不能點擊,要特別為此做修正
對這個修正來說MonoBehaviour下函式的執行順序就很重要了
有了順序就可以做關掉再開啟了
//點下會最先執行,判定UI遮蔽與被遮蔽 private void OnMouseDown() { OverlapUI(); } //Click點擊第二,沒遮蔽後的點擊事件 public void OnMouseUpAsButton() { if (!m_IsBlocking) { //執行3D物件點擊 } } //最後點擊放開,回復成未遮蔽和可點擊UI的狀態 private void OnMouseUp() { m_IsBlocking = false; if (m_Raycaster is not null) { m_Raycaster.enabled = true; } }
這樣就大功告成了,現在3D物件能防UI穿透又能遮蔽UI了!了
留言列表