Unity中的UGUI源码解析之事件系统(2)-EventSystem组件

Unity中的UGUI源码解析之事件系统(2)-EventSystem组件

今天介绍我们的第一个主角: EventSystem.

EventSystem在整个事件系统中处于中心, 相当于事件系统的管理器, 负责协调各个模块.

其本身是一个MonoBehavior, 继承于UI模块统一的父类UIBehaviour.

在一个场景中添加任意的UI控件, 会默认添加一个EventSystem对象, 身上挂载EventSystem组件和一个StandaloneInputModule(独立输入模块)组件. 各种事件要生效, 场景中至少需要一个EventSystem. 但是也可以存在多个, 但是只会生效一个.

Unity默认会从挂载EventSystem组件的对象上查找输入模块并添加到其内部列表中进行管理和配合使用.

主要作用

从官方文档和源码上看, EventSystem主要负责维护输入模块, 并提供一些配置参数给其它事件模块使用.

所以在EventSystem内部维护了一个BaseInputModule列表, 并保存了当前生效的输入模块. 在生命周期函数Update中更新每个输入模块.

同时持有了一个静态列表, 保存了所有的EventSystem, 每次取列表第一个来使用.

在生命周期函数OnEnable/OnDisable中添加和移除EventSystem.

UIBehaviour

大部分UI元素都是MonoBehavior, 和我们自己写的组件没有太大的区别. 只是Unity没有选择直接继承MonoBehavior, 而是在上面抽象了一层抽象类UIBehaviour, 封装了和默认实现了MonoBehavior部分生命周期函数(如protected virtual void Awake):

  • Awake
  • OnEnable
  • Start
  • OnDisable
  • OnDestroy
  • IsActive(自身)
  • Editor: OnValidate, Reset
  • OnRectTransformDimensionsChange
  • OnBeforeTransformParentChanged
  • OnTransformParentChanged
  • OnDidApplyAnimationProperties
  • OnCanvasGroupChanged
  • OnCanvasHierarchyChanged
  • IsDestroyed(自身)

大部分接口来自于MonoBehavior, 实际调用在C/C++层, 很遗憾我们看不到. 猜测应该是围绕Canvas触发.

主要属性, 字段和方法

下面对EventSystem的公开属性和方法做简单说明. 内容不是很多, 照官方描述, EventSystem更多是一个管理器的角色, 负责协调和中转, 自身并不主要负责具体的工作.

面板属性

EventSystem的面板属性比较少, 一般也不需要使用和修改.

  • First Selected: 首次选中的对象, 运行之前可以指定一个对象, 在运行之后立即选中该对象, 比如输入框
  • Send Navigation Events: 是否允许发送导航事件, 我们在下面单独介绍
  • Drag Threshold: 拖拽操作的容限区域(以像素为单位), 从官方解释上看, 貌似应该是判断拖拽的最小位移, 一般保持默认即可

导航事件

导航事件指的是: 移动(Move), 提交(Submit), 取消(Cancel).

这几个事件在Edit->ProjectSettings->Input中可以查找和配置, 其中移动事件就是Horizontal或者Vertical, 其余两个事件按照名字对应即可.

在这里插入图片描述

对应的事件声明和测试代码如下(也可以使用EventTrigger):

// IMoveHandler, ISubmitHandler, ICancelHandler

public class NavigationEventTest : MonoBehaviour, ISubmitHandler, ICancelHandler, IMoveHandler {
	public void OnSubmit(BaseEventData eventData) {
		Debug.LogError("eventData: " + eventData);
	}
	public void OnCancel(BaseEventData eventData) {
		Debug.LogError("onCancel: " + eventData);
	}
	public void OnMove(AxisEventData eventData) {
		Debug.LogError("onMove: " + eventData);
	}
}

我是将脚本挂载到一个InputField上, 输入完成后按两下回车就会触发提交事件, 按两下ESC就会触发取消事件, 按一下回车再按上下左右键就会触发移动. 有兴趣的同学可以自行尝试.

输入模块(BaseInputModule)

事件系统的主要工作由输入模块具体负责, EventSystem起着维护和驱动输入模块的职责.

下面我把涉及输入模块相关的代码聚集到一起, 方便观察.

private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();

/// 当前生效的输入模块
private BaseInputModule m_CurrentInputModule;

// 收集当前对象上所有的输入模块, 由BaseInputModule->OnEnable/OnDisable触发
// 也就是说在BaseInputModule可用和不可用时会通过触发此方法来添加或者移除自己
public void UpdateModules()
{
    GetComponents(m_SystemInputModules);
    for (int i = m_SystemInputModules.Count - 1; i >= 0; i--)
    {
        if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
            continue;

        m_SystemInputModules.RemoveAt(i);
    }
}

// 触发输入模块的更新, 由当前激活的EventSystem在生命周期函数Update中触发
// 注意这里的只是更新数据, 而不过进行真正的事件处理, 真正的事件处理只有当前的输入模块才处理, 也就是说同时只有一个输入模块会处理事件
private void TickModules()
{
    for (var i = 0; i < m_SystemInputModules.Count; i++)
    {
        if (m_SystemInputModules[i] != null)
            m_SystemInputModules[i].UpdateModule();
    }
}

// 切换当前生效的输入模块
private void ChangeEventModule(BaseInputModule module)
{
    if (m_CurrentInputModule == module)
        return;

    if (m_CurrentInputModule != null)
        m_CurrentInputModule.DeactivateModule();

    if (module != null)
        module.ActivateModule();
    m_CurrentInputModule = module;
}

