起因

策划提了个需求,具体为给游戏内的所有按钮都加一个0.5s的公CD

思路

笨方法是写个脚本给所有的ButtonToggle挂上,然后控制点击事件的响应
但是这样太麻烦了,要改动的太多了,假如我能拦截所有的点击事件,不就可以统一控制了
于是去查询了一下Unity点击事件的实现
发现都是基于IPointerClickHandler接口,凡是实现了该接口的对象都会触发点击事件

public interface IPointerClickHandler : IEventSystemHandler
{
    void OnPointerClick(PointerEventData eventData);
}

继续查在哪调用的,从EventSystem里面找到了BaseInputModule

private BaseInputModule m_CurrentInputModule;

这玩意就是跟EventSystem挂载一起的StandaloneInputModule继承的基类
然后看StandaloneInputModule里面的代码,全局查找IPointerClickHandler

private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{
    ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
    GameObject eventHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
    if (pointerEvent.pointerClick == eventHandler && pointerEvent.eligibleForClick)
    {
        ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
    }

    if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
    {
        ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
    }

    pointerEvent.eligibleForClick = false;
    pointerEvent.pointerPress = null;
    pointerEvent.rawPointerPress = null;
    pointerEvent.pointerClick = null;
    if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
    {
        ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);
    }

    pointerEvent.dragging = false;
    pointerEvent.pointerDrag = null;
    if (currentOverGo != pointerEvent.pointerEnter)
    {
        HandlePointerExitAndEnter(pointerEvent, null);
        HandlePointerExitAndEnter(pointerEvent, currentOverGo);
    }

    m_InputPointerEvent = pointerEvent;
}

大概就是在释放鼠标的时候会检查一次点击事件,仔细看核心代码就一句

ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);

内部代码

public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
    List<IEventSystemHandler> list = s_HandlerListPool.Get();
    GetEventList<T>(target, list);
    int count = list.Count;
    for (int i = 0; i < count; i++)
    {
        T handler;
        try
        {
            handler = (T)list[i];
        }
        catch (Exception innerException)
        {
            IEventSystemHandler eventSystemHandler = list[i];
            Debug.LogException(new Exception($"Type {typeof(T).Name} expected {eventSystemHandler.GetType().Name} received.", innerException));
            continue;
        }

        try
        {
            functor(handler, eventData);
        }
        catch (Exception exception)
        {
            Debug.LogException(exception);
        }
    }

    int count2 = list.Count;
    s_HandlerListPool.Release(list);
    return count2 > 0;
}

也就是说最后会执行 functor(handler, eventData);,这玩意是前面传过来的,也就是ExecuteEvents.pointerClickHandler,继续看里面

public static EventFunction<IPointerClickHandler> pointerClickHandler => s_PointerClickHandler;

是一个委托,指向了s_PointerClickHandler

private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
    handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}

到此就明了了,所有的点击事件都是通过invoke这个s_PointerClickHandler委托实现的
那我们就重写覆盖这个委托,因为是private的,所以需要用到反射

实现代码

using System.Reflection;
using UnityEngine;
using UnityEngine.EventSystems;

/// <summary>
/// 游戏内所有的按钮点击的共用内置CD
/// </summary>
public class PointerClickCoolDown : MonoBehaviour
{
    /// <summary>
    /// 延迟时间
    /// </summary>
    [SerializeField] float m_CoolDownDuration = 0.5f;
    bool m_EnableSelectable = true;

    void Awake()
    {
        typeof(ExecuteEvents).GetField("s_PointerClickHandler", BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, new ExecuteEvents.EventFunction<IPointerClickHandler>(OnPointerClick));
    }

    void OnPointerClick(IPointerClickHandler handler, BaseEventData eventData)
    {
        PointerEventData pointerEventData = ExecuteEvents.ValidateEventData<PointerEventData>(eventData);
        if (pointerEventData != null)
        {
            if (!m_EnableSelectable)
            {
                return;
            }
            handler.OnPointerClick(pointerEventData);
            m_EnableSelectable = false;
            // 自己实现的一个计时器
            this.StartDelayAction(m_CoolDownDuration, () =>
            {
                m_EnableSelectable = true;
            });
        }
    }
}

到此就实现了全局拦截点击事件,自定义自己想实现的功能

Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