1. 基本框架(CommonUI + CommonGame 分层)

基础框架

  • 大致UI框架如上图所示(简化了部分),最顶层的 Layout 由 GameUISubSystem 驱动创建,创建的时机是 LocalPlayer 被添加的时候,具体由 GameUIPolicy 来执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void UGameUIPolicy::NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer)
{
LocalPlayer->OnPlayerControllerSet.AddWeakLambda(this, [this](UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController)
{
NotifyPlayerRemoved(LocalPlayer);

if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
{
AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout);
LayoutInfo->bAddedToViewport = true;
}
else
{
CreateLayoutWidget(LocalPlayer);
}
});

if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer))
{
AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout);
LayoutInfo->bAddedToViewport = true;
}
else
{
CreateLayoutWidget(LocalPlayer);
}
}
  • Layout 里面具体定义哪些层级由 GameUIPolicy的LayoutClass 决定,可自行配置,我参考的是 lyra 项目定义的四层结构,每层的含义如下:
    • Game —— 游戏界面元素,例如 HUD。
    • GameMenu —— 专用于游戏过程的“菜单”,例如游戏内的物品栏界面。
    • Menu —— 常规菜单界面,例如设置画面。
    • Modal —— 模态对话框,例如确认提示框或错误提示框。
  • LayoutClass 本身是一个 CommonUserWidget, 如下图:

LayoutClass

  • 每个 Layer 是一个CommonActivatableWidgetStack,是一个栈结构,每次 push / pop 只激活栈顶的ActivatableWidget。四个 Layer 之间,越靠下,优先级越高,Modal 优先级最高,Game 优先级最低,优先级高的 Layer 会优先拦截输入

1.1 为什么要做 Layer 分层?

CommonUI 之所以要把 UI 拆成多个 Layer,本质上是把 UI 的 空间顺序、输入优先级、生命周期、平台行为 四个维度统一抽象为一个 GameplayTag 驱动的声明式系统,避免传统 UMG 中各种手动管理 ZOrder / Focus / Visibility / InputMode 带来的混乱问题。具体价值体现在以下几个方面:

  1. Z-Order(绘制顺序)的确定性管理

    • 传统 UMG 中通过 AddToViewport(ZOrder) 手动指定数值,多个模块各自约定数值时极易冲突(比如 A 模块和 B 模块都用了 ZOrder=100)。
    • Layer 机制下,每个 Layer 在 Layout 中按顺序排布,越靠后的 Layer ZOrder 越高,Modal 永远盖在 Menu 之上,Menu 永远盖在 Game 之上,无需手动协调。
  2. 输入路由(Input Routing)与焦点管理

    • 这是 Layer 分层最核心的价值,也是 UMG 原生最薄弱的部分。
    • UCommonUIActionRouterBase 依据 Layer 决定:当前应使用哪种 Input Mode、哪个 Widget 接收按键、是否捕获鼠标、是否显示光标。
    • Modal Layer 推入新 Widget 时,焦点会自动从下层 Layer 转移过来;Modal 关闭后焦点自动归还,完全消除「点击穿透」「焦点丢失」这一类 Bug
  3. 生命周期与激活栈(Activation Stack)

    • 每个 Layer 对应一个 CommonActivatableWidgetStack,遵循 栈语义(LIFO):Push 时旧 Widget Deactivate(保留实例)、新 Widget Activate;Pop 时新 Widget 销毁、旧 Widget 重新 Activate。
    • 这意味着「主菜单 → 设置 → 音频设置 → 按 B 返回」这种多级菜单导航天然支持,不必重建上一级页面,性能更优、状态自动保留
  4. 平台无关的输入抽象(PC / 主机 / 移动端)

    • CommonUI 诞生于《堡垒之夜》的跨平台需求,Layer 在这里承担了「不同界面层使用不同输入模式」的职责。
    • 例如 Game Layer 用 ECommonInputMode::Game(输入透传给角色),Menu Layer 用 ECommonInputMode::Menu(输入只给 UI),框架根据当前最顶层 Layer 自动切换,无需手写一堆 if (IsController) ... else ...
  5. 流程控制的天然隔离

    • 加载界面、网络断连提示、确认弹窗这类 必须打断一切其他交互 的 UI,天然属于最顶层的 Modal Layer。
    • 推入 Modal Layer 后,下层 Game / Menu 的输入会被自动屏蔽,无需写任何「禁用其他 UI」的额外逻辑

