Unity UGUI源码解析(二) InputModule

一. 引言

上一节我们在讲EventSystem类中的某些函数细节的时候,经常会讨论到xxxInputModule这个输入模块,今天就来仔细讲一下xxxInputModule到底做了些什么。
附上UGUI源码

二. BaseInputModule

在具体介绍输入模块之前,我们先要知道输入模块的结构构成是如何的。

输入事件捕获模块由四个类构成:BaseInputModulePointerInputModuleStandaloneInputModuleTouchInputModule
其中BaseInputModule是抽象基类,为子类提供必须的空接口和基本变量。我们先来具体看一下吧。

首先开头部分

[RequireComponent(typeof(EventSystem))]

说明BaseInputModule是依赖于EventSystem这个组件的,可以回看上一章讲EventSystem类时,在Unity创建Canvas会自动生成一个EventSystem的物体,其身上有EventSystem脚本和StandaloneInputModule脚本。

接下来举了个例子,用来表示我们可以通过创建自己的InputModule继承自BaseInputModule,用来引发某个事件并将此事件传递给某物体来处理。

    public class MyInputModule : BaseInputModule
    {
        public GameObject m_TargetObject;
    
        public override void Process()
         
            if (m_TargetObject == null)
                return;
            ExecuteEvents.Execute (m_TargetObject, new BaseEventData (eventSystem), ExecuteEvents.moveHandler);
        }
    }

其中重写的Process()函数在BaseInputModule中是抽象函数,无具体功能实现,用于表示处理模块中事件的当前瞬间。

ExecuteEvents.Execute()则是将一个move事件传递给了m_TargetObject
ExecuteEvents会在日后再开一章具体讲解(挖坑呗)…

说了那么多其实还没开始介绍BaseInputModule下到底有哪些变量和函数…先给大家一个概念,知道BaseInputModule大概做些啥,我们接下来继续

声明的变量其实都是一些基本用来接收事件或者接收输入用的,这里大家自行看下源码即可。

我们看到OnEnable()和OnDisable()两个函数中,里面每次触发时都会调用EventSystem类中的UpdateModules(),用于更新当前模块列表,剔除不活跃的模块,具体请看EventSystem类

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

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

接下来就开始讲有用的函数了

    /// <summary>
    /// 返回第一个合法的RaycastResult
    /// </summary>
    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();
    }

FindFirstRaycast()用于返回第一个RaycastResult,接收射线检测到的物体各类信息。
主要在PointerInputModule中有被调用进行数据处理。

    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;
    }

FindCommonRoot()用于给定两个物体,返回他们最近共同的根物体,这不就是二叉树找两节点的最近公共节点的方法么…

接下来的HandlePointerExitAndEnter()稍微有点复杂,主要用于当一个新的进入目标物体被发现,处理发送相应的进入和退出事件,截取了函数中关键内容,建议看下源码自行体会理解,代码中附带了一些自己的理解注释

    GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);

    // and we already an entered object from last time
    if (currentPointerData.pointerEnter != null)
    {
         
        // 发送退出事件的调用给t.gameobject到commonRoot这条路径上所有的物体(不包括commonRoot)
        Transform t = currentPointerData.pointerEnter.transform;

        while (t != null)
        {
            // if we reach the common root break out!
            if (commonRoot != null && commonRoot.transform == t)
                break;

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


    // 发送进入事件的调用给newEnterTarget到commonRoot这条路径上所有的物体(不包括commonRoot)
    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;
        }
    }
    public virtual bool IsPointerOverGameObject(int pointerId)
    {
        return false;
    }

BaseInputModule中定义了IsPointerOverGameObject()传入指针id判断是否放在GameObject上,具体实现在PointerInputModule中实现,待会会讲。

剩余的就是对模块的一些激活和失效等函数定义了虚方法,由派生类PointerInputModule去实现。

三. PointerInputModule

PointerInputModule继承自BaseInputModule,在其基础上扩展了对与点位的输入逻辑,增加了输入类型和状态。而且PointerInputModule也是抽象类,供StandaloneInputModule和TouchInputModule继承。

首先定义了鼠标指针的id,其次建立了指针id与PointerEventData数据处理的字典,以及一些对字典的添加与删除函数 GetPointerData()和RemovePointerData()

        // 建立指针id与数据的字典
        protected Dictionary<int, PointerEventData> m_PointerData = new Dictionary<int, PointerEventData>();

        // 根据id与bool create决定是否创建PointerEventData对象并建立字典映射
        protected bool GetPointerData(int id, out PointerEventData data, bool create)
        {
            if (!m_PointerData.TryGetValue(id, out data) && create)
            {
                data = new PointerEventData(eventSystem)
                {
                    pointerId = id,
                };
                m_PointerData.Add(id, data);
                return true;
            }
            return false;
        }

        // 从字典中移除PointerEventData
        protected void RemovePointerData(PointerEventData data)
        {
            m_PointerData.Remove(data.pointerId);
        }
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)

