Unity中的UGUI源码解析之事件系统(7)-输入模块(上)

Unity中的UGUI源码解析之事件系统(7)-输入模块(上)

从今天开始,我们进入事件系统的的最后一部分: 输入模块(InputModules).

输入模块是事件系统的核心, 是真正使用检测和调用回调的地方.

输入模块主要有几个角色, 分别为:

  • BaseInput: UIBehavior类, 对Input模块部分函数的简单封装
  • BaseInputModule: 输入模块的抽象基类, 定义了一些通用操作, 属性和字段
  • PointerInputModule: 处理PointerEvent(指针事件)的输入模块的二级抽象类
  • StandaloneInputModule: 处理PointerEvent(指针事件)的输入模块的具体实现类, 主要处理鼠标事件和触摸事件
  • TouchInputModule: 处理PointerEvent(指针事件)的输入模块的具体实现类, 主要处理触摸事件, 现已弃用

在这里插入图片描述

还有几个零散的类, 用到的时候再介绍.

BaseInput

BaseInput很简单, 首先它继承于UIBehavior, 其次是简单的封装Input模块函数. 它也没有面板属性, 纯粹是一个工具类.

它的作用很简单, 大部分是供输入模块查询鼠标或者触摸或者轴事件的一些基本信息, 本身并不维护数据, 只是一个中转站.

public class BaseInput : UIBehaviour
{
    // 用户键入的当前 IME 组合字符串, 所谓IME也就是输入法, Unity提供内置的输入法, 也提供自定义输入法
    public virtual string compositionString
    {
        get { return Input.compositionString; }
    }

    // 输入法模式: Auto(仅在选中文本字段时启用IME 输入(默认)), On(启用), Off(禁用)
    public virtual IMECompositionMode imeCompositionMode
    {
        get { return Input.imeCompositionMode; }
        set { Input.imeCompositionMode = value; }
    }

    // 输入法输入光标位置
    public virtual Vector2 compositionCursorPos
    {
        get { return Input.compositionCursorPos; }
        set { Input.compositionCursorPos = value; }
    }

    // 是否检测到鼠标
    public virtual bool mousePresent
    {
        get { return Input.mousePresent; }
    }

    // 鼠标点按的按下部分
    public virtual bool GetMouseButtonDown(int button)
    {
        return Input.GetMouseButtonDown(button);
    }

    // 鼠标点按的弹起部分
    public virtual bool GetMouseButtonUp(int button)
    {
        return Input.GetMouseButtonUp(button);
    }

	// 鼠标某个键一直按下(按住)
    public virtual bool GetMouseButton(int button)
    {
        return Input.GetMouseButton(button);
    }

    // 鼠标位置(屏幕坐标)
    public virtual Vector2 mousePosition
    {
        get { return Input.mousePosition; }
    }

    // 鼠标滚动量
    public virtual Vector2 mouseScrollDelta
    {
        get { return Input.mouseScrollDelta; }
    }

    // 当前设备是否支持触摸
    public virtual bool touchSupported
    {
        get { return Input.touchSupported; }
    }

    // 触摸次数
    public virtual int touchCount
    {
        get { return Input.touchCount; }
    }

    // 根据索引获取触摸
    public virtual Touch GetTouch(int index)
    {
        return Input.GetTouch(index);
    }

    // 获取原始的轴数据(未平滑)
    public virtual float GetAxisRaw(string axisName)
    {
        return Input.GetAxisRaw(axisName);
    }

    // 键盘的某个键点按的按下部分
    public virtual bool GetButtonDown(string buttonName)
    {
        return Input.GetButtonDown(buttonName);
    }
}

BaseInputModule

BaseInputModule是输入模块的祖先类, 也是一个抽象类, 不能实例化. 提供一些基本的字段, 属性和函数供子类使用.

它同时需要依赖EventSystem使用.

[RequireComponent(typeof(EventSystem))]
public abstract class BaseInputModule : UIBehaviour
{
    // ...
}

字段和属性

BaseInputModule提供了一些简单的字段和属性.

/// 射线投射结果缓存列表, 避免频繁申请
[NonSerialized] protected List<RaycastResult> m_RaycastResultCache = new List<RaycastResult>();

/// 轴数据, 根据传入的封装事件数据
private AxisEventData m_AxisEventData;

/// 引用EventSystem
private EventSystem m_EventSystem;

/// 基础的事件数据, 用于在一些地方进行初始化变量
private BaseEventData m_BaseEventData;

/// 提供的BaseInput重载, 自定义的Input
protected BaseInput m_InputOverride;

/// 对象身上找到的第一个BaseInput
private BaseInput m_DefaultInput;

