UIGU源码分析2:StandaloneInputModule

源码2:StandaloneInputModule

在上文EventSystem中,EventSystem调用了BaseInputModule的process函数,将输入的部分交给了输入模块去处理。根据处理流程的不一样,开发者可以实现不同的BaseInputModule子类,并重写process函数。Unity则是提供了默认的StandaloneInputModule类。这一篇文章先来了解BaseInputModule的继承结构,随后以process函数为入口,继续了解事件系统。

BaseInputModule 的继承结构

BaseInputModule抽象类

PointerInputModule抽象类 继承BaseInputModule

StandaloneInputModule类继承PointerInputModule

TouchInputModule类 继承PointerInputModule(已废弃 功能完全整合到了StandaloneInputModule)

上篇EventSytem 有说到 其主要是更新输入模块BaseInputModule 的process 函数 所以这个为输入响应入口

BaseInputModule

//	用来缓存射线投射后的取得的结果
[NonSerialized]
protected List<RaycastResult> m_RaycastResultCache = new List<RaycastResult>();

//轴向相关的数据  
private AxisEventData m_AxisEventData;

private EventSystem m_EventSystem;
private BaseEventData m_BaseEventData;

AxisEventData 的定义也比较简单 一个原始的输入方向 移动朝向的美剧

其他

  /// <summary>
    /// Event Data associated with Axis Events (Controller / Keyboard).
    /// </summary>
    public class AxisEventData : BaseEventData
    {
        /// <summary>
        /// Raw input vector associated with this event.
        /// </summary>
        public Vector2 moveVector { get; set; }

        /// <summary>
        /// MoveDirection for this event.
        /// </summary>
        public MoveDirection moveDir { get; set; }

        public AxisEventData(EventSystem eventSystem)
            : base(eventSystem)
        {
            moveVector = Vector2.zero;
            moveDir = MoveDirection.None;
       

通过输入参数确定轴向数据 (移动朝向 和方向枚举)

 /// <param name="deadZone">Dead zone.</param>
    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;
    }

实际上在输入模块启用的时候 是添加了一个BaseInput 脚本 ,这个脚本实际上是对以前常用的InputSystem的封装 。所有的输入触摸都是通过这里做检测。

/// <summary>
/// The current BaseInput being used by the input module.
/// </summary
public BaseInput input
{
    get
    {
        if (m_InputOverride != null)
        return m_InputOverride;

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

ventSystem调用Process接口处理输入以及事件。由子类去实现这个方法。

       /// <summary>
        /// Process the current tick for the module.
        /// </summary>
        public abstract void Process();

BaseInputModule的每帧更新函数,由EventSystem驱动。

        /// <summary>
        /// Update the internal state of the Module.
        /// </summary>
        public virtual void UpdateModule()
        {}

PointerInputModule

PointerInputModule定义了一些点操作的变量以及方法。

m_PointerData用来记录当前点的数据,每有一个Touch输入,那么会缓存在m_PointerData记录中。

	protected Dictionary<int, PointerEventData> m_PointerData = new Dictionary<int, PointerEventData>();

    /// <summary>
    /// Search the cache for currently active pointers, return true if found.
    /// </summary>
    /// <param name="id">Touch ID</param>
    /// <param name="data">Found data</param>
    /// <param name="create">If not found should it be created</param>
    /// <returns>True if pointer is found.</returns>
    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;
    }

这个是处理了触摸操作

        /// Given a touch populate the PointerEventData and return if we are pressed or released.
        /// </summary>
        /// <param name="input">Touch being processed</param>
        /// <param name="pressed">Are we pressed this frame</param>
        /// <param name="released">Are we released this frame</param>
        /// <returns></returns>
        protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
        {
            PointerEventData pointerData;
            var created = GetPointerData(input.fingerId, out pointerData, true);

            pointerData.Reset();

            pressed = created || (input.phase == TouchPhase.Began);
            released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);

            if (created)
                pointerData.position = input.position;

            if (pressed)
                pointerData.delta = Vector2.zero;
            else
                pointerData.delta = input.position - pointerData.position;

            pointerData.position = input.position;

            pointerData.button = PointerEventData.InputButton.Left;

            if (input.phase == TouchPhase.Canceled)
            {
                pointerData.pointerCurrentRaycast = new RaycastResult();
            }
            else
            {
                eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

                var raycast = FindFirstRaycast(m_RaycastResultCache);
                pointerData.pointerCurrentRaycast = raycast;
                m_RaycastResultCache.Clear();
            }
            return pointerData;
        }

GetMousePointerEventData()函数是处理鼠标输入 这里是个虚方法是支持重写

StandaloneInputModule

StandaloneInputModule是标准输入模块。鼠标输入和触摸出入都在这里管理

输入模块最终要的函数Process()

        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();

            if (eventSystem.sendNavigationEvents)
            {
                if (!usedEvent)
                    usedEvent |= SendMoveEventToSelectedObject();

                if (!usedEvent)
                    SendSubmitEventToSelectedObject();
            }
        }
  1. 首先检查当前应用是否是focus状态,如果是非focus状态,且在该状态下是忽略事件处理的,那么跳过处理。

  2. SelectedObject代表EventSystem当前记录的选中的UI。SendXXSelectedObject表示,是否要对当前的选中UI发送Update/Move/Submit/Cancel事件。Process会首先尝试执行update事件,同时记录usedEvent。usedEvent代表是否已经执行过事件,如果执行过,后续不执行,也就是同一帧里仅执行一次事件。如果未执行过事件且sendNavigationEvents,那么尝试继续处理Move事件以及Submit/Cancel事件。

  3. sendNavigationEvents代表什么呢?主机游戏常用手柄操作,PC游戏常用键盘。它们都有上下左右,以及确认、取消,常用的六个按钮。sendNavigationEvents表示当按下手柄的方向键的时候,会尝试调用被选中UI的Move事件。当前选中UI会deselect,并且在当前选中UI的某一个方向上的UI会被Select选中。在游戏中这是非常常见的,比如在设置面板中有很多玩家可设置的选项,方向键就可以用来在这些选项中进行切换。

    Move: 当按下上下左右方向键的时候,会根据当前SelectableObject的位置,查找对应方向的下一个SelectableObject,并Move至对应SelectableObject

    Submit、Cancel: 对当前SelectableObject发送对应事件,例如当前SelectableObject是一个Button,那么按下Submit事件,就会触发Button的OnSubmit接口。

  4. 如果没有触发以上事件的话,那么Process会尝试处理鼠标或者触摸事件。代码是首先去处理Touch事件,如果没有点击事件,且存在鼠标,那么去处理鼠标事件。

