Event system of UGUI source code analysis in Unity (4)-ExecuteEvents

Event system of UGUI source code analysis in Unity (4)-ExecuteEvents

Introducing the message system today: ExecuteEvents .

The message system implemented by Unity is very simple, a static class plus a bunch of interfaces, when processing events, the objects that need to be processed are dynamically obtained, and there is almost no state maintenance. Although each event needs to be obtained, part of the performance will be lost, but due to There are generally not too many components on each object, the performance loss is almost negligible, and the advantage is that most of the state management is removed, which greatly reduces the difficulty of maintenance and understanding.

EventInterfaces

Unity defines time through the interface (Interface). All interfaces that inherit the IEventSystemHandler interface, or classes that implement this interface, can be regarded as an event by Unity.

IEventSystemHandler and various event interfaces are defined in EventInterfaces , and the event data types required by the event are in brackets:

  • public interface IEventSystemHandler(PointerEventData): ancestor interface of all events
  • public interface IPointerEnterHandler(PointerEventData): Entry event, that is, the mouse enters the object area for the first time
  • public interface IPointerExitHandler(PointerEventData): Leaving event, that is, the mouse enters and leaves the object area for the first time
  • public interface IPointerDownHandler(PointerEventData): press event
  • public interface IPointerUpHandler(PointerEventData): lift event
  • public interface IPointerClickHandler(PointerEventData): Click event, that is, press and lift for a short time
  • public interface IInitializePotentialDragHandler(PointerEventData): After the draggable object is found, the event before the real start of dragging is sent only once during the entire dragging process
  • public interface IBeginDragHandler(PointerEventData): start dragging event, only sent once during the whole dragging process
  • public interface IDragHandler(PointerEventData): drag event
  • public interface IEndDragHandler(PointerEventData): Stop dragging event, only sent once during the whole dragging process
  • public interface IDropHandler(PointerEventData): Drag and release event, it is required that the mouse is still inside the dragged object when it is released, if the click event ( IPointerClickHandler) is to be processed at the same time, this event cannot be triggered
  • public interface IScrollHandler(PointerEventData): scroll event
  • public interface IUpdateSelectedHandler(BaseEventData): Update the selected event, sent almost every frame
  • public interface ISelectHandler(BaseEventData): Toggle selection event, sent to the selected object every time the selection is switched
  • public interface IDeselectHandler(BaseEventData): Unselect event, sent to the unselected object every time the selection is switched
  • public interface IMoveHandler(AxisEventData): navigation move event
  • public interface ISubmitHandler(BaseEventData): navigation submit event
  • public interface ICancelHandler(BaseEventData): Navigation cancel event

EventTrigger

As mentioned in the previous article, if we want to receive specific events, handle inheritance or implement event interfaces, we can also use EventTrigger to achieve the goal. For details, please refer to the article about the overview of the event system.

We can even combine the two methods together, such as the following example:

using UnityEngine;
using UnityEngine.EventSystems;

public class EventTriggerTest : MonoBehaviour, IPointerClickHandler, IBeginDragHandler {
    private void Start() {
        var eventTrigger = GetComponent<EventTrigger>();
        if (!eventTrigger) eventTrigger = gameObject.AddComponent<EventTrigger>();

        var entry1 = new EventTrigger.Entry();
        entry1.eventID = EventTriggerType.PointerClick;
        entry1.callback.AddListener((eventData => {
            var pointerEventData = eventData as PointerEventData;
            Debug.LogError("----- PointerClick");
        }));
        
        var entry2 = new EventTrigger.Entry();
        entry2.eventID = EventTriggerType.BeginDrag;
        entry2.callback.AddListener((eventData => {
            var pointerEventData = eventData as PointerEventData;
            Debug.LogError("----- BeginDrag");
        }));
        
        eventTrigger.triggers.Add(entry1);
        eventTrigger.triggers.Add(entry2);
    }
    public void OnPointerClick(PointerEventData eventData) {
        Debug.LogError("##### PointerClick");
    }
    public void OnBeginDrag(PointerEventData eventData) {
        Debug.LogError("##### OnBeginDrag");
    }
}

//-----------------------------------
// 输出
// ##### OnBeginDrag
// ----- BeginDrag
// ##### PointerClick
// ----- PointerClick

The various parts of EventTrigger are described in detail below .

EventTriggerType

Corresponding to the previous interfaces in EventTriggerType, various event types are defined to pass and identify events between modules:

