Common UI
Common UI
UE5 - Common UI Guide: A Complete Guide to Inputs & Widgets
Setup
打开插件:

Project Setting设置窗口为CommonGameViewportClinet

Input Setting
在Project Setting中可以设置不同平台中的输入设置

首先新建一个输入数据表:

在该表中新建4个按键行为:

这里可以键盘和手柄上对应的按键和图片,其中的Gamepad Input Overrides可以设置其他手柄对应的按键。
接着创建InputData:

默认有设置对应的InputActionTable,这个表中几乎有所有按键的设置,如果需要可以直接复制其中的选项到我们新建的表即可。

在Project Setting中就可以设置输入数据:

接着创建Controller Data:

该数据是设置不同平台的不同设配的控制数据,这里PC就可以创建两个:

设置按键与图片:

在设置Gamepad Name时,需要和Project Setting中对应平台的Default Gamepad Name一致:

这里改不了名字Gamepad Name,这个是定义在DataDrivenPlatformInfo.ini文件中的。
Common UI Widget
创建UI:


Common User Widget

Common Activatable Widget

Lyra UI
该项目将Lyra中UI模块提取出独立成插件,以便于添加到不同项目中。这篇文档主要讲解如何将该插件接入项目以及一些说明。
Setup
打开插件:

Project Setting设置:



- 需要配置输入和默认UIPolicy,具体配置在下文中。
测试场景:/All/Plugins/GameCommonUIBase/MainMenu
Widget

GameUIMessagingSubsystem管理GameUIPolicy,GameUIPolicy决定使用什么样的PrimaryGameLayout,接着PrimaryGameLayout会有多个Layer,每个Layer是一个Switcher,用于控制当前Layer下面显示什么Panel。
GameUIManagerSubsystem
GameUIManager的主要作用就是负责UIPolicy的加载与切换,同时在玩家加入游戏时(本地多人)可以调用NotifyPlayerAdded等函数执行一些操作,并且调用UIPolicy中对应的方法。
同时每帧都会设置所有玩家的PrimaryGameLayout和HUD的可见性同步。
GameUIPolicy
GameUIPolicy的工作,一言以蔽之就是根据本地玩家的状态(添加、删除或销毁)和本地多人交互的模式(单人、轮换和多人),来决定将RootLayout向视口加入或删除。并且向外提供一些get函数。 另外需要注意UCLASS宏内的Within = GameUIManagerSubsystem说明GameUIPolicy只能由GameUIManagerSubsystem的实例创建,不能单独创建。
需要在Project Setting中配置:

PrimaryGameLayout
PrimaryGameLayout负责Layer的注册,以及Widget到Layer的push和remove。此外还有布尔类型的值bIsDormant和相关函数,用于给GameUIPolicy判断本地玩家的RootLayout是否显示在屏幕上。
PrimaryGameLayout就是一个UCommonUserWidget。
当前默认的布局如下:

- GameLayer是游戏内UI,类似于HUD,通常用于显示游戏状态,即生命条、武器槽等。
- GameMenu是游戏内的菜单,即背包等。
- Menu是游戏菜单,例如游戏开始界面、设置界面等。
- Modal是游戏弹窗,例如保存游戏或者更改游戏设置时弹窗的确定弹窗之类。
每个层就是一个UCommonActivatableWidgetStack,内部包含了一个Switcher,将Panel添加到Layer时,会将Panel添加到Switcher里,再通过设置switcher的index来切换Panel。
每一个Layer对应着一个GameplayTag:

将Panel加到Layer中使用:

将Panel移除Layer使用:

Split Screen
Lyra中为了实现分屏UI,设计了GameUIPolicy和PrimaryGameLayout。这里可以简单理解:
- 在多人游戏且在同一台机器上运行时,UI的规则是GameUIPolicy A,每一个玩家有一个PrimaryGameLayout A,也就是UI布局。
- 在单人游戏时,UI的规则GameUIPolicy B。现在的玩家的UI布局就是PrimaryGameLayout B
这样有些抽象,可以看下图:

由此可以知道一个玩家对应自己UI布局,在项目中使用LocalPlayer来定义的玩家信息。
根据不同玩家加载不同的UI布局触发流程:
- UGameUIGameInstance::AddLocalPlayer 有本地玩家加入到游戏
- UGameUIManagerSubsystem::NotifyPlayerAdded
- UGameUIPolicy::NotifyPlayerAdded 监听本地玩家控制器设置
- AGameUIPlayerController::ReceivedPlayer 本地玩家控器设置完成
- UGameUILocalPlayer::OnPlayerControllerSet 触发委托
- UGameUIPolicy::CreateLayoutWidget 创建布局UI(PrimaryGameLayout)
- UGameUIPolicy::AddLayoutToViewport 添加布局UI到视窗
所以需要需要将以下文件中的实现复制到项目中对应的文件中去:
- GameUILocalPlayer
- GameUIGameInstance
- GameUIPlayerController
同时需要在Project Setting和GameMode中设置对应。
值得注意的是如果游戏不需要分屏UI这种功能时,GameUIPolicy是可以不需要的,在游戏开始时直接将PrimaryGameLayout加载的视窗渲染即可。
Panel

