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

接上一篇文章, 继续介绍输入模块.

StandaloneInputModule类是上一篇文章介绍的抽象类PointerInputModule的具体实现类, 事件系统的主要处理部分就在这个类.

TouchInputModule类本来是单独处理触摸指针事件的触摸事件部分, 但是后面被挪到StandaloneInputModule中了, 不在维护, 所以我们也不介绍了.

今天就和大家一起一步步来认识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_EDITORif (UnityEditor.EditorApplication.isRemoteConnected)return false;#endifreturn 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);// 如果没有找到则使用当前对象身上的pointerClickif (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;elsepointerEvent.clickCount = 1;pointerEvent.clickTime = time;}else{ // 否则初始化点击次数为1pointerEvent.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先处理触摸事件, 然后才处理鼠标事件, 然后将整个触摸过程细化, 分成几个小的状态, 然后分别记录数据和处理事件, 最后分发事件.

整个触摸过程中, 我们需要处理的主要状态和事件如下(注意, 以下的事件都是只要有任何的处理器, 则其它对象(父级对象)上的处理器不能再处理):

  • 触摸按下

    • 分发之前对象的离开事件和当前对象的选中事件和当前对象的反选事件(ISelectHandler, IDeselectHandler)
    • 分发之前对象的离开事件和当前对象的进入事件(IPointerEnterHandler, IPointerExitHandler)
    • 记录进入对象(pointerEnter)
    • 分发当前对象或者其父级对象上的按下事件(IPointerDownHandler)
    • 记录拖拽对象(pointerDrag)
    • 分发拖拽对象上的拖拽开始事件(IInitializePotentialDragHandler)
    • 记录当前事件按下的对象(可能是当前对象或者其父级对象, pointerPress), 按下次数, 按下时间等
  • 触摸抬起
    • 分发当前事件按下对象的抬起事件(IPointerUpHandler)
    • 如果满足条件, 分发按下对象的抬起事件点击事件(IPointerClick)或者分发拖拽抬起事件(IDropHandler)
    • 分发拖拽对象上的拖拽结束事件(IEndDragHandler)
    • 分发进入对象上的离开事件(IPointerExitHandler)
  • 触摸移动(后面介绍)
    • 移动
    • 拖拽

再简化一点:

触摸屏幕: 找出被触摸到的对象->分发反选和选中事件->分发离开和进入事件->分发按下事件->分发拖拽开始事件.开始移动: 处理移动(导航)->处理拖拽(记录拖拽状态->分发触摸开始->取消按下状态->分发触摸中).抬起手指: 分发抬起事件->分发点击或者拖拽抬起事件->分发拖拽结束事件->分发离开事件.

处理鼠标事件

鼠标事件的内容比较多, 我们从简单到复杂分别介绍.

触发处理

在没有触摸事件需要处理, 同时又检测到鼠标设备的时候, 触发鼠标事件的处理.

然后构造鼠标事件数据, 在这个过程中查找出了被击中的对象.

最后按照左右中键的顺序分别处理每个按键的各种状态.

其中左键有进出状态, 其它两个键只有按下(包含弹起)和拖拽的状态

