刨根系列 之 Unity3D UGUI 背后的工作原理

目录

前言

1. 处理玩家输入

EventSystem的作用

2.封装处理结果

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

4.响应玩家输入


 

前言

        在Unity场景中创建一个Canvas,可以发现,编辑器自动为我们创建了一个叫EventSystem的东西,我们可以发现这个EventSystem中默认包含两个组件:EventSystem和StandaloneInputModule,你可能想知道这两个东西是干啥的?没有它们不行么?它们工作的原理是什么?为什么点击一个按钮就可以触发其onClick事件?这个过程是什么?本文并不会教你怎么去制作UI界面、如何编写UI逻辑,而是刨一刨它背后的事件机制,带您一起揭露UGUI的神秘面纱,让您对UGUI的工作原理有一个更加深刻的认识。

        UGUI全称即Unity Graphical User Interface(Unity图形用户交互系统),用户交互界面(UI)是游戏开发过程中不可忽略的一个部分,通过它可以接收玩家输入,影响游戏运行内容,并向玩家传递信息和反馈视觉效果。

        概括来讲,就是它的工作流程大致是这样的:

  1. 处理玩家输入
  2. 把处理结果包装起来
  3. 把包装好的数据传给界面(其实就是一个函数传参)
  4. 响应玩家输入(例如监听button的onClick)

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<T> 的泛型委托,以及该委托的一堆实例(注),另外它还提供了两个静态方法 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去编写业务逻辑了。

其他事件同理,美滋滋~

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值