220 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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
- 数据表
- CommonInputActionDataBase各平台输入绑定。
## 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()根据CommonInputSubsystemULocalPlayerSubsystem的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()进行加载。其中MyLoadGuardSLoadGuard为旋转载入图标控件。
- 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