close

這次的專案遇到了一個遊走在畫面上的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就可以了

截圖 2023-01-16 下午5.02.40

 

正常情況下,這樣的確能解決基礎需求,不過如果場景上有多台攝影機呢?

如果是多台攝影機的情況下只是改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個 

Layer

而呈現方式也分成兩種:

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下函式的執行順序就很重要了

截圖 2023-01-17 上午11.58.27

有了順序就可以做關掉再開啟了

//點下會最先執行,判定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了!

 

arrow
arrow

    Kouhei 發表在 痞客邦 留言(0) 人氣()