问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Unity3D UGUI工作原理详解:从输入处理到事件响应

创作时间:
作者:
@小白创作中心

Unity3D UGUI工作原理详解:从输入处理到事件响应

引用
CSDN
1.
https://blog.csdn.net/zhaocg00/article/details/118999234

Unity3D的UGUI(Unity Graphical User Interface)是游戏开发中不可或缺的一部分,它负责处理玩家输入、封装处理结果、传递数据以及响应玩家操作。本文将深入探讨UGUI的工作原理,帮助开发者更好地理解其背后的机制。

在Unity场景中创建一个Canvas时,编辑器会自动创建一个名为EventSystem的对象,其中包含EventSystem和StandaloneInputModule组件。这些组件是如何工作的?为什么点击按钮可以触发其onClick事件?本文将为您揭示UGUI的神秘面纱。

UGUI的工作流程可以概括为以下四个步骤:

  1. 处理玩家输入
  2. 封装处理结果
  3. 传递包装好的数据
  4. 响应玩家输入

1. 处理玩家输入

InputModule负责处理玩家的各种输入,包括键盘输入、鼠标输入和触屏输入等。对于UI来说,键盘输入主要用于Submit和Navigation,而本篇文章主要以鼠标输入为例展开说明。

除了Unity提供的StandaloneInputModule和TouchInputModule之外,我们还可以通过泛化BaseInputModule来自定义InputModule。处理过程主要是重写父类的Process函数,在其内部对鼠标光标的各种状态进行计算和标记。

详细展开来看这个处理过程做了哪些事情:

  • 射线检测:EventSystem中的一个方法,以光标所在的屏幕坐标为起点向UI投一条射线,检查射线所穿过的所有UI控件(UI是分层的哦,在顶层的当然是先被照射的,当然免不了有些UI控间是忽略射线检测的),并提取其中第一个照射到的控件。
  • 检查鼠标按键的状态:鼠标左键、中键、右键
  • 处理鼠标按下(Press)、移动(进入(Enter)了哪个控件了又离开(Exit)哪个控件了)、拖拽(Drag),鼠标的三个按键都要做一下(移动的部分在处理鼠标中键和右键的时候不需要了)。

下面这幅图简单概括了一下EventSystem相关的UML类图:

EventSystem的作用

  • 管理游戏中的InputModule
  • 驱动InputModule的Update和Process
  • 做射线检查 和 射线结果的层级比较

如果把InputModule比喻成车轮子,那么EventSystem就是发动机,发动机不转,轮子怎么能跑呢?别告诉我用手推。

2. 封装处理结果

无论是哪一种InputModule,它们的共同目标便是将用户输入封装成一个EventData。其实,在处理的过程中就顺带把要封装的封装了,而不是处理完了之后再统一封装,因为有时候后面的处理步骤是需要前面的步骤的处理结果的。

EventData的UML类图如下:

我们着重看一下PointerEventData, PointerEventData中封装了鼠标数据,如:

  • 当前光标指向的是哪个物体 - pointerPress
  • 是否正在拖拽 - dragging
  • 点击次数(双击的时候是2) - clickCount
  • 按下时坐标 - pressPosition
  • 当前坐标 - position

Q&A

  1. Unity如何知道光标指向了哪个控体?
    答:一条射线打上去,看先射到了哪个,Raycaster就是干这个事的

  2. 如何判断正在拖拽?
    答:因为已经记录了按下时的坐标,又知道当前坐标,如果二者之差大于拖拽的阈值,则认为在拖拽,这个是在PointerInputModule里判断的。

3. 传递包装好的数据(BaseEventData)

现在你知道Unity里通过 InputModule 把用户的输入处理完之后包装到BaseEventData里,那么接下来呢?这个EventData怎么处理?答案当然是要传递给我们的UI组件了,然而如何传递呢?

我们知道,鼠标光标事件其实是多种多样的,比如:光标按下(Down)、抬起(Up)、进入(Enter)、离开(Exit),如果按下和抬起的时间很短又可以产生点击事件(Click),对于每一种事件都可以定义为一个接口,例如:IPointerDownHandler、IPointerUpHandler、IPointerEnterHandler、IPointerExitHandler、IPointerClickHandler,为了统一操作这些接口,我们让它们都继承自IEventSystemHandler。

