21 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	title, date, excerpt, tags, rating
| title | date | excerpt | tags | rating | 
|---|---|---|---|---|
| CommonUI | 2023-03-31 09:53:26 | 摘要 | ⭐ | 
前言
CommonUI主要解决了层叠式UI输入事件处理以及多平台多输入设备管理问题。
其中比较重要的机制是CommonUI实现的输入路由功能,其代码位于:CommonGameViewportClient.h 和 CommonUIActionRouterBase.h 。
官方教学
- 文档
 - 视频
 
CommonUI
- 数据表
- CommonInputActionDataBase:各平台输入绑定。
 
 - 蓝图类资产
- CommonInputBaseControllerData:各平台按键数据(图标指定、鼠标键盘/手柄按键绑定)。在
ProjectSettings - Game - CommonInputSettings - PlatformInput - XX - Default - ControllerData中指定。 - CommonUIInputData:UI Click/Back Action 绑定用数据。之后在
ProjectSettings - Game - CommonInputSettings - InputData -中指定。- Comfirm/Cancel DataTable需要填写CommonInputActionDataBase数据表。
 
 
 - CommonInputBaseControllerData:各平台按键数据(图标指定、鼠标键盘/手柄按键绑定)。在
 
CommonUI特有功能
- ⭐⭐自带过渡效果:在Stack控件的
Detail-Transition中可以设置控件激活/注销时过渡效果,同时还可以设置效果曲线类型与曲线数值。默认是渐隐(Fade Only)。 - ⭐⭐⭐CommonActivatableWidgetStack:会自动激活Stack顶部Widget。
 - ⭐BackHandle:后退动作处理,除了勾选BackHandle与IsModel之外,需要还需重写OnHandleBackAction(),
 