// 事件处理(选择更新/进出/触摸/鼠标)
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为falseif (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 notificationif (data.ReleasedThisFrame()){// ...// 避免错误, 刷新进出状态if (currentOverGo != pointerEvent.pointerEnter){HandlePointerExitAndEnter(pointerEvent, null);HandlePointerExitAndEnter(pointerEvent, currentOverGo);}}
}

因为整个过程与触摸的处理类似, 最后我们也做一个简化理解:

点击屏幕或者拖拽: 找出被点击到的对象并收集三个按钮的数据, 分别处理左键右键中键
左键(按下抬起, 进出, 拖拽)
右键(按下抬起, 拖拽)
中键(按下抬起, 拖拽)按下抬起:
按下: 分发反选和选中事件->分发按下事件->分发拖拽开始事件
抬起: 分发抬起事件->分发点击或者拖拽抬起事件->分发拖拽结束事件->分发离开事件.拖拽:记录拖拽状态->分发触摸开始事件->取消按下状态->分发触摸中事件.

总结

今天介绍的是事件系统中最重要的标准输入模块部分. 虽然各种事件各种条件五花八门, 但是相信经过整个系列的拆分和分析, 理解起来并没有什么难度.

经过将近一个月的学习和分享, 我们终于完成了整个事件系统源码的解析. 这对于我本人来说也算是一个小小的挑战, 庆幸的是最终还是完成了.

以前我虽然也大致看过这一部分的源码, 但是没有这么详细, 都是带着需求和问题去搜寻, 借这次机会终于大致将这部分啃下来了, 对整体的轮廓和关键的细节有了一定的掌握, 相信在未来的开发中能够让我少走很多弯路.

这段我专门了解了下, 很多同学对这些源码, 原理不怎么感兴趣, 希望我出一些实战和入门的文章, 我的意见是这些方面已经有很多优秀的作者写了很多文章, 包括我本人也因此受益良多, 但是源码和原理方面的文章还是比较少, 而我想在这方面做一份贡献, 所以未来很长一段时间还是会专注源码的学习和解读. 最多在过程中会穿插一些开发技巧类或者渲染方面的文章. 所以不管有多少同学感兴趣, 我还是会尽量坚持下去, 给有兴趣有缘的同学一些参考, 大家共同学习进步.

UGUI源码解析大概会分为三部分:

  • 事件系统
  • 常用组件
  • UGUI相关Editor部分

我们已经完成了第一部分, 接下来会进入源码解析的第二部分, 即UGUI常用组件源码解析.

相信整个系列下来, 我和大家都能对UGUI有很多新的认识, 以便在日常的开发中有的放矢, 心中有数.

顺便说一下, 下一个部分有很多内容看不到源码, 是写到C++里的, 我只能尽量根据表现来猜测, 不能保证正确.

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

Unity中的UGUI源码解析之事件系统(9)-输入模块(下)相关推荐

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

    Unity中的UGUI源码解析之事件系统(8)-输入模块(中) 接上一篇文章, 继续介绍输入模块. Unity中主要处理的是指针事件, 也就是在2d平面上跟踪指针设备输入坐标的的事件, 这一类事件有鼠 ...

  2. Unity中的UGUI源码解析之事件系统(6)-RayCaster(下)

    Unity中的UGUI源码解析之事件系统(6)-RayCaster(下) 接上一篇文章, 继续介绍投射器. GraphicRaycaster GraphicRaycaster继承于BaseRaycas ...

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

    Unity中的UGUI源码解析之事件系统(2)-EventSystem组件 今天介绍我们的第一个主角: EventSystem. EventSystem在整个事件系统中处于中心, 相当于事件系统的管理 ...

  4. Unity中的UGUI源码解析之事件系统(3)-EventData

    Unity中的UGUI源码解析之事件系统(3)-EventData 为了在事件系统中传递数据, Unity提供了EventData相关的类来封装这一类数据. 了解这些结构有助于我们对后面模块的学习. ...

  5. Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement

    Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement 在上一篇文章中, 我们对整个Graphic部分做了概述, 这篇文章我们介绍ICanvasElement和 ...

  6. elementui组件_elementui 中 loading 组件源码解析(续)

    上一篇我们说了elementui如何将loading组件添加到 Vue 实例上,具体内容见上期 elementui 中 loading 组件源码解析. 这一篇我们开始讲讲自定义指令 自定义指令 关于自 ...

  7. elementui table某一列是否显示_elementui 中 loading 组件源码解析(续)

    上一篇我们说了elementui如何将loading组件添加到 Vue 实例上,具体内容见上期 elementui 中 loading 组件源码解析. 这一篇我们开始讲讲自定义指令 自定义指令 关于自 ...

  8. Qt源码解析之事件系统(一)

    Qt源码解析之事件系统 一.向事件循环里面添加事件 二.QThread里面的事件循环 一.向事件循环里面添加事件 [static] void QCoreApplication::postEvent(Q ...

  9. UGUI源码解析——ContentSizeFitter

    一:前言 ContentSizeFitter继承自ILayoutSelfController,是调整对象自适应的组件,ContentSizeFitter不改变子物体的大小和位置,而是根据子物体(ILa ...

最新文章

  1. Google首席执行官:AI就像火和电,有用而又危险
  2. 「每周CV论文推荐」 初学深度学习单图三维人脸重建需要读的文章
  3. Java并发编程-volatile
  4. 小米今日正式进军越南市场 借助合作方铺渠道分销
  5. OSChina 周六乱弹 —— 买楼出一块钱,你们出么?
  6. Linux运维并行批量操作命令pssh的使用
  7. DVbbs8.2入侵思路与总结
  8. CSS伪类的三种写法
  9. 第四章 生命周期函数--35 vue-resource发起get、post、jsonp请求
  10. 一学就废的三种简单排序【冒泡、插入、选择】
  11. CCF NOI1001 温度转换
  12. steam加速_追梦加速器:Steam一周销量前十榜单,你的游戏排第几?
  13. MATLAB取整及位数
  14. python+OpenCv笔记(三):修改像素点、感兴趣区域、获取图像属性
  15. react 的 render 函数
  16. outlook使用笔记
  17. 魏永征《向媒介侵权讨说法:媒介侵权法律问题》
  18. C语言用指针法输入12个整数,然后按每行4个数输出(刷题)
  19. Android开发读取通讯录信息
  20. 对于手机号和邮箱的格式验证

热门文章

  1. java list sublist方法_(转)Java 中 List.subList() 方法的使用陷阱
  2. 【深度学习】从0学习YOLOV5:科大讯飞X光安检检测
  3. python如何写生日快乐说说_过生日发朋友圈怎么写 祝自己生日快乐的微信说说加配图...
  4. 扭曲文字动态效果怎么制作?教程来了
  5. 从电竞练习生到B站UP主,年轻一代的AI生活
  6. 5G上行,真是让人操碎了心!
  7. QuickJS 引擎一年见闻录
  8. java字节流读取word_java怎么样读取word文档,inputstream好像只可以读取记事本 啊...
  9. 湖畔大学终结?刚刚,官方回应更名!
  10. uni-app 选择城市(城市列表选择)