2023-06-29 11:55:02 +08:00

14 KiB
Raw Blame History

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

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 - CommonUIEditorTemplateTextStyleTemplateButtonStyleTemplateBorderStyle指定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()为例,其关键代码为:

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