點擊事件穿透是新手引導中最重要的一個功能,通常做法是使用一個全屏UI。該UI放置於UI的最高層級擋住所有UI,然后監聽IPointerClickHandler事件,當OnPointerClick回調觸發時,通過EventSystem.current.RaycastAll獲得當前點擊的對象列表。
對該對象列表中的結果對象執行ExecuteEvents.Execute實現點擊穿透功能。
相關代碼已上傳github
https://github.com/terrynoya/UnityMaskPanetrateExample
穿透功能實現
下面實現一下該功能:
創建一個空的GameObject,掛上EmptyGraphic,使之響應點擊事件,掛Image也可以同樣起到作用。
public class EmptyGraphic:MaskableGraphic
{
}
遮罩層穿透邏輯
public class PanerateMask : MonoBehaviour,IPointerClickHandler
{
public void OnPointerClick(PointerEventData eventData)
{
Raycast(eventData);
}
private void Raycast(PointerEventData eventData)
{
_rawRaycastResults.Clear();
EventSystem.current.RaycastAll(eventData, _rawRaycastResults);
foreach (var rlt in _rawRaycastResults)
{
Debug.Log(rlt.gameObject);
//遮罩層自身需要添加該腳本,否則會導致ExecuteEvents.Execute再次觸發遮罩層自身的IPointerClickHandler導致死循環
if (rlt.gameObject.GetComponent<IgnoreEventRaycast>())
{
continue;
}
ExecuteEvents.Execute(rlt.gameObject, eventData, ExecuteEvents.pointerClickHandler);
}
}
}
遮罩層自身需要添加該腳本,否則會導致ExecuteEvents.Execute再次觸發遮罩層自身的IPointerClickHandler導致死循環
public class IgnoreEventRaycast:MonoBehaviour
{
}
制作按鈕
下面制作按鈕進行測試
實際開發種,按鈕通常有2種做法
1.Image和Button在同一個GameObject上
2.Image是Button的子節點
測試
我們需要編寫一個Test,驗證按鈕的OnClick是否能被觸發
public class TestMain:MonoBehaviour
{
public Button Btn1;
public Button Btn2;
private void Awake()
{
Btn1.onClick.AddListener(OnBtn1Click);
Btn2.onClick.AddListener(OnBtn2Click);
}
private void OnBtn1Click()
{
Debug.Log("btn1 clicked!!");
}
private void OnBtn2Click()
{
Debug.Log("btn2 clicked!!");
}
}
最終的層級結構如下
下面點擊btn1
可以看到btn1的OnClick順利觸發了。
點擊btn2,並沒有出現我們期待的結果
解決方法
將ExecuteEvents.Execute改成ExecuteEvents.ExecuteHierarchy
測試后得到了正確的結果
原理
我們需要看下ExcuteEvents.Exeucte做了什么
Execute內部,會收集被點擊的GameObject實現的IEventSystemHandler接口列表,然后對接口進行調用。這里我們需要的是觸發IPointerClickHandler的回調
代碼如下:
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
var internalHandlers = s_HandlerListPool.Get();
GetEventList<T>(target, internalHandlers);
// if (s_InternalHandlers.Count > 0)
// Debug.Log("Executinng " + typeof (T) + " on " + target);
for (var i = 0; i < internalHandlers.Count; i++)
{
T arg;
try
{
arg = (T)internalHandlers[i];
}
catch (Exception e)
{
var temp = internalHandlers[i];
Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));
continue;
}
try
{
functor(arg, eventData);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
var handlerCount = internalHandlers.Count;
s_HandlerListPool.Release(internalHandlers);
return handlerCount > 0;
}
當點擊btn2時,穿透對象是Image,Image自身未實現IPointerClickHandler接口,所以Execute不起作用。
當點擊btn1時,穿透對象是btn1,而btn1上掛有Button,Button實現了IPointerClickHandler接口,因此Execute起作用。
讓我們看下ExecuteHierarchy
ExecuteHierarchy中,GetEventChain函數會收集點擊對象以及對象的所有父節點,對節點列表依次調用Execute,調用成功停止循環。
public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler
{
GetEventChain(root, s_InternalTransformList);
for (var i = 0; i < s_InternalTransformList.Count; i++)
{
var transform = s_InternalTransformList[i];
if (Execute(transform.gameObject, eventData, callbackFunction))
return transform.gameObject;
}
return null;
}
private static void GetEventChain(GameObject root, IList<Transform> eventChain)
{
eventChain.Clear();
if (root == null)
return;
var t = root.transform;
while (t != null)
{
eventChain.Add(t);
t = t.parent;
}
}
當點擊btn2時,ExecuteHierarchy將Image及其所有父節點添加到Chain列表中,依次調用Excute直到成功為止。