216 lines
14 KiB
Markdown
216 lines
14 KiB
Markdown
---
|
||
title: CommonUI
|
||
date: 2023-03-31 09:53:26
|
||
excerpt: 摘要
|
||
tags:
|
||
rating: ⭐
|
||
---
|
||
|
||
# 前言
|
||
CommonUI主要解决了**层叠式UI**输入事件处理以及**多平台**多输入设备管理问题。
|
||
其中比较重要的机制是CommonUI实现的**输入路由**功能,其代码位于:`CommonGameViewportClient.h` 和 `CommonUIActionRouterBase.h` 。
|
||
|
||
## 官方教学
|
||
- 文档
|
||
- https://docs.unrealengine.com/5.1/en-US/common-ui-quickstart-guide-for-unreal-engine/
|
||
- https://docs.unrealengine.com/5.1/en-US/overview-of-advanced-multiplatform-user-interfaces-with-common-ui-for-unreal-engine/
|
||
- 视频
|
||
- [Introduction to Common UI | Inside Unreal](https://www.youtube.com/live/TTB5y-03SnE?feature=share)
|
||
- [Lyra Cross-platform UI Development | Tech Talk](https://www.bilibili.com/video/BV1mT4y167Fm/?spm_id_from=333.999.0.0&vd_source=d47c0bb42f9c72fd7d74562185cee290)
|
||
|
||
# CommonUI
|
||
## CommonUI特有功能
|
||
1. ⭐⭐**自带过渡效果**:在Stack控件的`Detail-Transition`中可以设置控件激活/注销时过渡效果,同时还可以设置效果曲线类型与曲线数值。默认是**渐隐(Fade Only)**。
|
||
2. ⭐⭐⭐**CommonActivatableWidgetStack**:会自动激活Stack顶部Widget。
|
||
3. ⭐BackHandle:后退动作处理,除了勾选BackHandle与IsModel之外,需要还需重写OnHandleBackAction(),
|
||
|
||
## CommonUI Setup&Data
|
||
1. 启用CommonUI插件。
|
||
2. `Engine - GeneralSettings - DefaultClasses - GameViewportClientClass`将其指定为**CommonGameViewportClient**或其子类。
|
||
3. 创建一个基于CommonInputActionDataBase的数据表,并且创建按键事件映射表。
|
||
4. 创建新的按钮类继承自`CommonButtonbase,并在TriggeringInputAction应用数据表
|
||
5. 创建一个继承自`CommonActivatbleWidget`的界面并设置为自动激活,并将按钮放入其中。
|
||
1. 响应按键映射的前提是自己或某一Parent为已经激活的`CommonActivatbleWidget`
|
||
|
||
### CommonInputActionDataBase
|
||
默认的数据表可以填写**键盘**、**手柄**、**平板**的输入事件表。但对于其他主机平台(PS、Xbox、Switch)往往需要调换手柄按键位置,这个时候可以通过**GamepadInputOverrides**对指定平台进行按键修改。需要**安装主机平台SDK**后才会显示对应选项。
|
||
|
||
### CommonBaseControllerData
|
||
用于指定对应平台的**图标**资源。之后在`ProjectSettings - Game - CommonInputSettings - PlatformInput - XX - Default - ControllerData`中指定。
|
||
|
||
### CommonUIInputData
|
||
UI Click/Back Action 绑定用数据。之后在`ProjectSettings - Game - CommonInputSettings - InputData - `中指定。
|
||
|
||
## CommonStyle
|
||
与UMG、Slate不同,CommonUI没有通过**SlateWidgetStyle**而是通过继承对应的类来创建。每个CommonUI空间都有一个对应的Style类。之后可以在`ProjectSettings - Plugins - CommonUIEditor`的**TemplateTextStyle**、**TemplateButtonStyle**、**TemplateBorderStyle**指定CommonUI的默认风格。
|
||
|
||
## CommonUI控件
|
||
### UCommonUserWidget
|
||
CommonUI控件基类主要定义了:
|
||
1. `TArray<FUIActionBindingHandle> ActionBindings`:来管理控件的ActionBindings。
|
||
2. `TArray<TWeakObjectPtr<const UWidget>> ScrollRecipients`:滚动条控件。
|
||
3. `bool bConsumePointerInput`:鼠标滑过控件时是否会产生反应。(为true时返回FReply::Handled())
|
||
|
||
其他都是相关虚函数以及工具函数。
|
||
|
||
### CommonActivatableWidget
|
||
大致翻译官方注释的话:
|
||
CommonActivatableWidget的基础,是能够在其生命周期内被 "**activated** "和 "**deactivated**"而不被其他方式修改或销毁。通常是为了以下几个目的:
|
||
1. 该控件可以自由打开或者关闭,同时不会被从层次结构(WidgetTree)中移除。
|
||
2. 能从其他控件“退回”到这个控件(例如使用SwitchWidget或者StackWidget)。
|
||
3. 这个部件在层次结构中的位置是这样的:它在可激活的部件树中定义了一个有意义的结点,通过这个结点,输入被路由到所有部件。(不太懂这条,应该是不会破坏输入路由的意思)
|
||
|
||
同时它还具备以下性质:
|
||
1. 创建后不会自动激活,需要调用函数手动**activated**。
|
||
2. 默认情况下不会注册BackAction。
|
||
3. 如果注册了BackAction,那会在执行BackAction后自动**deactivated**。
|
||
|
||
主要实现了"**activated** "和 "**deactivated**"的相关逻辑,主要是可见性以及输入路由。
|
||
|
||
### UCommonActivatableWidgetContainerBase
|
||
添加的控件使用`FUserWidgetPool GeneratedWidgetsPool`控件池进行创建与生命周期管理。`TArray<TObjectPtr<UCommonActivatableWidget>> WidgetList`存储添加的控件指针。
|
||
|
||
内部使用SOverlay,来显示SCommonAnimatedSwitcher(用于显示控件)与SSpacer。
|
||
|
||
### CommonButtonBase
|
||
与其他的CommonUI组件不同,需要手动实现内部构造。
|
||
|
||
与普通的Button相比,拥有更好的可定制性,比如:
|
||
- 其Style增加了设置各种事件发出声音的选项。
|
||
- 拥有一个**Selectable**选项可以用来实现CheckBox或者多选框。
|
||
|
||
### 其他控件
|
||
- CommonActionWidget:继承自UWidget,用来显示指定游戏平台对应键盘/手柄按键图标。需要注意ProjectSettings-Game-CommonInputSettings中的DefaultGamepadName需要与CommonInputBaseControllerData资产的GamepadName相同,否则无法显示对应的按键图标。
|
||
- CommonActivatableWidgetSwitcher:继承自UCommonAnimatedSwitcher,重写几个虚函数来管理内部CommonActivatableWidget的 "**activated** "和 "**deactivated**"。
|
||
- CommonAnimatedSwitcher:继承自UWidgetSwitcher,主要实现了过渡动画(具体在Slate控件里)以及OnActiveWidgetIndexChanged、OnTransitioningChanged委托。
|
||
- CommonActivaableWidgetStack:继承自UCommonActivatableWidgetContainerBase,重写OnWidgetAddedToList(),添加控件时并显示末尾控件。
|
||
- CommonActivaableWidgetQueue:继承自UCommonActivatableWidgetContainerBase,重写OnWidgetAddedToList(),添加控件时并显示第一个控件。
|
||
- CommonBorder:继承UBorder,主要适配了CommonStyle。
|
||
- CommonHardwareVisibilityBorder:继承CommonBorder,重写OnWidgetRebuilt(),根据CommonInputSubsystem(ULocalPlayerSubsystem)的ComputedVisibilityTags,来决定是否显示。
|
||
- CommonCustomNavigation:继承UBorder,暴露一个FOnCustomNavigationEvent委托,用于定义自定义的Navigation行为。
|
||
- CommonTextBlock:继承自UTextBlock,主要适配了CommonStyle,并且添加了TextScroll。
|
||
- CommonDateTimeTextBlock:继承自UCommonTextBlock,主要存储了时间(FDateTime、int32、float)并对文字进行格式化,位于UpdateUnderlyingText(),提供了若干DateTime相关的方法。
|
||
- CommonNumericTextBlock:继承自UCommonTextBlock,实现了Number、Percentage、Seconds、Distance相关数字文字的格式化方法。
|
||
- CommonLazyImage:继承自UImage,使用异步加载Image或者Material,并在加载完之后替换图片。控制载入的函数为SetBrushFromLazyMaterial()、SetBrushFromLazyDisplayAsset(),在加载完成前显示默认图片。本质是使用UAssetManager::GetStreamableManager().RequestAsyncLoad()进行加载。其中MyLoadGuard(SLoadGuard)为旋转载入图标控件。
|
||
- CommonLazyWidget:继承自UWidget,默认Visibility为SelfHitTestInvisible。控制载入的函数为SetLazyContent(),本质是使用UAssetManager::GetStreamableManager().RequestAsyncLoad()进行加载。在RebuildWidget()与OnStreamingComplete()中设置MyLoadGuard与Content的可见性。
|
||
- CommonListVIew:继承自UListView,主要是重写OnGenerateEntryWidgetInternal()针对UCommonButtonBase进行适配;内部使用SCommonListView控件。
|
||
- CommonLoadGuard:继承自UContentWidget,维护一个MyLoadGuard(SLoadGuard)。并且暴露样式、委托以及SetLoadingText()、SetIsLoading()、IsLoading()。
|
||
- CommonVisibilitySwitcher:继承自UOverlay,一个适配了UCommonActivatableWidget的基础控件切换器。控件存在`TArray<TObjectPtr<UPanelSlot>> Slots;`。
|
||
- CommonBoundActionBar:继承自UDynamicEntryBoxBase,适配CommonUI输入控制的版本。主要在OnWidgetRebuilt()中调用MonitorPlayerActions()绑定CommonUI输入路由的OnBoundActionsUpdated委托。
|
||
- CommonWidgetCarousel:继承自UPanelWidget,卡片旋转式的WidgetSwitcher。
|
||
- CommonWidgetCarouselNavBar:继承自UWidget,控制CommonWidgetCarousel控件用的导航栏。
|
||
- List
|
||
- CommonTileView:继承自UTileView,替换了内部使用的Slate控件为SCommonTileView。并且重写OnGenerateEntryWidgetInternal()对UCommonButtonBase进行适配(替换成SCommonButtonTableRow)。
|
||
- CommonTreeView:继承自UTreeView,替换了内部使用的Slate控件为SCommonTreeView。并且重写OnGenerateEntryWidgetInternal()对UCommonButtonBase进行适配(替换成SCommonButtonTableRow)。
|
||
- Pannel
|
||
- CommonHierarchicalScrollBox:继承自UScrollBox,替换了内部使用的Slate控件为SCommonHierarchicalScrollBox。
|
||
- CommonVisualAttachment:继承自USizeBox,使用Slate控件为SVisualAttachmentBox。添加一个Widget作为另一个Widget的零尺寸附件。其设计目的为不改变Widget的尺寸计算。
|
||
|
||
# 输入路由相关
|
||
## UCommonGameViewportClient
|
||
主要重写InputKey()、InputAxis()、InputTouch(),之后通过HandleRerouteInput() 、HandleRerouteAxis()、HandleRerouteTouch()进行输入路由判断。其路由逻辑主要编写在
|
||
UCommonUIActionRouterBase,是一个ULocalPlayerSubsystem子类。
|
||
|
||
## UCommonUIActionRouterBase
|
||
主要用于处理注册/卸载ActionBindings、维护ActivateTree以及输入控制。
|
||
|
||
## Input Binding & Process
|
||
### Process
|
||
以ProcessInput()为例,其关键代码为:
|
||
```c++
|
||
PersistentActions->ProcessHoldInput(ActiveMode, Key, InputEvent);
|
||
ActiveRootNode->ProcessHoldInput(ActiveMode, Key, InputEvent);
|
||
PersistentActions->ProcessNormalInput(ActiveMode, Key, Event);
|
||
ActiveRootNode->ProcessNormalInput(ActiveMode, Key, Event);
|
||
```
|
||
|
||
在对各种结果**依次**进行判断处理之后,最终返回结果:ERouteUIInputResult枚举。
|
||
```c++
|
||
bool FActionRouterBindingCollection::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
|
||
{
|
||
for (FUIActionBindingHandle BindingHandle : ActionBindings)
|
||
{
|
||
if (TSharedPtr<FUIActionBinding> Binding = FUIActionBinding::FindBinding(BindingHandle))
|
||
{
|
||
if (ActiveInputMode == ECommonInputMode::All || ActiveInputMode == Binding->InputMode)
|
||
{
|
||
for (const FUIActionKeyMapping& KeyMapping : Binding->NormalMappings)
|
||
{
|
||
// A persistent displayed action skips the normal rules for reachability, since it'll always appear in a bound action bar
|
||
const bool bIsDisplayedPersistentAction = Binding->bIsPersistent && Binding->bDisplayInActionBar;
|
||
if (KeyMapping.Key == Key && Binding->InputEvent == InputEvent && (bIsDisplayedPersistentAction || IsWidgetReachableForInput(Binding->BoundWidget.Get())))
|
||
{
|
||
// Just in case this was in the middle of a hold process with a different key, reset now
|
||
Binding->CancelHold();
|
||
Binding->OnExecuteAction.ExecuteIfBound();
|
||
if (Binding->bConsumesInput)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool FActivatableTreeNode::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
|
||
{
|
||
if (IsReceivingInput())
|
||
{
|
||
for (const FActivatableTreeNodeRef& ChildNode : Children)
|
||
{
|
||
if (ChildNode->ProcessNormalInput(ActiveInputMode, Key, InputEvent))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return FActionRouterBindingCollection::ProcessNormalInput(ActiveInputMode, Key, InputEvent);
|
||
}
|
||
return false;
|
||
}
|
||
```
|
||
|
||
### Binding
|
||
可以看得出主要是ActionBindings,而其注册逻辑位于UCommonUIActionRouterBase::Tick()=>ProcessRebuiltWidgets=>RegisterWidgetBindings(),之后依次对构建的FActivatableTree中**CommonUserWidget**的绑定数据进行注册(RegisterWidgetBindings、RegisterInputTreeNode)。每个**CommonUserWidget**都使用这个RegisterUIActionBinding()来注册绑定信息。
|
||
```c++
|
||
FUIActionBindingHandle UCommonUserWidget::RegisterUIActionBinding(const FBindUIActionArgs& BindActionArgs)
|
||
{
|
||
if (UCommonUIActionRouterBase* ActionRouter = UCommonUIActionRouterBase::Get(*this))
|
||
{
|
||
FBindUIActionArgs FinalBindActionArgs = BindActionArgs;
|
||
if (bDisplayInActionBar && !BindActionArgs.bDisplayInActionBar)
|
||
{
|
||
FinalBindActionArgs.bDisplayInActionBar = true;
|
||
}
|
||
FUIActionBindingHandle BindingHandle = ActionRouter->RegisterUIActionBinding(*this, FinalBindActionArgs);
|
||
ActionBindings.Add(BindingHandle);
|
||
return BindingHandle;
|
||
}
|
||
|
||
return FUIActionBindingHandle();
|
||
}
|
||
```
|
||
|
||
其绑定参数`FBindUIActionArgs`数据为:
|
||
|
||
| | |
|
||
| ---------------------- | ----------------------------------------------------------------- |
|
||
| ActionTag | 目前没找到用处,可能用于按下时给角色添加按下的Tag |
|
||
| LegacyActionTableRow | 按键的映射表行 |
|
||
| InputMode | 输入模式,只有当当前输入模式覆盖了Key的输入模式才触发事件,默认all |
|
||
| KeyEvent | 什么状态下触发(按下、松开、etc...) |
|
||
| bIsPersistent | 持久化,为true则无视Activate规则始终响应按键输入 |
|
||
| bConsumeInput | 是否消耗掉本次输入(相同按键绑定不再能触发),默认为true |
|
||
| bDisplayInActionBar | 是否在CommonActionBar上实时显示按键提示 |
|
||
| OverrideDisplayName | 新的显示名 |
|
||
| OnExecuteAction | 当按键触发时执行的回调函数 |
|
||
| OnHoldActionProgressed | 当按住时的回调 |
|
||
|
||
# Lyra中的用法
|
||
- Safe-Zone
|
||
|
||
创建了若干控件:
|
||
- MaterialProgressBar:继承自UCommonUserWidget
|
||
- LyraBoundActionButton:继承自UCommonBoundActionButton |