【UE·底层篇】Slate源码分析——点击事件的触发流程梳理
我们都知道**Slate是UE4的UI底层框架**。但是UE4的UMG和Unity的UGUI比起来还是有很多不足,所以在实际开发的过程中经常需要对Slate进行改造。本文将带大家由浅入深地阅读**Slate源码里关于点击事件相关的部分**。
我们都知道Slate是UE4的UI底层框架。但是UE4的UMG和Unity的UGUI比起来还是有很多不足,所以在实际开发的过程中经常需要对Slate进行改造。本文将带大家由浅入深地阅读Slate源码里关于点击事件相关的部分。
基础知识回顾
SetFocus
SetFocus是UWidget类里面的方法,最终会调用到FSlateApplication的SetUserFocus方法。SetFoucs顾名思义,就是设置聚焦,当widget获得聚焦或失去聚焦时会触发相关事件。功能说明如图,首先有两个面板A和B,按下A的按钮时调用A面板的SetFocus,按下B的按钮时调用B面板的SetFocus。
游戏运行,按下A的按钮,Focus相关事件调用顺序如下:
- A的OnFocusChanging
- A的OnFocusReceived
此时再按下B的按钮,Focus相关事件调用顺序如下:
- A的OnFocusChanging
- B的OnFocusChanging
- A的OnFocusLost
- B的OnFocusReceived
注意SetFocus并不会引起改变任何widget的响应功能,这点和等会要提的CaptureMouse有很大不同。
CaptureMouse
CaptureMouse是FReply里的方法,参数为TSharedRef,用于锁定某个Widget。调用CaptureMouse后整个游戏将只能和锁定的Widget进行响应,该Widget的父级和子级也是不能响应的,直到调用ReleaseMouseCapture。(需要注意的是如果在ue4编辑器下调用了CaptureMouse,那编辑器自己的按钮也是不能响应的。)
FakingTouch
在ue4里,鼠标点击和Touch(移动设备的触屏)是两种不同的操作。为了在编辑器下模拟Touch,通常会勾选ProjectSettings里的UseMouseForTouch。这个情况下要想判断是真正的Touch还是模拟的Touch,可以调用FSlateApplication的IsFakingTouchEvents。
FPointerEvent
FPointerEvent 点输入事件类, 用于处理鼠标以及移动设备的触摸按键,继承自FInputEvent输入事件类。
PreviewMouseButtonDown
PreviewMouseButtonDown事件的调用时机在MouseButtonDown和TouchStart前面,和其他事件不同的地方是它是从后往前遍历的。如图,B面板在A面板上面,两个面板都接入了OnMouseButtonDown事件并且都返回Handle,同时A面板接入了PreviewMouseButtonDown事件也返回FReply::Handle。
运行游戏,点击B面板,结果为:
- 调用了A面板的PreviewMouseButtonDown
- A和B的MouseButtonDown都没有被调用
调用栈总览
下面以UserWidget为例,看一下它的TouchStart事件在PC上的调用流程。
首先是平台层,根据不同的硬件设备和操作系统调用不同平台的API。window平台调用的是WindowsApplication类。在这个类里面会接受玩家输入,判断玩家输入类型,最终来到Slate层的SlateApplication的OnMouseDown并且传入玩家输入相关的数据。
OnMouseDown判断了一下是FakingTouch,接着调用了OnTouchStarted。
OnTouchStarted根据传入的数据生成了一个PointerEvent,接着调用ProcessTouchStartedEvent。
ProcessTouchStartedEvent调用了SlateUser的NotifyTouchStarted,这里面会记录每根手指头的Touch位置信息。然后调用ProcessMouseButtonDownEvent。
ProcessMouseButtonDownEvent里会判断当前有没有Caputure某个Widget,有的话调用FEventRouter::Route处理点击事件,没有的话则调用RoutePointerDownEvent。
RoutePointerDownEvent最后也会调用FEventRouter::Route。
FEventRouter::Route里则会调用SWidget的OnTouchStarted。SWidget是SObjectWidget的基类,OnTouchStated里面默认不会做任何事情。SObjectWidget重写了OnTouchStarted,最终调用了UserWidget的NativeOnTouchStarted。
这里面最复杂且最核心的三个方法为ProcessMouseButtonDownEvent、RoutePointerDownEvent以及FEventRouter::Route,后面将进行更深入的分析。
Slate中几个重要类的含义
在细读源码之前,还需要把几个重要类的概念搞明白。
FSlateUser
SlateUser用于处理每个玩家的输入。对于手游来说,SlateUser只有一个。对于主机和PC游戏来说,本地每多一个硬件输入设备(比如插多个手柄),SlateUser就会多一个。SlateUser里方法的功能主要分为以下几部分:
- 处理Focus相关的功能。设置、清除Focus等。
- 处理Capture相关的功能。设置、释放Capture,获得当前Capture的WidgetPath等。
- 记录和获取点击位置。
- 处理拖拽相关的功能。获得拖拽操作类FDragDropOperation,判断当前是否有拖拽等。
FWidgetPath、FArrangedChildren、FArrangedWidget
我们都知道,UI是一个树形结构。使用编辑器下的WidgetReflector可以清楚地看到当前选中的widget所在的树形结构。
在代码里就是用WidgetPath、ArrangedChildren、ArrangedWidget这三个类来表示这个结构。
ArrangedWidget是树的单个节点,包含一个SWidget和一个Geometry。
ArrangedChildren是节点数组的封装,它包含了一个ArrangedWidget的TArray,里面还有一些增删查的方法。
WidgetPath代表着一个Widget树的分支,它不仅包含了一个ArrangedChildren,还包含了一个树根节点的SWindow指针。(如果根节点不是SWindow则为空)
它们的关系如下:
FDirectPolicy、FToLeafmostPolicy、FTunnelPolicy、FBubblePolicy
这四个类位于FEventRouter内部。代表着遍历WidgetPath的四种策略。每一种策略类构造函数里都有WidgetPath。
这四种遍历策略的区别如下:
- FDirectPolicy只查找根节点,用于拖拽
- FToLeafmostPolicy只查找最后的叶子节点,用于Capture
- FTunnelPolicy从根节点往下遍历,用于PreviewMouseButton
- FBubblePolicy从叶子节点往上遍历,用于上面提到的以外的情况
在FEventRouter::Route里会根据遍历策略进行遍历,当RoutingPolicy的ShouldKeepGoing返回false且Reply为Handle时结束遍历。
FHittestGrid
FHittestGrid,是用于检测玩家当前会点击哪个widget的网格。包含在SWindow里面。FHittestGrid里的格子是FCell类。如图,最大的网格才是FHittestGrid,每个小格子是FCell。
上面也提到,WidgetPath是一个Widget树的一个分支。那么,代码里是怎么知道玩家当前选中的是哪个Widget?知道选中的Widget以后又怎么构造出一个WidgetPath呢?
相关的方法是FSlateApplication的LocateWidgetInWindow。
接下来简单分析GetBubblePath里的逻辑(因为代码较多就不贴了,原理就是网格法来进行空间划分,详细的可以看我文末的学习资料)。
- 根据点击的位置算出行数、列数,得出在哪个Cell
- 取出Cell里的Widget列表进行遍历,判断点是否在Widget内
- 得出第一个符合条件的Widget
- 根据Widget的父Widget构造整个WidgetPath
核心方法分析
接下来将对调用栈里最核心的三个方法进行分析。
ProcessMouseButtonDownEvent
由于代码量较多,接下来用伪代码进行说明。
{
if(SlateUser是否正在拖拽 == false)
{
if(SlateUser是否有Capture)
{
取出Capture的WidgetPath;
FEventRouter::Route对WidgetPath进行遍历,策略为FToLeafmostPolicy,遍历成功时的委托为发送PreviewMouseButtonDown事件;
if(PreviewMouseButtonDown返回的Reply没有Handle)
{
FEventRouter::Route对WidgetPath进行遍历,策略为FToLeafmostPolicy,遍历成功时的委托为
{
if(当前输入为Touch)发送TouchStart事件;
if(当前输入不是Touch)发送MouseButtonDown事件;
}
}
}
else
{
拿到顶层SWindow,从SWindow的HittestGrid获取WigdetPath;
调用RoutePointerDownEvent,传入WidgetPath和输入信息;
}
}
}
RoutePointerDownEvent
注意遍历策略和上面的方法使用的是不一样的。
{
SlateUser记录点击位置;
FEventRouter::Route对WidgetPath进行遍历,策略为FTunnelPolicy,遍历成功时的委托为发送PreviewMouseButtonDown事件;
if(PreviewMouseButtonDown返回的Reply没有Handle)
{
FEventRouter::Route对WidgetPath进行遍历,策略为FBubblePolicy,遍历成功时的委托为
{
if(当前输入为Touch)发送TouchStart事件;
if(当前输入不是Touch)发送MouseButtonDown事件;
}
if(如果是真Touch)
{
对每一个WidgetPath上的Widget发送OnMouseEnter事件;
}
}
处理鼠标的Focus;
}
FEventRouter::Route
Route里面就很简单了。根据传入的策略进行遍历,每次遍历后执行委托,委托返回的Reply为Handle时且遍历策略的ShouldKeepGoing返回false时结束遍历。
最后还会调用到Application的ProcessReply:
ProcessReply代码也比较多,但是和本文主要讨论的点击流程关系不大,这里简单说下这里面都做了什么事情:
- 处理Capture是否需要释放
- 处理Focus释放需要释放
- 处理拖拽
- 处理MouseLeave
- 处理Navigation
(注:UI的Navigation是只使用键盘或者手柄的游戏才有的一种系统)
总结
以上就是Widget收到点击事件的全流程分析。总结如下,在没有开启Capture的情况下 ,流程为:
- 根据点击位置,从HittestGrid获取WidgetPath
- 对WidgetPath根节点往下遍历,触发PreviewMouseButtonDown事件,如果返回的Replay为Handle则返回,没有则下一步
- 对WidgetPath叶子节点往上遍历,触发TouchStart或MouseButtonDown事件
- 调用SWidget的OnTouchStart或OnMouseButtonDown方法
学习资料
关于作者
- 水曜日鸡,喜欢ACG的游戏程序员。曾参与索尼中国之星项目《硬核机甲》的开发。 目前在某大厂做UE4项目。
CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
游戏同行聊天群:891809847
更多推荐
所有评论(0)