public enum EventTriggerType
{
    PointerEnter = 0,
    PointerExit = 1,
    PointerDown = 2,
    PointerUp = 3,
    PointerClick = 4,
    Drag = 5,
    Drop = 6,
    Scroll = 7,
    UpdateSelected = 8,
    Select = 9,
    Deselect = 10,
    Move = 11,
    InitializePotentialDrag = 12,
    BeginDrag = 13,
    EndDrag = 14,
    Submit = 15,
    Cancel = 16
}

EventTrigger.TriggerEvent

Use UnityEvent to encapsulate event data, and AddListeneradd event processing callbacks.

public class TriggerEvent : UnityEvent<BaseEventData> {}

EventTrigger.Entry

Combining event types and event handling callbacks.

public class Entry
{
    public EventTriggerType eventID = EventTriggerType.PointerClick;
    public TriggerEvent callback = new TriggerEvent();
}

EventTrigger

EventTrigger itself is a MonoBehavior , not inherited from UIBehavior , so it can also be used for message processing of non-UI modules.

EventTrigger itself is very simple, just by implementing the various interfaces introduced above, and then when triggering various events, use the type of EventTrigger.Entry encapsulated to determine the callback for event processing.

public class EventTrigger :
        MonoBehaviour,
        IPointerEnterHandler,
        IPointerExitHandler,
        IPointerDownHandler,
        IPointerUpHandler,
        IPointerClickHandler,
        IInitializePotentialDragHandler,
        IBeginDragHandler,
        IDragHandler,
        IEndDragHandler,
        IDropHandler,
        IScrollHandler,
        IUpdateSelectedHandler,
        ISelectHandler,
        IDeselectHandler,
        IMoveHandler,
        ISubmitHandler,
        ICancelHandler
        {}

Compared with the method of directly implementing the event interface of the business class, EventTrigger greatly improves the flexibility of the code. For the same event, multiple processing callbacks can be added, and processing callbacks can also be dynamically deleted.

EventTrigger has only one field, which is used to store encapsulated entries, and each entry is abstracted as a delegate (delegate), which means that EventTrigger itself does not process events, but only delegates processing: private List<Entry> m_Delegates;this delegate container is lazily loaded and created when it is acquired.

public List<Entry> triggers
{
    get
    {
        if (m_Delegates == null)
            m_Delegates = new List<Entry>();
        return m_Delegates;
    }
    set { m_Delegates = value; }
}

private void Execute(EventTriggerType id, BaseEventData eventData)
{
    for (int i = 0, imax = triggers.Count; i < imax; ++i)
    {
        var ent = triggers[i];
        if (ent.eventID == id && ent.callback != null)
            ent.callback.Invoke(eventData);
    }
}

public virtual void OnPointerEnter(PointerEventData eventData)
{
    Execute(EventTriggerType.PointerEnter, eventData);
}

public virtual void OnPointerExit(PointerEventData eventData)
{
    Execute(EventTriggerType.PointerExit, eventData);
}

//.............

The codes are similar, here are just some representative codes.

After the first OnPointerEntertrigger, call Execute, traverse all agents, find an agent that matches the event type, and then perform event callback processing.

Of course, we can also inherit EventTrigger to customize our own logic for certain event types, which is also the suggestion Unity gave us.

ExecuteEvents

The event system distributes events through ExecuteEvents , that is to say, ExecuteEvents is a message system . Although its code size and complexity seem to be "not worthy" of the so-called "system".

ExecuteEvents itself is a static class, divided into the following main parts.

Provide delegate encapsulation event handler function

For all event types mentioned above, delegate properties are provided to encapsulate event triggering operations. The following code is also an extraction part, and other codes are similar.

public static class ExecuteEvents
{
    // 通过泛型声明委托函数, 封装处理器, 事件数据
    public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData);

    // 转换事件数据类型
    public static T ValidateEventData<T>(BaseEventData data) where T : class
    {
        if ((data as T) == null)
            throw new ArgumentException(String.Format("Invalid type: {0} passed to event expecting {1}", data.GetType(), typeof(T)));
        return data as T;
    }
    
    // 事件处理函数, 包含事件处理器, 事件处理回调, 事件数据
    private static void Execute(IPointerEnterHandler handler, BaseEventData eventData)
    {
        handler.OnPointerEnter(ValidateEventData<PointerEventData>(eventData));
    }

    // 将事件处理函数转换为委托字段
    private static readonly EventFunction<IPointerEnterHandler> s_PointerEnterHandler = Execute;

    // 委托属性
    public static EventFunction<IPointerEnterHandler> pointerEnterHandler
    {
        get { return s_PointerEnterHandler; }
    }
}

