Files
BlueRoseNote/03-UnrealEngine/UI/CommonUI.md

385 lines
21 KiB
Markdown
Raw Normal View History

2025-08-02 12:09:34 +08:00
---
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各平台输入绑定。
- 蓝图类资产
- CommonInputBaseControllerData各平台按键数据图标指定、鼠标键盘/手柄按键绑定)。在`ProjectSettings - Game - CommonInputSettings - PlatformInput - XX - Default - ControllerData`中指定。
- CommonUIInputDataUI Click/Back Action 绑定用数据。之后在`ProjectSettings - Game - CommonInputSettings - InputData - `中指定。
- Comfirm/Cancel DataTable需要填写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
2025-08-31 22:06:51 +08:00
- LyraBoundActionButton继承自UCommonBoundActionButton
# # CommonUI Navigating
2025-09-02 17:49:10 +08:00
- [x] [How to Handle Input and Navigation in Unreal Motion Graphics (UMG) | Unreal Fest Gold Coast 2024](https://www.youtube.com/watch?v=kZUwEEtkItg)
2025-09-04 11:49:18 +08:00
- [ ] [# UE5 - Common Ui: Enhanced Input Action Setup (Short Version)](https://www.youtube.com/watch?v=17nARa4jruc)
- [ ] [# UE5 - Common UI: Keyboard Controls Setup](https://www.youtube.com/watch?v=aDoNLyUVy2o)
2025-09-02 12:08:31 +08:00
PS:
1. 针对含有子Widget的控件CommonActivatedWidget需要勾选Auto Activate
## 相关属性
- Auto Activate针对含有子Widget的控件CommonActivatedWidget需要勾选Auto Activate
- Autp Restore Focus这样Widget可以自动恢复焦点位置。
2025-09-02 15:59:13 +08:00
- CommonActivatableWidget
- IsBackHandler
2025-09-02 16:47:32 +08:00
- IsFocusable勾选true
2025-09-02 12:08:31 +08:00
## 相关事件
- OnActivated
- OnDeactivated
2025-09-02 17:49:10 +08:00
- OnAddedToFocusPath
2025-09-03 11:07:14 +08:00
- OnRemoveFromFocusPath
## 函数?
2025-09-03 13:52:28 +08:00
KeyBoardFocus
```c++
void UWidget::SetKeyboardFocus()
{
TSharedPtr<SWidget> SafeWidget = GetCachedWidget();
if (SafeWidget.IsValid())
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if ( !SafeWidget->SupportsKeyboardFocus() )
{
FMessageLog("PIE").Warning(FText::Format(LOCTEXT("ThisWidgetDoesntSupportFocus", "The widget {0} does not support focus. If this is a UserWidget, you should set bIsFocusable to true."), FText::FromString(GetNameSafe(this))));
}
#endif
if ( !FSlateApplication::Get().SetKeyboardFocus(SafeWidget) )
{
if ( UWorld* World = GetWorld() )
{
if ( ULocalPlayer* LocalPlayer = World->GetFirstLocalPlayerFromController() )
{
LocalPlayer->GetSlateOperations().SetUserFocus(SafeWidget.ToSharedRef(), EFocusCause::SetDirectly);
}
}
}
}
}
void UWidget::SetFocus()
{
SetUserFocus(GetOwningPlayer());
}
void UWidget::SetUserFocus(APlayerController* PlayerController)
{
if ( PlayerController == nullptr || !PlayerController->IsLocalPlayerController() || PlayerController->Player == nullptr )
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
FMessageLog("PIE").Error()->AddToken(FTextToken::Create(LOCTEXT("NoPlayerControllerToFocus", "The PlayerController is not a valid local player so it can't focus on ")))->AddToken(FUObjectToken::Create(this));
#endif
return;
}
TSharedPtr<SWidget> SafeWidget = GetCachedWidget();
if ( SafeWidget.IsValid() )
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if ( !SafeWidget->SupportsKeyboardFocus() )
{
TSharedRef<FTokenizedMessage> Message = FMessageLog("PIE").Warning()->AddToken(FUObjectToken::Create(this));
#if WITH_EDITORONLY_DATA
if(UObject* GeneratedBy = WidgetGeneratedBy.Get())
{
Message->AddToken(FTextToken::Create(FText::FromString(TEXT(" in "))))->AddToken(FUObjectToken::Create(GeneratedBy));
}
#endif
if (IsA(UUserWidget::StaticClass()))
{
Message->AddToken(FTextToken::Create(LOCTEXT("UserWidgetDoesntSupportFocus", " does not support focus, you should set bIsFocusable to true.")));
}
else
{
Message->AddToken(FTextToken::Create(LOCTEXT("NonUserWidgetDoesntSupportFocus", " does not support focus.")));
}
}
#endif
if ( ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer() )
{
TOptional<int32> UserIndex = FSlateApplication::Get().GetUserIndexForController(LocalPlayer->GetControllerId());
if (UserIndex.IsSet())
{
FReply& DelayedSlateOperations = LocalPlayer->GetSlateOperations();
if (FSlateApplication::Get().SetUserFocus(UserIndex.GetValue(), SafeWidget))
{
DelayedSlateOperations.CancelFocusRequest();
}
else
{
DelayedSlateOperations.SetUserFocus(SafeWidget.ToSharedRef());
}
}
}
}
}
2025-09-03 15:53:16 +08:00
```
2025-09-04 18:14:03 +08:00
# CommonUI Use Enhanced Input
- 官方文档
- [Using CommonUI With Enhanced Input](https://dev.epicgames.com/documentation/en-us/unreal-engine/using-commonui-with-enhnaced-input-in-unreal-engine)
- 视频
- [UE5 - Common Ui: Enhanced Input Action Setup (Short Version)](https://www.youtube.com/watch?v=17nARa4jruc)
1. 创建InputAction资产。
2. 创建DataAesset(CommonMappingContextMetaData)为每个控制器都创建一个比如Generic、PS5等
1. EnhancedInputMetaData 选择 Common Input MetaData
2. 设置NavBarPriorityEpic的教程推荐为10这样流出一些空余给后续更高优先级的输入
3. 根据需求勾选IsGenericInputAction**这个输入是否在被UI接收的同时发送到Character或者PlayerController中并执行事件**。
4. 添加创建的InputAction到PerActionEnhancedInputMetaData中。
3. 在InputAction的UserSettings - PlayerMappableKeySettings的MetaData中添加上一步创建CommonMappingContextMetaData
4. 创建IMC添加InputAction并且设置按键。
5. 在ProjectSettings - Enginge - EnhancedInput中的DefaultMappingContext中添加上一步创建的IMC并且勾选RegisterWithUserSettings。
6. 在CommonUI控件中添加IMC
1. 在CommonButton的TriggerEnhancedInputAction中添加InputAction。
2. 另一个可以添加的就是TabList控件TabList - Previous / Next Tab Input Action 中添加。
PS. CommonUI开启输入增强后**CommonUI InputData**中也可以添加InputAction了。
主要是作为CommonUI中InputActionDataTable的补充DataTable面向多平台通用输入一些犄角旮旯的还是通过InputAction设置比较方便。
2025-09-03 15:53:16 +08:00
# TODO
2025-09-04 11:49:18 +08:00
1. [x] 解决一些Button Release方面的颜色bug
2025-09-05 10:05:55 +08:00
2. [ ] ~~修复KeyBoard UI Nav的bug鼠标&键盘同时使用问题~~ 逻辑就是判断在OnHovered事件中判断当前控件是否有UserFocus没有的话就设置一个Hovered颜色与Normal颜色相同的Style。
2025-09-04 13:17:54 +08:00
3. [ ] ~~鼠标点击空白区域会导致丢失Focus的问题~~
2025-09-04 19:43:52 +08:00
1. [ ] 需要通过c++扩展来获得Focus, https://georgy.dev/posts/get-focused-widget/
2025-09-04 11:49:18 +08:00
4. KeySelector扩展添加图标模型以及只有GamePad输入模式。
5. 声音没有保存设置。
6. 给其他控件实现类似CommonButton的bShouldUseFallbackDefaultInputAction的功能
7. Settings 界面添加设置说明
2025-09-04 10:01:12 +08:00
## 其他手柄输出
2025-09-04 11:49:18 +08:00
- GameInput for Windows Plugin (需要[Microsoft Game Development Kit](https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/input/overviews/input-overview) 权限,需要填表注册)
2025-09-04 10:01:12 +08:00
- Dualshock Input Plugin
2025-09-04 11:49:18 +08:00
- 其他传统Input插件
- XInput(只支持XInput 手柄)
- Raw Input Plugin
2025-09-04 10:01:12 +08:00
现在我们通过新的“Game Input for Windows”插件在虚幻引擎中更轻松地支持 PS4/5 控制器:[Game Input for Windows - Experimental Release Notes](https://dev.epicgames.com/community/learning/tutorials/EpZ4/unreal-engine-game-input-for-windows-experimental-release-notes)
我想澄清一下关于虚幻引擎中的 **Dualshock 输入插件**的一些内容。不幸的是,目前这仅适用于开发人员安装了 **PlayStation SDK** 的项目,只有在 Sony PlayStation 开发网络上注册的工作室才能做到这一点。
如果您无法访问 Sony PlayStation 开发网络,目前没有简单的方法可以让 PS5 控制器“即插即用”,与我所知道的虚幻引擎游戏配合使用。