处理触摸Touch事件

    private bool ProcessTouchEvents()
    {
        for (int i = 0; i < input.touchCount; ++i)
        {
            Touch touch = input.GetTouch(i);

            if (touch.type == TouchType.Indirect)
                continue;

            bool released;
            bool pressed;
            //获取手指状态存储到pointer里
            var pointer = GetTouchPointerEventData(touch, out pressed, out released);

            ProcessTouchPress(pointer, pressed, released);

            if (!released)
            {
                ProcessMove(pointer);
                ProcessDrag(pointer);
            }
            else
                RemovePointerData(pointer);
        }
        return input.touchCount > 0;
    }

这里是遍历每根手指的Touch示例,每一个都能得到一个PointerEventData ,这个包含了手指触摸后的很多信息 ,包括位置,拖动信息等等。同时根据手指按下抬起能获取到三个状态 从而可以处理 第一帧按下 中间拖动 最后一帧抬起的事件处理。

    protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
    {
        PointerEventData pointerData;
        //获取缓存的输入信息,没有就新创建(代表手指刚触摸)  参考上面PointerInputModule的方法
        var created = GetPointerData(input.fingerId, out pointerData, true);

        pointerData.Reset();
		//这里判断是否手指刚按下(true 按下  false 不是刚刚下)
        pressed = created || (input.phase == TouchPhase.Began);
        //这里判断手指是否抬起释放
        released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);

		//如果是第一次创建  保存输入位置信息
        if (created)
            pointerData.position = input.position;

		//如果是刚按下 位置移动偏移初始化为(0,0)
        if (pressed)
            pointerData.delta = Vector2.zero;
        else
        //不是刚按下(其他滑动或者抬起状态)就一直记录偏移量
            pointerData.delta = input.position - pointerData.position;

        pointerData.position = input.position;

        pointerData.button = PointerEventData.InputButton.Left;

		//获取射线结果  后续射线相关再分析
        if (input.phase == TouchPhase.Canceled)
        {
            pointerData.pointerCurrentRaycast = new RaycastResult();
        }
        else
        {
            eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

            var raycast = FindFirstRaycast(m_RaycastResultCache);
            pointerData.pointerCurrentRaycast = raycast;
            m_RaycastResultCache.Clear();
        }
        return pointerData;
    }