CommonUI Setup&Data
- 启用CommonUI插件。
 Engine - GeneralSettings - DefaultClasses - GameViewportClientClass将其指定为CommonGameViewportClient或其子类。- 创建一个基于CommonInputActionDataBase的数据表,并且创建按键事件映射表。
 - 创建新的按钮类继承自`CommonButtonbase,并在TriggeringInputAction应用数据表
 - 创建一个继承自
CommonActivatbleWidget的界面并设置为自动激活,并将按钮放入其中。- 响应按键映射的前提是自己或某一Parent为已经激活的
CommonActivatbleWidget 
 - 响应按键映射的前提是自己或某一Parent为已经激活的
 
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控件基类主要定义了:
TArray<FUIActionBindingHandle> ActionBindings:来管理控件的ActionBindings。TArray<TWeakObjectPtr<const UWidget>> ScrollRecipients:滚动条控件。bool bConsumePointerInput:鼠标滑过控件时是否会产生反应。(为true时返回FReply::Handled())
其他都是相关虚函数以及工具函数。
CommonActivatableWidget
大致翻译官方注释的话: CommonActivatableWidget的基础,是能够在其生命周期内被 "activated "和 "deactivated"而不被其他方式修改或销毁。通常是为了以下几个目的:
- 该控件可以自由打开或者关闭,同时不会被从层次结构(WidgetTree)中移除。
 - 能从其他控件“退回”到这个控件(例如使用SwitchWidget或者StackWidget)。
 - 这个部件在层次结构中的位置是这样的:它在可激活的部件树中定义了一个有意义的结点,通过这个结点,输入被路由到所有部件。(不太懂这条,应该是不会破坏输入路由的意思)
 
同时它还具备以下性质:
- 创建后不会自动激活,需要调用函数手动activated。
 - 默认情况下不会注册BackAction。
 - 如果注册了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()为例,其关键代码为:
PersistentActions->ProcessHoldInput(ActiveMode, Key, InputEvent);
ActiveRootNode->ProcessHoldInput(ActiveMode, Key, InputEvent);
PersistentActions->ProcessNormalInput(ActiveMode, Key, Event);
ActiveRootNode->ProcessNormalInput(ActiveMode, Key, Event);
在对各种结果依次进行判断处理之后,最终返回结果:ERouteUIInputResult枚举。
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()来注册绑定信息。
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
 
# CommonUI Navigating
- How to Handle Input and Navigation in Unreal Motion Graphics (UMG) | Unreal Fest Gold Coast 2024
 - # UE5 - Common Ui: Enhanced Input Action Setup (Short Version)
 - # UE5 - Common UI: Keyboard Controls Setup
 
PS:
- 针对含有子Widget的控件(CommonActivatedWidget)需要勾选Auto Activate
 
相关属性
- Auto Activate:针对含有子Widget的控件(CommonActivatedWidget)需要勾选Auto Activate
 - Autp Restore Focus:这样Widget可以自动恢复焦点位置。
 - CommonActivatableWidget
- IsBackHandler
 
 - IsFocusable:勾选true
 
相关事件
- OnActivated:
 - OnDeactivated:
 - OnAddedToFocusPath:
 - OnRemoveFromFocusPath:
 
函数?
KeyBoardFocus
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());
				}
				
			}
		}
	}
}
CommonUI Use Enhanced Input
- 官方文档
 - 视频
 
- 创建InputAction资产。
 - 创建DataAesset(CommonMappingContextMetaData),为每个控制器都创建一个,比如Generic、PS5等
- EnhancedInputMetaData 选择 Common Input MetaData
 - 设置NavBarPriority(Epic的教程推荐为10,这样流出一些空余给后续更高优先级的输入)
 - 根据需求勾选IsGenericInputAction,这个输入是否在被UI接收的同时,发送到Character或者PlayerController中,并执行事件。
 - 添加创建的InputAction到PerActionEnhancedInputMetaData中。
 
 - 在InputAction的UserSettings - PlayerMappableKeySettings的MetaData中添加上一步创建CommonMappingContextMetaData
 - 创建IMC,添加InputAction并且设置按键。
 - 在ProjectSettings - Enginge - EnhancedInput中的DefaultMappingContext中添加上一步创建的IMC,并且勾选RegisterWithUserSettings。
 - 在CommonUI控件中添加IMC
- 在CommonButton的TriggerEnhancedInputAction中添加InputAction。
 - 另一个可以添加的就是TabList控件,TabList - Previous / Next Tab Input Action 中添加。
 
 
PS. CommonUI开启输入增强后,CommonUI InputData中也可以添加InputAction了。 主要是作为CommonUI中InputActionDataTable的补充(DataTable面向多平台通用输入),一些犄角旮旯的还是通过InputAction设置比较方便。
TODO
- 解决一些Button Release方面的颜色bug
 修复KeyBoard UI Nav的bug,鼠标&键盘同时使用问题逻辑就是判断在OnHovered事件中判断当前控件是否有UserFocus,没有的话就设置一个Hovered颜色与Normal颜色相同的Style。鼠标点击空白区域会导致丢失Focus的问题- 需要通过c++扩展来获得Focus, https://georgy.dev/posts/get-focused-widget/
 
- UI 控件扩展:KeySelector扩展,添加图标模型以及只有GamePad输入模式。
 - UI 控件扩展:CommonActivatedWidget => CommonActivatedInputActionWidget,参考CommonButton.
 - 声音没有保存设置。
 - 给其他控件实现类似CommonButton的bShouldUseFallbackDefaultInputAction的功能
 - Settings 界面添加设置说明
 - 建造模式 设置颜色与材质 UI。
 
其他手柄输出
- GameInput for Windows Plugin (需要Microsoft Game Development Kit 权限,需要填表注册)
 - Dualshock Input Plugin
 - 其他传统Input插件
- XInput(只支持XInput 手柄)
 - Raw Input Plugin
 
 
现在,我们通过新的“Game Input for Windows”插件,在虚幻引擎中更轻松地支持 PS4/5 控制器:Game Input for Windows - Experimental Release Notes
我想澄清一下关于虚幻引擎中的 Dualshock 输入插件的一些内容。不幸的是,目前这仅适用于开发人员安装了 PlayStation SDK 的项目,只有在 Sony PlayStation 开发网络上注册的工作室才能做到这一点。
如果您无法访问 Sony PlayStation 开发网络,目前没有简单的方法可以让 PS5 控制器“即插即用”,与我所知道的虚幻引擎游戏配合使用。