Get an event handler from an object

The second big part is the core of the messaging system, getting event handlers from objects.

The general idea is to collect all valid components on the object, and then use these components as event handlers to handle specific events.

The so-called effective component is to implement the IEventSystemHandler interface, and can handle the specified event (implement the specified event interface, such as IPointerClickHandler ).

At the same time, it is also a component that can be enabled or disabled ( Behaviour ). Unity uses this property ( isActiveAndEnabled ) to identify whether the component handles events.

So by default, event processing can only be ignored by disabling components. Of course, we can decide whether to actually process events in the business layer through secondary processing after the message system is distributed. The implementation of Unity does not care about these, and only guarantees the most basic function, and leave the rest to ourselves.

// 判断组件是否有效
// 组件必须事件特定事件接口
// 组件必须处于可用状态
private static bool ShouldSendToComponent<T>(Component component) where T : IEventSystemHandler
{
    var valid = component is T;
    if (!valid)
        return false;

    var behaviour = component as Behaviour;
    if (behaviour != null)
        return behaviour.isActiveAndEnabled;
    return true;
}

// 获取对象身上所有满足条件的事件处理器
private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
{
    // Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");
    if (results == null)
        throw new ArgumentException("Results array is null", "results");

    if (go == null || !go.activeInHierarchy)
        return;

    var components = ListPool<Component>.Get();
    go.GetComponents(components);
    for (var i = 0; i < components.Count; i++)
    {
        if (!ShouldSendToComponent<T>(components[i]))
            continue;

        // Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));
        results.Add(components[i] as IEventSystemHandler);
    }
    ListPool<Component>.Release(components);
    // Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");
}

Get the first event handler (handler) that meets the condition from the object node level

// 处理器的对象池
private static readonly ObjectPool<List<IEventSystemHandler>> s_HandlerListPool = new ObjectPool<List<IEventSystemHandler>>(null, l => l.Clear());


// 判断指定对象是否可以可以处理特定事件
// 通过对象身上是否存在满足条件的事件处理器
public static bool CanHandleEvent<T>(GameObject go) where T : IEventSystemHandler
{
    var internalHandlers = s_HandlerListPool.Get();
    GetEventList<T>(go, internalHandlers);
    var handlerCount = internalHandlers.Count;
    s_HandlerListPool.Release(internalHandlers);
    return handlerCount != 0;
}

// 从指定节点顺着父节点一直往上找, 一直找到一个可以处理特定事件的对象
public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
{
    if (root == null)
        return null;

    Transform t = root.transform;
    while (t != null)
    {
        if (CanHandleEvent<T>(t.gameObject))
            return t.gameObject;
        t = t.parent;
    }
    return null;
}

Because the handler is acquired almost every frame, Unity uses the object pool technology to reduce performance loss. This object pool has been mentioned in the previous article, you can refer to here .

event distribution

// 向对象身上所有满足要求的处理指定事件的处理器发送事件
// 使用对象池获取handler
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;
}

private static readonly List<Transform> s_InternalTransformList = new List<Transform>(30);

// 收集对象和其所有父节点
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;
    }
}

// 向对象身上和所有父节点身上的所有满足要求的处理指定事件的处理器发送事件[遇到第一个满足要求的对象后中断]
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;
}

use

ExecuteEvents.Execute(gameObject, pointerData, ExecuteEvents.pointerEnterHandler);
ExecuteEvents.ExecuteHierarchy(gameObject, pointerEvent, ExecuteEvents.dropHandler);

Some events only need to be handled by the object itself, while others need to be handled by the entire lineage level of the object. We will introduce them separately in the input module later.

Summarize

Today we introduced the very important message definition and message system part of the event system, and our understanding of the entire event system has entered a new level.

At the same time, I have to sigh with emotion here. After reading so many Unity source codes, I gradually have a deeper understanding of Unity's design philosophy, especially the C# layer. Most of the designs are relatively simple, which is worth learning.

I feel that Unity is more advocating simplicity and maximum openness (of course, not open source is what people have to eat, understandable), they basically do not do nanny functions, simplify the framework code as much as possible, and hand over the customizable parts to client.

Now many frameworks are seriously coupled with business, resulting in low reusability and difficult to promote. It still needs a lot of effort in framework design. Of course, my own framework has almost the same problem, so I need to keep learning and making progress. . I hope everyone will encourage each other.

Well, that's it for today, I hope it can be helpful to everyone.

Guess you like

Origin blog.csdn.net/woodengm/article/details/123519471