// 更新输入模块, 切换当前激活的输入模块并处理事件
protected virtual void Update()
{
    // 只有一个EventSystem能够处理事件
    if (current != this)
        return;
    
    // 更新输入模块
    TickModules();

   	// 切换输入模块的当帧不进行事件处理
    bool changedModule = false;
    
    // 切换当前激活的输入模块
    for (var i = 0; i < m_SystemInputModules.Count; i++)
    {
        var module = m_SystemInputModules[i];
        if (module.IsModuleSupported() && module.ShouldActivateModule())
        {
            if (m_CurrentInputModule != module)
            {
                ChangeEventModule(module);
                changedModule = true;
            }
            break;
        }
    }

    // 没有找到当前激活的输入模块, 则设置第一个有效的为当前
    // no event module set... set the first valid one...
    if (m_CurrentInputModule == null)
    {
        for (var i = 0; i < m_SystemInputModules.Count; i++)
        {
            var module = m_SystemInputModules[i];
            if (module.IsModuleSupported())
            {
                ChangeEventModule(module);
                changedModule = true;
                break;
            }
        }
    }

    // 进行事件处理
    if (!changedModule && m_CurrentInputModule != null)
        m_CurrentInputModule.Process();
}

注释应该比较详细了, 我就不再另外说明.

总之就是维护所有的输入模块, 然后在适当的时机切换当前激活的输入模块, 最后在每帧的Update中更新输入模块和使用当前输入模块处理事件.

其它属性和方法

ToString

在运行的状态下, 我们可以通过点击EventSystem对象, 然后在Inspector面板看到当前事件系统的各种状态. 如图:

在这里插入图片描述

移动, 点击鼠标等可以看到数据变化.

这是通过EventSystemPointerInputModuleTostring方法来完成的.

// EventSystem
public override string ToString()
{
    var sb = new StringBuilder();
    sb.AppendLine("<b>Selected:</b>" + currentSelectedGameObject);
    sb.AppendLine();
    sb.AppendLine();
    sb.AppendLine(m_CurrentInputModule != null ? m_CurrentInputModule.ToString() : "No module");
    return sb.ToString();
}

// PointerInputModule
public override string ToString()
{
    var sb = new StringBuilder("<b>Pointer Input Module of type: </b>" + GetType());
    sb.AppendLine();
    foreach (var pointer in m_PointerData)
    {
        if (pointer.Value == null)
            continue;
        sb.AppendLine("<B>Pointer:</b> " + pointer.Key);
        sb.AppendLine(pointer.Value.ToString());
    }
    return sb.ToString();
}

简单说的就是打印两个组件的详情而已.

OnEnable/OnDisable

// 通过类字段来持有所有的EventSystem
private static List<EventSystem> m_EventSystems = new List<EventSystem>();

// 只有第一个EventSystem生效
public static EventSystem current
{
    get { return m_EventSystems.Count > 0 ? m_EventSystems[0] : null; }
    set
    {
        int index = m_EventSystems.IndexOf(value);

        if (index >= 0)
        {
            m_EventSystems.RemoveAt(index);
            m_EventSystems.Insert(0, value);
        }
    }
}

// 在每个EventSystem可用时添加到列表
protected override void OnEnable()
{
    base.OnEnable();
    m_EventSystems.Add(this);
}

// 在每个EventSystem不可用时从列表移除
protected override void OnDisable()
{
    if (m_CurrentInputModule != null)
    {
        m_CurrentInputModule.DeactivateModule();
        m_CurrentInputModule = null;
    }

    m_EventSystems.Remove(this);

    base.OnDisable();
}

SetSelectedGameObject

设置当前选择对象和发送事件(反选和选中)

// 当前选中对象
private GameObject m_CurrentSelected;

// 选中保护, 因为在反选和选中的过程中会触发事件, 需要在这个过程中对状态进行保护
private bool m_SelectionGuard;
public bool alreadySelecting
{
    get { return m_SelectionGuard; }
}

public void SetSelectedGameObject(GameObject selected, BaseEventData pointer)
{
    if (m_SelectionGuard)
    {
        Debug.LogError("Attempting to select " + selected +  "while already selecting an object.");
        return;
    }

    m_SelectionGuard = true;
    if (selected == m_CurrentSelected)
    {
        m_SelectionGuard = false;
        return;
    }

    // Debug.Log("Selection: new (" + selected + ") old (" + m_CurrentSelected + ")");
    ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
    m_CurrentSelected = selected;
    ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
    m_SelectionGuard = false;
}

Raycaster

提供了一个射线检测和排序的方法供其它模块调用, 我们将在使用时在介绍, 这里就不贴代码了.

private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
{
    //...
}

private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
    //...
}

其它

EventSystem还提供了部分其它模块使用的属性和方法, 不是主要部分, 这里就不一一列举了, 我们在使用到的时候再顺便介绍.

总结

今天跟着官方文档和源码对EventSystem做了一个比较详细的介绍, 我们了解和印证了官方的说法.

EventSystem的确只是一个事件系统总的管理者和协调者, 本身的内容并不复杂, 主要维护输入模块, 并在合适的时候刷新和驱动输入模块处理具体的事件, 同时自身也会在标记当前选择对象时发送对应的事件, 最后还提供了一部分供其它模块使用的属性和方法.

下一篇文章我们会介绍在事件系统中用于封装和传递的基础数据结构: EventData, 这是后续模块的基础.

好了, 今天的内容就是这些, 希望对大家有所帮助.

猜你喜欢

转载自blog.csdn.net/woodengm/article/details/123416615