因为Panel都是有Switcher管理的,所以所有的Panel都需要继承UGameUIActivatablePanel,也就是需要继承至UCommonActivatableWidget。这样才能有Switcher控制显示隐藏。
CommonActivatableWidget
- CommonActivatableWidget在Create出来后默认是Deactivate状态,需要手动Activate:

- 也可以在Details-Activation中勾选Auto Activate,勾选后会在Widget Create后自动Activate,Remove后自动Deactivate。

- CommonActivatableWidget可以覆写OnActivated和OnDeactivated来配置改变Activated状态后的回调:

- 必须实现GetDesiredFocusTarget方法,用于激活面板时聚焦那个Widget。当CommonActivatableWidget(及其子类对象)成为主激活控件时,可以在蓝图中通过重写这个函数来设置默认聚焦的控件对象。
- 请注意: 当勾选了bAutoRestoreFocus时,系统在恢复聚焦时会先检查是否保存了有效的聚焦对象(Widget)。如果该对象不存在,系统才会调用 GetDesiredFocusTarget。
- RequestRefreshFocus 允许在满足特定条件(当前节点是叶节点)的情况下,重新设置焦点到期望的焦点目标,而不需要对每个元素都进行Wrap.
- 蓝图:

- 即使CommonActivatableWidget被Collapsed了,如果CommonActivatableWidget没有被Deactivate的话,依然会影响输入路由。
- 默认情况下CommonActivatableWidget的Activate与Deactivate是不会影响Widget的Visibility的,可以在Details-Activation中单独配置:

- OnHandleBackAction :当CommonActivatableWidget(及其子类对象)被激活为主控件时,通常需要处理返回操作的相关逻辑。为了方便处理返回事件,CommonUI提供了一些配置,当触发返回输入时,可以调用BP_OnHandleBackAction函数来优化返回事件的处理逻辑。这样可以使返回事件的绑定逻辑更加清晰和易于维护。
Slot Panel
其中有一个特殊的Panel - GameUIHUDLayoutPanel,用于制作游戏中的HUD也就是战斗中UI。特殊点是它使用GameUISlotPointWidget对子Widget进行占位,使用GameplayTag对应子Widget,动态加载不同的子Widget。
这样做到子Widget和Panel之间的完全解耦。
其中WBP_GameHUDLayout如下图:

在GameUISlotPointWidget对应配置:

加载GameplayTag对应的widget和卸载:

子Widget:

最终显示效果:

其原理是在GameUISlotPointWidget在构造时,会去执行RebuildWidget方法,在该方法把SlotPointTag注册到GameUISlotSubSystem中,然后存储起来。
当调用GameUISlotSubSystem::RegisterSlot时,也就是上图。会去查询SlotPointTag是否有GameUISlotPointWidget注册过。如果有就将子widget创建出来。
值得注意的是,如果没有需要Panel中布局是一定的,内容会根据情况不同的需求时。直接使用完整的panel,做会更加只管一些,不一定非要用这中方式。
Message

调用方式:

也可以直接调用:
- UGameUIMessagingSubsystem::ShowConfirmation
- UGameUIMessagingSubsystem::ShowError
需要在Project Setting中配置默认的Panel:

Widget
在Widget目录下有一些复写的基础控件
InputActionWidget
一般会配合着按键使用,作为子Widget放在按键的Widget蓝图中。如果配置Input Actions也就是下图:

意味着指定按键对应该触发点击事件。如果不指定就是默认按键触发点击事件。
Button
基础按键结构如下:

Tab

设置选项卡配置:

每一个选项都要可以设置:
- 选项按钮
- 选项内容Widget
需要将内容控制的Switcher注册到选项列表组件中:

Widget Focus

Input
在Common UI中一定不要使用任何关于Enhanced Input相关的设置,Common UI对Enhanced Input的适配并没有非常的完善。如果Gameplay中使用了Enhanced Input,也会跟UI的输入打架。所以这里就是使用Common Input即可。
在引擎中,处理输入的流程大概如下:

CommonUI主要对上图中的两个红色区域进行了一些自定义输入的扩展。
Setup
- 首先新建一个输入数据表

- 在该表中新建4个按键行为

- 接着创建InputData

- 默认有设置对应的InputActionTable,这个表中几乎有所有按键的设置,如果需要可以直接复制其中的选项到我们新建的表即可。

- 在Project Setting中就可以设置输入数据

- 接着创建Controller Data

- 该数据是设置不同平台的不同设配的控制数据,这里PC就可以创建两个

- 设置按键与图片

- 在设置Gamepad Name时,需要和Project Setting中对应平台的Default Gamepad Name一致

Panel
GameUIActivatablePanel的InputConfig中可以配置该Widget的输入模式,一旦Widget被Activate,会立刻切换至该Widget的输入模式:

- Default:默认是GamePlay和Widget都可以接收输入。
- Game and Menu:同理。
- Game:只有Gameplay能够接收输入,自动隐藏鼠标。
- Menu:只有Widget能够接收输入,对玩家的控制等都会被屏蔽,且会自动显示鼠标。
也可以配置鼠标的捕获模式:

- NoCapture:鼠标不会捕获此窗口的鼠标事件。
- CapturePermanently:此窗口会捕获玩家的鼠标事件。
- CapturePermanentlyIncludingInitialMouseDown:会捕获玩家的鼠标事件,并且除了鼠标点击外还能够处理鼠标按下的事件。
- CaptureDuringMouseDown:鼠标按下触发捕获,抬起releases。
- CaptureDuringRightMouseDown:鼠标右键按下触发捕获。
CommonAnalogCursor
为了让Gamepad操作沿用鼠标的处理逻辑,CommonUI引入了合成光标FCommonAnalogCursor。它可以通过摇杆/Dpad导航到一个可交互的UI控件上,然后通过点击“EKeys::Virtual_Accept”按键来模拟鼠标左键点击UI
继承结构:

为了在使用Gamepad操作时显示光标,我们需要执行以下控制台命令:
- CommonUI.AlwaysShowCursor 1
- CommonInput.EnableGamepadPlatformCursor 1
FAnalogCursor类中实现了摇杆移动多少,鼠标就移动多少的逻辑。对于不想通过导航来选中UI的情况,可以考虑使用这种方式。目前在Common中,它只被当做调试时可用的选项。如果想要了解更多,请在FCommonAnalogCursor中搜索关键字bIsAnalogMovementEnabled
通过Gamepad按键来模拟鼠标点击的执行流程如下:

光标位置的更新逻辑在FCommonAnalogCursorVirtual_Accept”按键来点击该UI.
光标移动到聚焦控件的简化逻辑如下:

Default Input
如果想要修改默认按键映射,也就是默认手柄确认按键是A,如果想要修改为Y的话。直接修改Input Data中的配置并不能生效。使用Common Input的话,需要修改源码,也就是下面这里:

所以并不建议修改默认按键映射。
EKeysVirtual_Accept对应于“Gamepad_FaceButton_Bottom”
InputCoreTypes.cpp
const FKey EKeys::Virtual_Accept = FPlatformInput::GetGamepadAcceptKey();
| ControlType | EKey | Key | File |
|---|---|---|---|
| Snoy | Gamepad_FaceButton_Bottom | X | |
| XBOX360 | Gamepad_FaceButton_Bottom | A | |
| Switch | Gamepad_FaceButton_Right | A | SwitchPlatformInput.cpp |
输入预处理层的扩展
CommonUI对输入预处理进行了扩展,新增了两个输入预处理类:
FCommonAnalogCursor:这个类主要实现了通过光标模拟鼠标点击UI和使用摇杆滚动UI的逻辑。
FCommonEnhancedInputProcessor:用于CommonUI监听和处理输入设备切换事件以及过滤指定输入类型的处理。
GameViewportClient层输入的扩展:CommonUI在GameViewportClient层进行了自定义扩展,主要实现了UI绑定按键的功能。它会根据UI的激活状态、UI的层级以及所绑定按键的状态来执行UI的按键绑定逻辑。
Display Name

在按键上会被设置:

Reference
- https://www.youtube.com/watch?v=u06GAVxyIag
- https://www.youtube.com/watch?v=TTB5y-03SnE&t=494s
- https://www.youtube.com/watch?v=p_biHD6QZvA
- https://zhuanlan.zhihu.com/p/694293691
- https://zhuanlan.zhihu.com/p/18123648655
- https://zhuanlan.zhihu.com/p/598803136
- https://zhuanlan.zhihu.com/p/609657844
- https://zhuanlan.zhihu.com/p/506574041