/// BaseInput属性
public BaseInput input
{
    get
    {
        // 提供重载机制
        if (m_InputOverride != null)
            return m_InputOverride;

        // 只使用BaseInput, 不使用子类
        if (m_DefaultInput == null)
        {
            var inputs = GetComponents<BaseInput>();
            foreach (var baseInput in inputs)
            {
                // We dont want to use any classes that derrive from BaseInput for default.
                if (baseInput != null && baseInput.GetType() == typeof(BaseInput))
                {
                    m_DefaultInput = baseInput;
                    break;
                }
            }

            // 没有则添加新的
            if (m_DefaultInput == null)
                m_DefaultInput = gameObject.AddComponent<BaseInput>();
        }

        return m_DefaultInput;
    }
}

/// 引用的eventSystem属性
protected EventSystem eventSystem
{
    get { return m_EventSystem; }
}

默认情况下生成的EventSystem对象身上并没有BaseInput组件, 一般都是启动后动态添加.

OnEnable和OnDisable

前面的文章介绍过了, 输入模块在OnEnable和OnDisable时调用EventSystem的更新接口, 在内部将自己从EventSystem管理的输入模块列表中移除.

// BaseInputModule.cs
protected override void OnEnable()
{
    base.OnEnable();
    m_EventSystem = GetComponent<EventSystem>();
    m_EventSystem.UpdateModules();
}

protected override void OnDisable()
{
    m_EventSystem.UpdateModules();
    base.OnDisable();
}

// EventSystem.cs
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);
    }
}

抽象方法和类方法

BaseInputModule提供了一些抽象方法需子类具体实现, 并提供了一些通用的类方法.

// 抽象的事件处理方法, 需要具体子类实现
public abstract void Process();

// 查找第一个投射结果, 需要游戏对象存在, 如果没有则返回一个新建的投射结果
protected static RaycastResult FindFirstRaycast(List<RaycastResult> candidates)
{
    for (var i = 0; i < candidates.Count; ++i)
    {
        if (candidates[i].gameObject == null)
            continue;

        return candidates[i];
    }
    return new RaycastResult();
}

// 根据坐标确定移动方向
protected static MoveDirection DetermineMoveDirection(float x, float y)
{
    return DetermineMoveDirection(x, y, 0.6f);
}

// 根据坐标确定移动方向(上下左右, 未移动)
// moveDeadZone用于最小位移判断
protected static MoveDirection DetermineMoveDirection(float x, float y, float deadZone)
{
    // 忽略数值很小的移动
    // if vector is too small... just return
    if (new Vector2(x, y).sqrMagnitude < deadZone * deadZone)
        return MoveDirection.None;

    
    if (Mathf.Abs(x) > Mathf.Abs(y))
    { // 水平方向(左右)
        if (x > 0)
            return MoveDirection.Right;
        return MoveDirection.Left;
    }
    else
    { // 竖直方向(上下)
        if (y > 0)
            return MoveDirection.Up;
        return MoveDirection.Down;
    }
}

// 找出两个游戏对象共同的父节点
// 通过两重循环
protected static GameObject FindCommonRoot(GameObject g1, GameObject g2)
{
    if (g1 == null || g2 == null)
        return null;

    var t1 = g1.transform;
    while (t1 != null)
    {
        var t2 = g2.transform;
        while (t2 != null)
        {
            if (t1 == t2)
                return t1.gameObject;
            t2 = t2.parent;
        }
        t1 = t1.parent;
    }
    return null;
}

查询相关函数

BaseInputModule提供了一些查询方法供子类和外部使用.

// 根据传入的数据, 封装轴事件数据
protected virtual AxisEventData GetAxisEventData(float x, float y, float moveDeadZone)
{
    if (m_AxisEventData == null)
        m_AxisEventData = new AxisEventData(eventSystem);

    m_AxisEventData.Reset();
    m_AxisEventData.moveVector = new Vector2(x, y);
    m_AxisEventData.moveDir = DetermineMoveDirection(x, y, moveDeadZone);
    return m_AxisEventData;
}

// 获取基础的事件数据, 用于在一些地方进行初始化变量
protected virtual BaseEventData GetBaseEventData()
{
    if (m_BaseEventData == null)
        m_BaseEventData = new BaseEventData(eventSystem);

    m_BaseEventData.Reset();
    return m_BaseEventData;
}

// 给定id的指针是否位于某个对象区域
public virtual bool IsPointerOverGameObject(int pointerId)
{
    return false;
}

// 本输入模块是否应该激活
public virtual bool ShouldActivateModule()
{
    return enabled && gameObject.activeInHierarchy;
}

// 反激活当前模块
public virtual void DeactivateModule()
{}

// 激活当前模块
public virtual void ActivateModule()
{}

// 更新当前模块
public virtual void UpdateModule()
{}

// 当前模块模块是否受支持
public virtual bool IsModuleSupported()
{
    return true;
}

处理指针的离开和进入事件