不同的UI组件可以选择性的支持这些鼠标事件,比如有的控件我们就是不希望它响应点击事件,那么不让它实现IPointerClickHandler的接口就行了

这就有了下面这个类图:

注:上面这张图因画面仅提供了部分Pointer相关的几个接口,除此之外还有很多,可以参考源码中的IEventSystemHandler

理论上来讲,既然我们(通过raycaster)已经知道了光标落在了哪一个GameObject上,那么我们就可以调用该GameObject所实现的任意一个接口的接口函数。

因此举例,既然Button实现了IPointerClickHandler接口,那么对于一个按钮控件来说,当光标点击事件到来时,就可以调用到它的Button组件(as IPointerClickHandler)的OnPointerClick函数。

上面虽然只用一句话就描述完了,但是真正对于一个UI系统来说,由于事件种类的复杂性,所以还是要花一定的心思来想想如何架构的,UGUI中把事件的触发封装到了一个叫ExecuteEvents 的类里面。

ExecuteEvents中定义了一个叫EventFunction的泛型委托,以及该委托的一堆实例(注),另外它还提供了两个静态方法 Execute 和 ExecuteHierachy,前者是目标控件身上所有实现了泛型T类型组件都 执行泛型T的接口函数,后者则是按着层级面板向上递归查询,直到找到一个实现T接口的组件后执行泛型T的接口函数(例如ScrollView的滚动)。

读上去不太容易理解,其实就是一个数据分发的过程,前者是给身上每一个有需要的组件都发,如果有组件接收则返回true,如果没有,那就当啥也没发生(返回false);后者是,如果它自己身上没有组件需要这个数据,就向上找它父物体,父物体也不要那就去找它父物体的父物体,这么一直向上找下去,直到找到有某位大哥接收了(返回这个大哥GameObject的引用)或者到了根节点没法再往上找了(最终数据还是没人要,返回null)。

(注)所谓的“一堆实例”是说,每一个IEventSystemHandler在这里都对应有一个委托实例。

下面是这两个方法的具体参数:

public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler{
    ...
}
public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler{
    ...
}

对于一个已知的target(ExecuteHierarchy其实也是在寻找一个target),在C#中我们很容易使用GetComponents来获取其身上的所有组件,然后用 is 来判断是否实现了某个接口(T),如果实现了,则使用functor把eventData传递给这个组件,源码中是这样写的:

functor(arg, eventData)

这里的arg就是代表了组件列表中的其中一个组件

其实就相当于arg.OnPointerXXX(eventData)

functor的内部实现其实就是调用arg各自的接口函数了, 以IPointerClickHandler举例:

private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
    TriggerExecuteEvent(GetEventName(typeof(IPointerClickHandler)), handler, eventData);
    handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}

这样就把eventData传给了我们的目标组件了

4. 响应玩家输入

经过上面说了一堆,你大概明白了,原来UnityUGUI是先把数据处理了一下,并标记了一些状态然后传递给UI组件,那么UI组件拿到这个数据后怎么响应呢?或者说,我们的游戏内容如何响应玩家的输入呢?

答案是:事件

我们前文中所讲的事件只是针对鼠标而言,鼠标点击了发出来了一个点击事件,鼠标进入了产生一个进入事件,这个事件需要在UI组件中定义出来,供我们开发业务逻辑时监听。

例如:按钮 Button 便定义了一个onClick事件,因此我们便可以使用

btn.onClick.AddListener(DoSomethingFunc);

来监听这个事件了。

那么何时会触发这个onClick呢?回顾上一节我们讲的,UGUI把封装好的数据传递给UI组件,那么这些组件在调用其对应类型的接口函数时,便会触发相应事件。对于IPointerClickHandler按钮来说,鼠标点击一个按钮时,便会调用它的OnPointerClick接口,在OnPointerClick中释放onClick事件。

接下来便是监听onClick去编写业务逻辑了。其他事件同理,美滋滋~

本文原文来自CSDN

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号