在这里要说明下TouchPhase 相关的内容 ,以前没有UGUI的时候 我们自己做触摸相关的都是通过这个枚举判断

//
// 摘要:
//     Describes phase of a finger touch.
public enum TouchPhase
{
    //
    // 摘要:
    //     A finger touched the screen.
    Began = 0,
    //
    // 摘要:
    //     A finger moved on the screen.
    Moved = 1,
    //
    // 摘要:
    //     A finger is touching the screen but hasn't moved.
    Stationary = 2,
    //
    // 摘要:
    //     A finger was lifted from the screen. This is the final phase of a touch.
    Ended = 3,
    //
    // 摘要:
    //     The system cancelled tracking for the touch.
    Canceled = 4
}

通过上面函数 就拿到了 三个状态 我们就可以处理4个事件

//press事件 和 release事件   刚好就是第一帧和最后一帧
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)

这个函数中要注意几行代码

// search for the control that will receive the press
// if we can't find a press handler set the press
// handler to be what would receive a click.
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

 send exit events as we need to simulate this on touch up on touch device
                ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);

上面代码是通过当前对象沿着Hierarchy一层层往上找实现了对应 事件接口的对象 同时发布事件

相似的还有 ExecuteEvents.Execute 只再当前对象上发布事件 事件相关系统 后面再讲

//手指移动事件
   /// <summary>
        /// Process movement for the current frame with the given pointer event.
        /// </summary>
protected virtual void ProcessMove(PointerEventData pointerEvent)
{
        var targetGO = (Cursor.lockState == CursorLockMode.Locked ? null : 		
        pointerEvent.pointerCurrentRaycast.gameObject);
        HandlePointerExitAndEnter(pointerEvent, targetGO);
}

在手指按下移动过程中 我们可能会进入某些对象的Enter handler 离开某些Exit haandler

参数列表的第一个参数PointerData会记录之前的enter的GameObject(oldEnterObject),第二个参数则是新enter的GameObject(newEnterTarget)。

  //拖拽事件
  /// <summary>
        /// Process the drag for the current frame with the given pointer event.
        /// </summary>
        protected virtual void ProcessDrag(PointerEventData pointerEvent)
        {
            if (!pointerEvent.IsPointerMoving() ||
                Cursor.lockState == CursorLockMode.Locked ||
                pointerEvent.pointerDrag == null)
                return;

            if (!pointerEvent.dragging
                && ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
            {
                ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
                pointerEvent.dragging = true;
            }

            // Drag notification
            if (pointerEvent.dragging)
            {
                // Before doing drag we should cancel any pointer down state
                // And clear selection!
                if (pointerEvent.pointerPress != pointerEvent.pointerDrag)
                {
                    ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

                    pointerEvent.eligibleForClick = false;
                    pointerEvent.pointerPress = null;
                    pointerEvent.rawPointerPress = null;
                }
                ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
            

这段代码也比较清晰 就是主要处理拖拽的状态 处理拖拽的事件。

整体来说 触摸事件 结构写的还是蛮清晰。 主要是理解下 其中的一些手势判定,和对应的数据存储PointerData

鼠标操作类似 也是相关状态 (目前已经合并到标准输入模块中了 ,所以基本是无感)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值