赞
踩
接上一篇文章, 继续介绍输入模块.
StandaloneInputModule类是上一篇文章介绍的抽象类PointerInputModule的具体实现类, 事件系统的主要处理部分就在这个类.
TouchInputModule类本来是单独处理触摸指针事件的触摸事件部分, 但是后面被挪到StandaloneInputModule中了, 不在维护, 所以我们也不介绍了.
今天就和大家一起一步步来认识StandaloneInputModule.
目前在Unity中, 默认的输入模块就是StandaloneInputModule, 大部分事件处理过程也是这个模块, 我们也可以通过继承PointerInputModule并参考StandaloneInputModule实现自己的输入模块.
前面的文章介绍过, 如果场景中不存在EventSystem组件, 则在创建任意UI元素时, 会自动创建一个EventSystem对象, 上面默认带了两个组件: EventSystem和StandaloneInputModule, 在结合Canvas身上的Graphic Raycaster组件, 就构成了基本的事件系统.
public class StandaloneInputModule : PointerInputModule { // --------------------------------------------------------------------- // -- 几个轴名称, 用于Input.GetAxisRaw或者轴数据 [SerializeField] private string m_HorizontalAxis = "Horizontal"; [SerializeField] private string m_VerticalAxis = "Vertical"; [SerializeField] private string m_SubmitButton = "Submit"; [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; }
对应的属性不再列出.
/// 上一个动作的发生的时间, 主要用于移动事件 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 bool ShouldIgnoreEventsOnNoFocus() { switch (SystemInfo.operatingSystemFamily) { case OperatingSystemFamily.Windows: case OperatingSystemFamily.Linux: case OperatingSystemFamily.MacOSX: #if UNITY_EDITOR if (UnityEditor.EditorApplication.isRemoteConnected) return false; #endif return true; default: return false; } } // 获取原始的位移向量方向(未被平滑处理) private Vector2 GetRawMoveVector() { Vector2 move = Vector2.zero; move.x = input.GetAxisRaw(m_HorizontalAxis); move.y = input.GetAxisRaw(m_VerticalAxis); if (input.GetButtonDown(m_HorizontalAxis)) { if (move.x < 0) move.x = -1f; if (move.x > 0) move.x = 1f; } if (input.GetButtonDown(m_VerticalAxis)) { if (move.y < 0) move.y = -1f; if (move.y > 0) move.y = 1f; } return move; }
// 更新输入模块 public override void UpdateModule() { // 过滤非焦点状态 if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus()) return; // 记录鼠标的上一个位置和当前位置 m_LastMousePosition = m_MousePosition; m_MousePosition = input.mousePosition; } // 获取当前状态是否受支持(能不能处理事件) public override bool IsModuleSupported() { // 强制支持或者支持鼠标或者支持触摸 return m_ForceModuleActive || input.mousePresent || input.touchSupported; } // 是否应该激活模块, 切换输入模块时使用 public override bool ShouldActivateModule() { // 游戏对象激活并且层级激活 if (!base.ShouldActivateModule()) return false; // 激活状态(任意一个都标识激活) var shouldActivate = m_ForceModuleActive; // 强制 shouldActivate |= input.GetButtonDown(m_SubmitButton); // 提交键被按下(比如回车) shouldActivate |= input.GetButtonDown(m_CancelButton); // 取消键被按下(比如Esc) shouldActivate |= !Mathf.Approximately(input.GetAxisRaw(m_HorizontalAxis), 0.0f); // 存在水平位移 shouldActivate |= !Mathf.Approximately(input.GetAxisRaw(m_VerticalAxis), 0.0f); // 存在竖直位移 shouldActivate |= (m_MousePosition - m_LastMousePosition).sqrMagnitude > 0.0f; // 鼠标位置有变化 shouldActivate |= input.GetMouseButtonDown(0); // 左键被按下 // 多点触摸必定激活 if (input.touchCount > 0) shouldActivate = true; return shouldActivate; } // 激活模块, 切换模块时使用 public override void ActivateModule() { // 过滤非焦点状态 if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus()) return; base.ActivateModule(); // 记录鼠标两个位置 m_MousePosition = input.mousePosition; m_LastMousePosition = input.mousePosition; var toSelect = eventSystem.currentSelectedGameObject; if (toSelect == null) toSelect = eventSystem.firstSelectedGameObject; // 向焦点对象发送选中事件 eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData()); } // 反激活模块, 清空各种状态 public override void DeactivateModule() { base.DeactivateModule(); ClearSelection(); } // 事件处理(选择更新/导航/触摸/鼠标) public override void Process() { // 过滤非焦点状态 if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus()) return; // 向焦点对象发送updateSelected事件 bool usedEvent = SendUpdateEventToSelectedObject(); // 导航事件处理(位移, 提交, 取消) if (eventSystem.sendNavigationEvents) { // 如果对象不自行处理updateSelected事件, 则进一步向焦点对象发送位移(主要是水平和竖直的轴事件)事件 if (!usedEvent) usedEvent |= SendMoveEventToSelectedObject(); // 如果对象不自行处理updateSelected和位移事件, 则进一步向焦点对象发送剩下的导航事件(提交和取消) if (!usedEvent) SendSubmitEventToSelectedObject(); } // 处理触摸事件和鼠标事件 if (!ProcessTouchEvents() && input.mousePresent) ProcessMouseEvent(); }
// 向当前焦点对象发送选择更新事件(updateSelected)
protected bool SendUpdateEventToSelectedObject()
{
if (eventSystem.currentSelectedGameObject == null)
return false;
var data = GetBaseEventData();
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler);
return data.used;
}
当选择更新事件的处理器没有截获事件(BaseEventData.used != true
), 同时EventSystem支持导航事件, 就需要处理导航事件, 导航事件指的的是: 位移(Move), 提交(Submit), 取消(Cancel), 都在Project Settings->Input
中设置.
// 处理导航事件中的位移事件(主要是水平和竖直方向上的轴事件) protected bool SendMoveEventToSelectedObject() { float time = Time.unscaledTime; // 获取当前位移方向 Vector2 movement = GetRawMoveVector(); //过滤微小位移 if (Mathf.Approximately(movement.x, 0f) && Mathf.Approximately(movement.y, 0f)) { m_ConsecutiveMoveCount = 0; return false; } // 只有按下[位移按钮]才处理 bool allow = input.GetButtonDown(m_HorizontalAxis) || input.GetButtonDown(m_VerticalAxis); // 两次位移基本同向 bool similarDir = (Vector2.Dot(movement, m_LastMoveVector) > 0); if (!allow) { // 长按[位移按钮] // 同方向且连续次数为1, 说明是按下[位移按钮]后第一个长按判断, 等待延时后当做重复处理 if (similarDir && m_ConsecutiveMoveCount == 1) allow = (time > m_PrevActionTime + m_RepeatDelay); else // 已经进入重复按键, 等待延时处理 allow = (time > m_PrevActionTime + 1f / m_InputActionsPerSecond); } // 不处理位移事件 if (!allow) return false; // 根据位移封装轴事件数据 var axisEventData = GetAxisEventData(movement.x, movement.y, 0.6f); // 有位移方向则开始处理位移事件 if (axisEventData.moveDir != MoveDirection.None) { // 向当前焦点对象发送位移事件 ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, axisEventData, ExecuteEvents.moveHandler); // 两次位移方向不同, 清空连续事件次数 if (!similarDir) m_ConsecutiveMoveCount = 0; // 两次位移方向相同, 增加连续事件次数 m_ConsecutiveMoveCount++; // 记录上次位移时间 m_PrevActionTime = time; // 记录上次位移方向 m_LastMoveVector = movement; } else { // 两次位移方向不同, 清空连续事件次数 m_ConsecutiveMoveCount = 0; } return axisEventData.used; }
我们在上面的Process方法中看到, Unity优先处理触摸事件, 如果有触摸事件被处理, 则略过鼠标事件的处理.
private bool ProcessTouchEvents() { // 支持多点触控, 分别处理多个触摸, 大部分情况下只需要处理一个 for (int i = 0; i < input.touchCount; ++i) { Touch touch = input.GetTouch(i); // 只处理直接来自设备的触摸(TouchType.Direct)和来自触控笔的触摸(TouchType.Stylus) if (touch.type == TouchType.Indirect) continue; bool released; bool pressed; // 构造触摸事件数据, 检测出被触摸的对象(pointerData.pointerCurrentRaycast) var pointer = GetTouchPointerEventData(touch, out pressed, out released); // 处理触摸按下 ProcessTouchPress(pointer, pressed, released); // 没有抬起的状态下, 处理移动和拖拽事件, 这两个事件处理和鼠标事件保持一致, 抽象为Poninter事件统一处理 if (!released) { ProcessMove(pointer); ProcessDrag(pointer); } else // 移除触摸事件 RemovePointerData(pointer); } return input.touchCount > 0; } // 处理触摸按下 protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released) { var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject; // 处理和分发按下事件 if (pressed) { // 赋值用于按下事件的各种数值 pointerEvent.eligibleForClick = true; pointerEvent.delta = Vector2.zero; pointerEvent.dragging = false; pointerEvent.useDragThreshold = true; pointerEvent.pressPosition = pointerEvent.position; pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast; // 选中按下的对象 DeselectIfSelectionChanged(currentOverGo, pointerEvent); // 分发进入事件和设置进入对象 if (pointerEvent.pointerEnter != currentOverGo) { // send a pointer enter to the touched element if it isn't the one to select... HandlePointerExitAndEnter(pointerEvent, currentOverGo); pointerEvent.pointerEnter = currentOverGo; } // 在当前对象和其所有的父级对象上查找拥有ponterDown事件处理器的对象 var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler); // 如果没有找到则使用当前对象身上的pointerClick if (newPressed == null) newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo); // Debug.Log("Pressed: " + newPressed); float time = Time.unscaledTime; if (newPressed == pointerEvent.lastPress) { // 上次和本次的点击对象相同(也可能都是空对象), 记录点击次数和时间, 如果两次点击间隔少于0.3s, 则增加点击次数 var diffTime = time - pointerEvent.clickTime; if (diffTime < 0.3f) ++pointerEvent.clickCount; else pointerEvent.clickCount = 1; pointerEvent.clickTime = time; } else { // 否则初始化点击次数为1 pointerEvent.clickCount = 1; } // 记录[按下对象](有pointerDown或者当前对象上有pointerClick处理器) pointerEvent.pointerPress = newPressed; // 记录原始按下对象 pointerEvent.rawPointerPress = currentOverGo; pointerEvent.clickTime = time; // 记录ponterDrag对象(当前对象上有ponterDrag处理器) pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo); // 向ponterDrag对象分发initializePotentialDrag事件 if (pointerEvent.pointerDrag != null) ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag); } // 处理和分发抬起事件 if (released) { // 向[按下对象]分发抬起事件 ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler); // 用于判断按下对象与处理PointerClick的对象是不是同一个 var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo); if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick) { // 如果是同一个对象且同时标记了点击(没有被拖拽打断), 则分发pointerClick事件 ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler); } else if (pointerEvent.pointerDrag != null && pointerEvent.dragging) { // 向当前对象和其父级对象分发drop事件(拖拽过程中抬起) ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler); } // 清空按下数据和状态 pointerEvent.eligibleForClick = false; pointerEvent.pointerPress = null; pointerEvent.rawPointerPress = null; // 向要处理[pointerDrag]的对象分发拖拽结束事件 if (pointerEvent.pointerDrag != null && pointerEvent.dragging) ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler); // 清空拖拽数据和状态 pointerEvent.dragging = false; pointerEvent.pointerDrag = null; // 向要处理[pointerEnter]的对象分发pointerExit事件 ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler); pointerEvent.pointerEnter = null; } }
因为这段是比较核心的, 我们在简单做个归纳.
通过上面的代码, 我们知道了Unity先处理触摸事件, 然后才处理鼠标事件, 然后将整个触摸过程细化, 分成几个小的状态, 然后分别记录数据和处理事件, 最后分发事件.
整个触摸过程中, 我们需要处理的主要状态和事件如下(注意, 以下的事件都是只要有任何的处理器, 则其它对象(父级对象)上的处理器不能再处理):
再简化一点:
触摸屏幕: 找出被触摸到的对象->分发反选和选中事件->分发离开和进入事件->分发按下事件->分发拖拽开始事件.
开始移动: 处理移动(导航)->处理拖拽(记录拖拽状态->分发触摸开始->取消按下状态->分发触摸中).
抬起手指: 分发抬起事件->分发点击或者拖拽抬起事件->分发拖拽结束事件->分发离开事件.
鼠标事件的内容比较多, 我们从简单到复杂分别介绍.
在没有触摸事件需要处理, 同时又检测到鼠标设备的时候, 触发鼠标事件的处理.
然后构造鼠标事件数据, 在这个过程中查找出了被击中的对象.
最后按照左右中键的顺序分别处理每个按键的各种状态.
其中左键有进出状态, 其它两个键只有按下(包含弹起)和拖拽的状态
// 事件处理(选择更新/进出/触摸/鼠标) public override void Process() { // ... // 处理触摸事件和鼠标事件 if (!ProcessTouchEvents() && input.mousePresent) ProcessMouseEvent(); } protected void ProcessMouseEvent(int id) { // 构造鼠标事件, 检测出被击中的对象(pointerData.pointerCurrentRaycast) var mouseData = GetMousePointerEventData(id); var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData; m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject; // 处理左键(按下-抬起, 进出, 拖拽) ProcessMousePress(leftButtonData); ProcessMove(leftButtonData.buttonData); ProcessDrag(leftButtonData.buttonData); // 处理右键(按下-抬起, 拖拽) ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData); ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData); // 处理中键(按下-抬起, 拖拽) ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData); ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData); // 分发滚轮事件 if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f)) { var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject); ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler); } }
只有鼠标未锁定时才处理进出事件, 其它已经介绍过, 不再赘述.
protected virtual void ProcessMove(PointerEventData pointerEvent)
{
var targetGO = (Cursor.lockState == CursorLockMode.Locked ? null : pointerEvent.pointerCurrentRaycast.gameObject);
HandlePointerExitAndEnter(pointerEvent, targetGO);
}
与上面一样, 只有鼠标未锁定时才处理拖拽事件.
protected virtual void ProcessDrag(PointerEventData pointerEvent) { // 拖拽事件处理条件(有位移, 鼠标未锁定, 有拖拽对象) if (!pointerEvent.IsPointerMoving() || Cursor.lockState == CursorLockMode.Locked || pointerEvent.pointerDrag == null) return; // 分发拖拽开始事件(IBeginDragHandler), 并设置拖拽状态 // 条件: 未处于拖拽状态, 超过最小位移判断 if (!pointerEvent.dragging && ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold)) { ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler); pointerEvent.dragging = true; } // 分发抬起和拖拽事件 if (pointerEvent.dragging) { // 如果按下的和拖拽的不是同一个对象, 那么需要取消按下对象的按下状态, 向其分发抬起事件并清空按下的相关数据 // 也就是说, 同一个对象上可以同时处理点击和拖拽, 如果不同对象, 只要拖拽, 点击就无法触发了 // ScrollRect就是利用了这一点来实现拖拽和内部的点击互不影响! // 如果需要在同一个对象上可以同时处理点击和拖拽, 而且在拖拽之后不要触发点击, 则可以参考这里的代码, 在拖拽回调中设置一些信息来屏蔽后续的点击事件触发, 如置空pointerPress或者设置eligibleForClick为false 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); } }
处理鼠标按下抬起事件的过程和代码与处理触摸的高度一致, 这里只贴不同的地方, 重复的地方不在赘述.
protected void ProcessMousePress(MouseButtonEventData data) { var pointerEvent = data.buttonData; var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject; if (data.PressedThisFrame()) { // ... // 分发进入事件和设置进入对象, 鼠标处理没有这部分 // if (pointerEvent.pointerEnter != currentOverGo) // { // send a pointer enter to the touched element if it isn't the one to select... // HandlePointerExitAndEnter(pointerEvent, currentOverGo); // pointerEvent.pointerEnter = currentOverGo; //} // ... } // PointerUp notification if (data.ReleasedThisFrame()) { // ... // 避免错误, 刷新进出状态 if (currentOverGo != pointerEvent.pointerEnter) { HandlePointerExitAndEnter(pointerEvent, null); HandlePointerExitAndEnter(pointerEvent, currentOverGo); } } }
因为整个过程与触摸的处理类似, 最后我们也做一个简化理解:
点击屏幕或者拖拽: 找出被点击到的对象并收集三个按钮的数据, 分别处理左键右键中键
左键(按下抬起, 进出, 拖拽)
右键(按下抬起, 拖拽)
中键(按下抬起, 拖拽)
按下抬起:
按下: 分发反选和选中事件->分发按下事件->分发拖拽开始事件
抬起: 分发抬起事件->分发点击或者拖拽抬起事件->分发拖拽结束事件->分发离开事件.
拖拽:记录拖拽状态->分发触摸开始事件->取消按下状态->分发触摸中事件.
今天介绍的是事件系统中最重要的标准输入模块部分. 虽然各种事件各种条件五花八门, 但是相信经过整个系列的拆分和分析, 理解起来并没有什么难度.
经过将近一个月的学习和分享, 我们终于完成了整个事件系统源码的解析. 这对于我本人来说也算是一个小小的挑战, 庆幸的是最终还是完成了.
以前我虽然也大致看过这一部分的源码, 但是没有这么详细, 都是带着需求和问题去搜寻, 借这次机会终于大致将这部分啃下来了, 对整体的轮廓和关键的细节有了一定的掌握, 相信在未来的开发中能够让我少走很多弯路.
这段我专门了解了下, 很多同学对这些源码, 原理不怎么感兴趣, 希望我出一些实战和入门的文章, 我的意见是这些方面已经有很多优秀的作者写了很多文章, 包括我本人也因此受益良多, 但是源码和原理方面的文章还是比较少, 而我想在这方面做一份贡献, 所以未来很长一段时间还是会专注源码的学习和解读. 最多在过程中会穿插一些开发技巧类或者渲染方面的文章. 所以不管有多少同学感兴趣, 我还是会尽量坚持下去, 给有兴趣有缘的同学一些参考, 大家共同学习进步.
UGUI源码解析大概会分为三部分:
我们已经完成了第一部分, 接下来会进入源码解析的第二部分, 即UGUI常用组件源码解析.
相信整个系列下来, 我和大家都能对UGUI有很多新的认识, 以便在日常的开发中有的放矢, 心中有数.
顺便说一下, 下一个部分有很多内容看不到源码, 是写到C++里的, 我只能尽量根据表现来猜测, 不能保证正确.
好了, 今天就是这些, 希望对大家有所帮助.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。