GetTouchPointerEventData()通过给定触摸输入信息input来返回PointerEventData信息并且得知当前是指针是按下还是释放状态.

    /// <summary>
    /// Copy one PointerEventData to another.
    /// </summary>
    protected void CopyFromTo(PointerEventData @from, PointerEventData @to)
    {
        @to.position = @from.position;
        @to.delta = @from.delta;
        @to.scrollDelta = @from.scrollDelta;
        @to.pointerCurrentRaycast = @from.pointerCurrentRaycast;
        @to.pointerEnter = @from.pointerEnter;
    }

CopyFromTo()是对PointerEventData数据对象的拷贝

接下来创建了两个类用于定义输入状态的类型和控制 ButtonStateMouseState,这里就不细讲了,有兴趣的自行查看

来看一个上次在EventSystem没有细讲的IsPointerOverGameObject(),也是在Unity中会经常用到的

    public override bool IsPointerOverGameObject(int pointerId)
    {
        var lastPointer = GetLastPointerEventData(pointerId);
        if (lastPointer != null)
            return lastPointer.pointerEnter != null;
        return false;
    }

这个函数就是在EventSystem中,通过变量m_CurrentInputModule,可以得知是哪个模块正在处理事件,其次再调用IsPointerOverGameObject(),传入一个指针id,进而判断该指针id是否落在物体上
其中的函数GetLastPointerEventData()代码如下,其中的GetPointerData()就是开头介绍PointerInputModule中,根据指针id来获取到字典中相应PointerEventData数据的函数。
这个data数据里就包含了是哪个物体等各种位置信息。真的是一环套一环…这种编码思想值得学习…

    protected PointerEventData GetLastPointerEventData(int id)
    {
        PointerEventData data;
        GetPointerData(id, out data, false);
        return data;
    }

四. StandaloneInputModule

StandaloneInputModule模块主要是针对鼠标/键盘/控制器的输入。 继承自PointerInputModule。

直接看代码!

    private float m_PrevActionTime;    // 记录上一个动作的时间
    private Vector2 m_LastMoveVector;  // 记录上一个移动的方向向量
    private int m_ConsecutiveMoveCount = 0;  // 连续移动次数

    private Vector2 m_LastMousePosition;   // 记录上一次鼠标位置
    private Vector2 m_MousePosition;       // 记录当前鼠标位置

    private GameObject m_CurrentFocusedGameObject;  // 记录当前聚焦的物体

    private PointerEventData m_InputPointerEvent;   // 输入事件
    [SerializeField]
    private string m_HorizontalAxis = "Horizontal";

    /// <summary>
    /// Name of the vertical axis for movement (if axis events are used).
    /// </summary>
    [SerializeField]
    private string m_VerticalAxis = "Vertical";

    /// <summary>
    /// Name of the submit button.
    /// </summary>
    [SerializeField]
    private string m_SubmitButton = "Submit";

    /// <summary>
    /// Name of the submit button.
    /// </summary>
    [SerializeField]
    private string m_CancelButton = "Cancel";

    [SerializeField]
    private float m_InputActionsPerSecond = 10;

    [SerializeField]
    private float m_RepeatDelay = 0.5f;

    [SerializeField]
    [FormerlySerializedAs("m_AllowActivationOnMobileDevice")]
    private bool m_ForceModuleActive;

这部分是不是和上一章Unity面板上显示的一样,主要是可以自定义修改数据,进行一些输入上的微调

函数那块许多都是对于操作上一些逻辑处理…看的有点心烦…就讲个关键的Process()吧,之前在讲BaseInputModule中有介绍到过,这里是重写了基类的方法,记住Process()函数是每个间隔tick都会发送一个事件给目标物体

    public override void Process()
    {
        // 输入模块未聚焦(个人认为就是没有实际把指针放到物体上)
        if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
            return;

        // 用于获取当前指针放到的物体身上是否允许发送事件
        bool usedEvent = SendUpdateEventToSelectedObject();

        // case 1004066 - touch / mouse events should be processed before navigation events in case
        // they change the current selected gameobject and the submit button is a touch / mouse button.

        // touch needs to take precedence because of the mouse emulation layer
        // 先处理触摸事件,如果没有则再处理鼠标事件
        if (!ProcessTouchEvents() && input.mousePresent)
            ProcessMouseEvent();

        // 判断EventSystem是否允许导航事件(move/ submit/ cancel)
        if (eventSystem.sendNavigationEvents)
        {
            if (!usedEvent)
                // 发送移动事件给物体 返回是否事件触发了
                usedEvent |= SendMoveEventToSelectedObject();

            if (!usedEvent)
                // 发送提交事件给物体
                SendSubmitEventToSelectedObject();
        }
    }

这边省略了许多函数的具体实现讲解,建议大家自行看一下,比较好理解。

五. TouchInputModule

TouchInputModule主要用于对触摸板的输入事件,继承自PointerInputModule

绝大多数函数与StandaloneInputModule中实现思路相似,就不重新赘述了,大家自行查看源码了解一下就行了(你不就是想偷懒么…)

六. 总结

输入事件模块这四个类中,实际主要被运用到的就是StandaloneInputModule和TouchInputModule(在EventSystem中的m_SystemInputModules列表中),它们继承自PointerInputModule,分别处理各自适用类型的输入逻辑处理,并将触发的事件传递给相应的物体进行处理。主要函数依靠Process()进行上述操作。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值