// 向上一个[进入对象](也就是当前指针事件数据中存储的[进入对象])和其所有父节点发送离开事件
// 向当前的新的[进入对象]发送进入事件和其所有父节点发送进入事件
// 如果两个对象存在共同的父节点, n
protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
	// 如果没有新的[进入对象](指针游离在所有的对象之外), 或者当前的指针事件的[进入对象]被删除
    // 那么就简单的向所有悬停对象(之前文章介绍过, 就是当前进入对象和其所有父节点链条上所有节点对象)发送离开事件, 下面会介绍
    if (newEnterTarget == null || currentPointerData.pointerEnter == null)
    {
        for (var i = 0; i < currentPointerData.hovered.Count; ++i)
            ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);

        currentPointerData.hovered.Clear();

        // 如果没有新的[进入对象], 将当前的事件中的[进入对象]也清空
        // 因为没有当前对象, 所以不需要发送进入事件, 直接返回
        if (newEnterTarget == null)
        {
            currentPointerData.pointerEnter = newEnterTarget;
            return;
        }
    }

    // 如果[进入对象]存在且未变化, 则啥也不做
    if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
        return;

    // -------------------------------------------------------
    // 下面的区域处理的是一定有新的[进入对象], 但是不一定有上一个[进入对象]
    
    // 查找上一个[进入对象]和新的[进入对象]共同的父节点
    GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);

    // 如果有上一个[进入对象], 向"上一个[进入对象]一直向上直到共同父节点(或者没有)链条上所有的节点发送离开事件, 且不包含共同的父节点"
    if (currentPointerData.pointerEnter != null)
    {
        Transform t = currentPointerData.pointerEnter.transform;

        while (t != null)
        {
            // 遇到共同父节点停止
            if (commonRoot != null && commonRoot.transform == t)
                break;

            ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
            currentPointerData.hovered.Remove(t.gameObject);
            t = t.parent;
        }
    }

    // 向新的[进入对象]一直到共同父节点(或者没有)链条上所有节点发送进入事件
    currentPointerData.pointerEnter = newEnterTarget;
    if (newEnterTarget != null)
    {
        Transform t = newEnterTarget.transform;

        // 遇到共同父节点停止
        while (t != null && t.gameObject != commonRoot)
        {
            ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
            currentPointerData.hovered.Add(t.gameObject);
            t = t.parent;
        }
    }
}

上面的代码说的有点迷糊, 我重构之后的代码可能会好理解一点:

// 向所有的悬停对象发送离开事件
private static void ExecutePointerExitForHover(PointerEventData currentPointerData, bool isClearPointerEnter) {
    if (currentPointerData.hovered.Count <= 0) return;
    
    foreach (var go in currentPointerData.hovered)
    {
        ExecuteEvents.Execute(go, currentPointerData, ExecuteEvents.pointerExitHandler);
    }

    currentPointerData.hovered.Clear();

    if (isClearPointerEnter) currentPointerData.pointerEnter = null;
}

// 向target层级链条上所有对象发送离开事件
private static void ExecutePointerExitEventForChain(GameObject target, PointerEventData currentPointerData, GameObject stopGameObject = null)
{
    var t = target.transform;
    while (t != null)
    {
        if (stopGameObject && stopGameObject == t.gameObject) break;

        ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
        currentPointerData.hovered.Remove(t.gameObject);
        t = t.parent;
    }
}

// 向target层级链条上所有对象发送进入事件
private static void ExecutePointerEnterEventForChain(GameObject target, PointerEventData currentPointerData, GameObject stopGameObject = null)
{
    var t = target.transform;
    while (t != null)
    {
        if (stopGameObject && stopGameObject == t.gameObject) break;

        ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
        currentPointerData.hovered.Add(t.gameObject);
        t = t.parent;
    }
}

protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
    // [进入对象]未变化, 直接返回
    if (newEnterTarget && newEnterTarget == currentPointerData.pointerEnter) return;

    // 没有新的[进入对象], 向所有悬停对象发送离开事件(因为此时指针游离于所有对象之外), 直接返回(因为不用处理进入事件)
    if (newEnterTarget == null) {
        ExecutePointerExitForHover(currentPointerData, true);
        return;
    }

    var commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);
    if (currentPointerData.pointerEnter != null)
    { // 有上一个[进入对象], 向其层级链条上所有对象发送离开事件
        ExecutePointerExitEventForChain(currentPointerData.pointerEnter, currentPointerData, commonRoot);
    } else { // // 没有上一个[进入对象], 向所有悬停对象发送离开事件
        ExecutePointerExitForHover(currentPointerData, false);
    }

	// 向新的[进入对象]层级链条上所有对象发送进入事件
    currentPointerData.pointerEnter = newEnterTarget;
    ExecutePointerEnterEventForChain(newEnterTarget, currentPointerData, commonRoot);
}

总结

今天简单介绍了输入模块, 并对BaseInput和BaseInputModule做了详细的分析, 剩余的部分会在下一篇文章给出.

希望对大家有所帮助.

猜你喜欢

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