可以用一个对照表直观地看出 Layer 解决了哪些痛点:

痛点 传统 UMG CommonUI + Layer
Z-Order 冲突 手动协调数字 Layer Tag 天然有序
输入穿透 需手动 SetVisibility(HitTestInvisible) 自动屏蔽下层
焦点丢失(手柄) 手动 SetUserFocus,常出 Bug 框架自动管理
返回上一级菜单 自己实现栈 内置 Stack 语义
Modal 弹窗 自己写遮罩 + 禁用按钮 Push 到 Modal Layer 即可
跨平台输入切换 大量 if/else InputMode 声明式配置

2. Widget 类型及其应用场景

  • UE 中 UI widget 有三种类型,UCommonActivatableWidget,UCommonUserWidget和UUserWidget, 三者的关系是:UCommonActivatableWidget 继承自 UCommonUserWidget,而 UCommonUserWidget 继承自 UUserWidget。
  • 下面是它们的特点介绍和具体应用场景:

    1. UUserWidget (最基础的 UMG 原生类)
      • 概念:它是 UE 引擎最原始、最基础的 UI 蓝图父类。所有你在 UE 里右键创建的普通 Widget Blueprint 默认都是它。
      • 特点:
        1. “傻瓜式”渲染: 它只负责显示图片、文字,播放简单的 UI 动画。
        2. 缺乏系统性输入管理: 它接收点击、悬停,但不知道什么是“手柄导航”、什么是“全局返回键”、什么是“UI层级”。
        3. 无状态管理: 只有“创建(Construct)”和“销毁(Destruct)”,没有“激活”和“挂起”的概念。
      • 应用场景:
        1. 不需要复杂交互的 HUD: 屏幕上的血条、小地图、准星、得分显示。
        2. 非 CommonUI 项目: 如果你的项目是简单的手机游戏,或者纯鼠标点击的 PC 游戏,完全不需要引入 CommonUI,直接全部用 UUserWidget 即可。
        3. 极简的子控件: 一个纯展示用的底板背景、一条纯文本的分隔线。
    2. UCommonUserWidget (CommonUI 的基础组件)
      • 概念: 它是 CommonUI 插件提供的一个扩展基类。它本质上还是一个普通控件,但它“听得懂” CommonUI 系统的语言。它的核心价值是接入了 CommonUI 的 Input Action 路由系统 (UCommonUIActionRouterBase),并把若干本地玩家级子系统的访问入口封装成了便捷 getter。
      • 特点:
        1. 内置 CommonUI 输入路由接入: 通过 RegisterUIActionBinding(...) 可声明式地绑定一个 FDataTableRowHandle(指向 CommonInputActions 数据表的某一行),由 UCommonUIActionRouterBase 根据当前输入设备自动分发按键事件,并可联动底部 Action Bar 的按键提示。这套机制 UUserWidget 不具备。
        2. 便捷访问 CommonUI 子系统: 直接提供 GetInputSubsystem()GetActionRouter()、以及重写后返回 UCommonLocalPlayer*GetOwningLocalPlayer<T>() 等便捷接口,省去手写 GetGameInstance()->GetSubsystem<...>() 的样板代码。
        3. 额外的输入提示开关: 暴露 bDisplayInputActionWhenNotInteractablebAlwaysListenForInputAction 等属性,控制 Action 提示在控件不可交互时是否仍显示、绑定是否在隐藏后仍持续监听。
        4. 不拦截输入、无激活/失活生命周期: 它依然是一个“零件”,没有独占输入的能力,必须依附于更大的页面(典型情况下是某个 UCommonActivatableWidget)。
      • 关于“感知输入设备切换”的澄清: 设备切换检测本身由 UCommonInputSubsystem 提供(一个 ULocalPlayerSubsystem),任何能拿到 LocalPlayerUUserWidget 也能订阅它的 OnInputMethodChangedNative 委托来响应键鼠↔手柄切换。UCommonUserWidget 只是把访问入口封装为 GetInputSubsystem(),并非独有能力。
      • 关于“样式系统”的澄清: CommonUI 的 Style 资产(UCommonButtonStyleUCommonTextStyle 等)作用于 UCommonButtonBaseUCommonTextBlock 这类具体控件不是 UCommonUserWidget 自身的能力;它本身并不持有任何 Style 资产,也不参与所谓“全局换肤”。
      • 应用场景(作为子控件):
        1. 需要参与 Input Action 路由的功能型组件: 例如一个自定义面板,希望声明“按 X / F 接受、按 B / Esc 取消”,并自动出现在底部 Action Bar 中。
        2. 复杂的自定义组件: 比如“角色属性条”(包含图标、文字、进度条),其内部按钮多为 UCommonButtonBase,作为容器使用 UCommonUserWidget 可顺手访问 ActionRouter / InputSubsystem
        3. 动态提示控件: 一个提示面板,当玩家用键盘时显示 [F] 拾取,切到手柄时瞬间变成 [X] 拾取(注:此场景 UUserWidget 也能做到,只是用 UCommonUserWidget 写起来更简洁)。
    3. UCommonActivatableWidget (具有路由能力的完整页面/弹窗)
      • 概念:它是 CommonUI 的核心灵魂。它不再是一个“零件”,而是一个具有生命周期、能独占输入、能管理焦点的“完整页面”或“模态弹窗”
      • 特点:
        1. 状态管理: 引入了 Activate (激活) 和 Deactivate (失活) 的概念。页面可以被隐藏挂起,而不是直接销毁。
        2. 输入模式切换:GetDesiredInputConfig() — 激活时自动切换输入模式。
        3. 接管焦点: 当它被 Activate 时,它可以自动把手柄/键盘焦点抓取到自己设定的默认按钮上。
        4. 模态支持:bIsModal — 作为输入路由的根节点,阻断父级操作。
        5. 自带“返回”逻辑:原生支持 Back Handler。玩家按 ESC 或手柄 B键 时,它会自动响应并关闭自己(Deactivate),而不需要你写一堆按键绑定事件。
        6. 配合 UI 栈 (Stack):它可以被推入(Push)到 CommonActivatableWidgetStack 中,实现像浏览器网页一样的“前进、后退”历史记录。
      • 应用场景(作为大页面/弹窗):
        1. 全屏主菜单: 游戏开始画面的主菜单(包含开始游戏、设置、退出)。
        2. 暂停菜单界面 (Pause Menu): 游戏中按 ESC 弹出的菜单,激活时阻断游戏底层输入。
        3. 设置页面 (Settings Screen): 以及里面的“音频设置”、“画面设置”等各个大分页。
        4. 确认弹窗 (Modal Dialog): “确定要删除存档吗?[是/否]” —— 这种必须玩家处理完才能进行下一步的弹窗。
        5. 背包/角色全屏界面 (Inventory UI): 玩家打开背包时,整个屏幕的控制权交给这个界面。
    4. 推荐的层级结构

      • 基于上述三种 Widget 的特点,推荐采用 「Activatable 当页面 / Common 当组件 / User 当零件」 的分层组合方式:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        UCommonActivatableWidget (页面级根节点,提供输入路由 / 焦点 / 返回栈)

        ├── UCommonUserWidget (功能型子控件,参与 CommonUI Input Action 路由)
        │ │
        │ ├── UCommonUserWidget (更细粒度的子组件,例如带图标的按钮)
        │ │
        │ └── UUserWidget (纯展示零件,例如底板 / 分隔线 / 装饰图)

        ├── UCommonUserWidget (另一个功能型子控件,例如属性条)
        │ │
        │ └── UUserWidget (纯展示的进度条贴图 / 文本)

        └── UUserWidget (页面内不需要参与输入路由的纯展示元素)
      • 分层原则:

        1. 页面级(顶层)必须用 UCommonActivatableWidget:负责输入独占、焦点管理、返回键处理、压栈出栈等”页面”职责。一个全屏菜单 / 弹窗 / HUD 主面板就是一个 Activatable。
        2. 中间组件优先用 UCommonUserWidget:当这个控件需要参与 CommonUI 的 Input Action 路由(声明式按键绑定、Action Bar 提示),或需要便捷访问 ActionRouter / InputSubsystem 时,使用它。注意:单纯响应“键鼠 ↔ 手柄”切换并不构成必须用它的理由——UUserWidget 通过订阅 UCommonInputSubsystem::OnInputMethodChangedNative 同样可以做到。
        3. 末端纯展示控件用 UUserWidget:仅用于显示图片 / 文字 / 简单动画,不参与输入路由、不关心输入设备、不需要全局样式联动的”叶子节点”。
        4. 避免反向嵌套:不要把 UCommonActivatableWidget 当作普通子控件嵌进 UUserWidget 里,否则它的输入路由和返回栈能力会失效(因为父节点不在 CommonUI 的输入路由树上)。

3. 输入模式的切换

  • ActivatableWidget 有一个 GetDesiredInputConfig 方法用于返回当前的输入模式给输入系统,输入系统通过不同的输入模式来启用对应的 IMC(InputMappingContext)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TOptional<FUIInputConfig> ULyraActivatableWidget::GetDesiredInputConfig() const
{
switch (InputConfig)
{
case ELyraWidgetInputMode::GameAndMenu:
return FUIInputConfig(ECommonInputMode::All, GameMouseCaptureMode);
case ELyraWidgetInputMode::Game:
return FUIInputConfig(ECommonInputMode::Game, GameMouseCaptureMode);
case ELyraWidgetInputMode::Menu:
return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture);
case ELyraWidgetInputMode::Default:
default:
return TOptional<FUIInputConfig>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FGameplayTagContainer UCommonUIActionRouterBase::GetGameplayTagsForInputMode(const ECommonInputMode Mode) const
{
FGameplayTagContainer Tags;

switch (Mode)
{
case ECommonInputMode::Game:
Tags.AddTag(TAG_InputModeGame);
break;

case ECommonInputMode::Menu:
Tags.AddTag(TAG_InputModeMenu);
break;

case ECommonInputMode::All:
Tags.AddTag(TAG_InputModeGame);
Tags.AddTag(TAG_InputModeMenu);
break;
}

return Tags;
}
  • 每个 InputMode 对应一个 Tag,增强输入系统会通过当前的 tag 来对 IMC 进行过滤,IMC的InputModeFilterOptions 可以配置过滤 tag,我们可以将 GAME_IMC 的过滤 tag 配置成 TAG_InputModeGame,UI_IMC 的过滤 tag 配置成 TAG_InputModeMenu

InputMappingContext

  • 对于 HUD 界面,可以将 InputMode 设置为 ALL(根据自己的需求),UI_IMC 和 GAME_IMC 同时生效,对于打开设置界面和系统界面的action是配置在UI_IMC中,按键监听操作交给 HUD 去监听处理。当系统界面打开时,会将 InputMode 设置为 Menu, 只让 UI_IMC 生效。
  • UActivatableWidget 中有一个 InputConfig 属性用于设置 InputMode,直接在蓝图中配置即可,对于二级界面的 ActivatableWidget,如果不需要改变输入模式,保持默认值即可,正常情况下,输入模式只需要配置在一级界面
  • 对于 UI_IMC 和 GAME_IMC 存在按键冲突的问题可以通过设置GAME_IMC的优先级更高,使得 GAME_IMC 的 action 优先触发

3.1 场景题

  • 考虑到之前遇到的一个场景,道具拾取的 action 和技能释放的 action 在手柄模式可能存在按键冲突(绑在一个键上),需求上希望优先触发拾取操作,如何解决?
    • 由于按键优先级覆盖操作只能以 IMC 为HoldActionHelper.h单位,可以通过为拾取action新增一个IMC,让它的优先级高于 GAME_IMC,然后在拾取 tips 出现的时候注册 IMC,隐藏的时候取消注册,从而达到目的。
    • 如果拾取是用 GameplayAbilitySystem 实现的,可以通过 Tag 阻止技能触发。
      1. 拾取交互组件一个 State.CanInteract 的 GameplayTag。
      2. 在 GA_Cast 的 Activation Blocked Tags 中加入 State.CanInteract。
      • 当玩家进入拾取范围时给 Pawn 加这个 Tag,离开时移除。这样按 A 时:
        1. GA_Cast 因为 Blocked Tag 不会激活;
        2. 拾取交互正常执行。

4. Widget如何监听按键输入

  • CommonUI 对于按键输入的监听做了自己的封装,需要用 CommonUI 的 RegisterUIActionBinding 来实现,使用方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void UHUDLayout::NativeOnInitialized()
{
Super::NativeOnInitialized();

for (int32 Index = 0; Index < InputActionWidgetMappings.Num(); ++Index)
{
const FInputActionWidgetMapping& Mapping = InputActionWidgetMappings[Index];
if (!Mapping.InputAction || Mapping.WidgetClass.IsNull() || !Mapping.LayerTag.IsValid())
{
continue;
}

FBindUIActionArgs BindArgs(Mapping.InputAction, false,
FSimpleDelegate::CreateWeakLambda(this, [this, Mapping]()
{
HandleInputActionWidget(Mapping);
})
);
BindArgs.InputMode = ECommonInputMode::Game;
RegisterUIActionBinding(BindArgs);
}
}
  • 对于 FBindUIActionArgs 需要指定 InputMode 参数,InputMode 与当前的 widget 启用的 InputMode 保持一致,不一致的话会被拦截。
  • 另外,CommonUI 的长按只支持了旧输入系统,RegisterUIActionBinding 之后会生成 FUIActionBinding,FUIActionBinding 有一个 HoldMappings 属性用于处理长按逻辑,对于旧输入系统 CommonUI 会从 DataTable 配置中读取长按配置,对于增强输入系统则没有支持配置读取(Enhanced Input v1.0),但是可以从代码层面手动注入配置:

5. CommonUI 输入路由流程

  • UCommonUIActionRouterBase 是 CommonUI 输入路由系统的核心,它通过一棵 Activatable Tree(可激活树) 来维护 UCommonActivatableWidget、UCommonUserWidget、UUserWidget 之间的嵌套关系。
    • FActivatableTreeNode — 树节点,每个节点持有一个 UCommonActivatableWidget 的弱引用,并拥有 Children 子节点数组和 Parent 父节点指针
    • FActivatableTreeRoot — 树根节点,继承自 FActivatableTreeNode,额外追踪 LeafMostActiveNode(最深层已激活节点)
  • 只有 UCommonActivatableWidget 才能成为树节点,普通 UCommonUserWidget / UUserWidget 不会成为节点,但会挂载到最近的父级 ActivatableWidget 节点上
  • 树形嵌套示意图如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UCommonUIActionRouterBase

├── RootNodes[0]: FActivatableTreeRoot (UCommonActivatableWidget_A - 如 HUD 根)
│ │
│ ├── FActivatableTreeNode (UCommonActivatableWidget_B - 如背包面板)
│ │ │
│ │ ├── [附属 bindings] UCommonUserWidget_1 的 ActionBindings → 挂在此节点
│ │ ├── [附属 bindings] UUserWidget_2 的 ActionBindings → 挂在此节点
│ │ └── FActivatableTreeNode (UCommonActivatableWidget_C - 如物品详情弹窗)
│ │ └── [附属 bindings] UCommonUserWidget_3 的 ActionBindings → 挂在此节点
│ │
│ └── FActivatableTreeNode (UCommonActivatableWidget_D - 如地图面板)

├── RootNodes[1]: FActivatableTreeRoot (UCommonActivatableWidget_E - Modal 对话框)

└── ActiveRootNode → RootNodes[0] (当前接收输入的根)
  • 当按键输入触发时,UCommonUIActionRouterBase 会对已激活的优先级最高的 FActivatableTreeRoot 进行深度优先遍历,实现代码如下所示,子节点优先消费输入,由 IsReceivingInput 实现可知,当父界面 Deactivate 的时候,子节点无法接收输入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
virtual bool IsReceivingInput() const override { return bCanReceiveInput && IsWidgetActivated(); }

bool FActivatableTreeNode::ProcessNormalInput(...) const
{
if (IsReceivingInput())
{
// ① 先递归处理所有子节点
for (const FActivatableTreeNodeRef& ChildNode : Children)
{
if (ChildNode->ProcessNormalInput(...))
{
return true; // 子节点消费了,直接返回
}
}
// ② 子节点都没消费,才处理自身的 bindings
return FActionRouterBindingCollection::ProcessNormalInput(...);
}
return false;
}

5.1 场景分析

  1. 父子界面是 UCommonActivatableWidget,绑定了相同的 InputAction :
    1
    2
    3
    父 ActivatableWidget (Node_A) ← 绑定了 IA_Confirm

    ├── 子 ActivatableWidget (Node_B) ← 也绑定了 IA_Confirm
    • 子界面 Node_B 会触发回调,父界面 Node_A 不触发(前提是 Node_B 必须处于 Activated)
  2. 父界面是 UCommonActivatableWidget,子界面是UCommonUserWidget :
    1
    2
    3
    父 ActivatableWidget (Node_A) ← 绑定了 IA_Confirm

    ├── 子 CommonUserWidget (非节点) ← 也绑定了 IA_Confirm
    • 两者的 bindings 都挂载在同一个 Node_A 上,按注册顺序谁先匹配谁触发,注册顺序由 OnWidgetRebuilt 的调用顺序决定——UE Widget 树的构建是自内向外(子 Widget 先 Rebuild),所以通常子 UCommonUserWidget 的 binding 先注册,因此子界面先触发。也可以通过 FBindUIActionArgs 中的 PriorityWithinCollection 字段来设置优先级来控制调用顺序
    • 如果希望父子界面都能响应输入,可以设置 BindArgs.bConsumeInput = false 来实现

6. 手柄导航

6.1 设置默认焦点

  • UCommonActivatableWidget 的 NativeGetDesiredFocusTarget 虚函数方法可用于设置 widget 被激活时的需要获取焦点的控件
  • 当 UCommonActivatableWidget 被激活时,会递归找到最深的激活的 ActivatableWidget 子节点,调用子节点的 NativeGetDesiredFocusTarget 方法来设置为默认焦点

DPadNavigation

6.2 可导航控件

  • 如果想让widget中的某个控件可被导航,需要将 bIsFocusable = true,CommonUI 提供了内置的可导航控件,基本上能满足开发需求。这些控件默认 bIsFocusable = true,无需额外设置
  • UCommonButtonBase : 最常用,自带聚焦、悬停、选中状态
  • UCommonListView / UCommonTreeView / UCommonTileView : 列表导航,自带 bSelectItemOnNavigation
  • UCommonRotator : 左右选择器,覆写了 NativeOnNavigation 拦截左右方向
  • UCommonActionWidget : 绑定到指定的InputAction,根据当前的输入设备自动显示按键图标

6.3 控制导航方向和边界行为

  • 当 DPad/左摇杆 按下时,Slate 通过 FNavigationConfig 将输入转为 EUINavigation 方向,然后按照 搜索算法搜索下一个可聚焦控件。
  • 只有Visibility为Visible状态的widget才有资格参与导航.
  • 导航规则有六种,可在编辑器中配置,选中控件 → Details → Navigation 面板,可以为 Up/Down/Left/Right/Next/Previous 六个方向分别设置规则:
    1. Escape (逃逸 / 默认自动)
      • 含义:这是系统默认的规则。它的意思是:“引擎,请你帮我自动计算最近的控件”。当按下方向键时,引擎会在该方向上寻找几何位置最近的可聚焦控件。如果在当前容器内找不到,焦点就会“逃逸(Escape)”到父级容器,让父级容器去寻找该方向上的下一个控件。
      • 使用示例(90%的情况) :
        • 垂直主菜单:你放了一个Vertical Box,里面有“开始游戏”、“设置”、“退出”三个按钮。你全部保持默认的 Escape。当在“开始游戏”上按下时,引擎自动发现下面是“设置”,焦点就顺理成章地过去了。
    2. Stop (停止)
      • 含义:在这个方向上绝对禁止焦点移动。就像撞到了一堵空气墙,按下该方向键不会发生任何反应,焦点留在原地。
      • 使用示例 :
        • 防止误触出界:假设你有一个位于屏幕左侧的装备栏(Grid Panel)。为了防止玩家在最左侧的一列按下“左”键时,焦点意外跑到屏幕背后隐藏的其他 UI 上,你可以选中最左列的所有格子,把它们的 Left 导航规则强制设置为 Stop。
    3. Wrap (循环 / 环绕)
      • 含义:当在该方向上走到尽头(没有下一个控件)时,焦点会自动“瞬移”回当前容器内相反方向的最远端。
      • 使用示例 :
        • 角色选择界面:你有一排水平排列的角色头像(Horizontal Box)。玩家当前选中了最右边的角色,如果他再按一次“右”,焦点会自动回到最左边的第一个角色身上。此时只需将这排按钮的最右侧控件的 Right 规则设为 Wrap(或在整个容器级别设置)。
    4. Explicit (显式 / 指定)
      • 含义:放弃引擎的自动计算, 硬编码(手动绑定) 下一步必须去哪个具体地控件。选择此项后,下方会出现一个吸管工具或下拉框,让你直接指定目标 Widget。
      • 使用示例 :
        • 不对称或奇特的 UI 布局:假设你的 UI 是星系图,星球按钮散乱地分布在屏幕上。引擎的自动计算(Escape)很容易算错最近的按钮。此时你可以使用 Explicit:在地球按钮上,强行设置按下 Up 必须跳转到火星按钮,按下 Right 必须跳转到木星按钮,即使它们在几何位置上并不完全对齐。
    5. Custom (自定义 - 蓝图逻辑)
      • 含义:将这个方向的导航权限完全交给蓝图或 C++ 代码来决定。选择后,你可以绑定一个返回值为 Widget 的函数(Delegate)。每次按下该方向键,都会执行这个函数,函数返回哪个控件,焦点就去哪个控件。
      • 使用示例 :
        • 带条件判断的导航:在天赋树界面中,玩家在一个节点上按下“下”。在 Custom 绑定的蓝图函数里,你写了个逻辑:如果“火球术”已经解锁,返回“大火球”按钮作为下一个焦点;如果“火球术”未解锁,返回一个带有锁图标的提示按钮作为焦点。
    6. Custom Boundary (自定义边界 - 蓝图逻辑)
      • 含义:它和 Custom 类似,都需要绑定一个蓝图函数。区别在于触发时机:Custom 是只要按下方向键就触发;而 Custom Boundary 只有在引擎使用默认自动计算发现“这个方向上已经没有控件了,即将跨越边界(逃逸)”时,才会触发你写的蓝图逻辑。
      • 使用示例 :
        • 复杂的跨面板导航:屏幕左边是“背包网格”,右边是“角色装备槽”。在背包内部移动时,你希望用默认的上下左右(它会自动在格子里移动)。但是,当你处于背包的最右侧边缘格子上,再按“右”时(即将离开背包边界),触发 Custom Boundary。在蓝图里你可以写:如果现在正在比较戒指,就聚焦到右边的“戒指装备槽”;如果是武器,就聚焦到“武器装备槽”。

6.4 自动导航模式下的搜索算法

  • 导航系统内置了两种搜索算法:
    1. 正交扫描:沿导航方向的矩形扫描带进行逐格扫描,找到最近的对侧边缘 Widget,简单可靠
    2. 零近距离:搜索锥 + Minkowski 距离,可处理非轴对齐的布局,更智能
  • 默认走固定的正交扫描算法。只有开启实验模式后,Widget 上配置的 NavigationMethod 才会生效

Navigation_Method

  • 开启后

Navigation_Method