1. 模块概述

  • 「全局 UI 容器」——基建层的 UPrimaryGameLayout / UUIManagerSubSystem / UHUDLayout / AHUD 提供整套基于 CommonUI Layer/Stack 范式的 UI 注入框架,支撑全项目所有 UI 的注册、激活、HitTest 联动、GameFeature 接入、手柄热插拔、键鼠/手柄输入模式自适配等横切关注点。这层基建是项目里所有 UI(背包、技能、菜单、加载、登录等)能够工作的前提。
  • 模块的关键设计:
    1. Layer 自动收集 + HitTest 联动UPrimaryGameLayout 通过 RequestGameplayTagChildren 自动收集所有 UI.Layer.* 子 Tag 注册的 Layer,按面板 ChildIndex 隐式排优先级,监听每个 Layer 的 OnDisplayedWidgetChanged,高优先级有内容时自动把低优先级 Layer 设为 HitTestInvisible 防止焦点穿透。
    2. 手柄热插拔 + PS/Xbox 自动识别UUIManagerSubSystem 在 Initialize 时运行时修改 ControllerData CDO 的 GamepadName / GamepadHardwareIdMapping,让 BP_Input_PS / BP_Input_Xbox 配置正确生效;订阅 IPlatformInputDeviceMapper::OnInputDeviceConnectionChange 在手柄热插拔时调用 LocalPlayer->SetControllerId 重映射,避免 ControllerId 与 InputDeviceId 错位导致按键失灵。
    3. GameFeature ↔ HUD 注入AHUD::PreInitializeComponents 注册为 GameFrameworkComponentReceiverBeginPlay 发送 NAME_GameActorReady,让所有已激活的 GameFeatureAction_AddWidgets 把 Widget 注入到指定 Layer/ExtensionPoint。

2. 设计思路与架构决策

2.1 PrimaryGameLayout 的 Layer HitTest 自动联动

  • 问题:CommonUI 的 PrimaryGameLayout 把 UI 分到多个 Layer Stack,但当 Modal 层弹出后,下方 Game/Menu 层的按钮仍然可以被鼠标点击或被手柄焦点导航选中——违反”模态窗口阻塞下层”的常识。
  • 方案:在子类 USamplerPrimaryGameLayout 中:
    1. NativeConstruct 时通过 UGameplayTagsManager::Get().RequestGameplayTagChildren(UI.Layer) 自动收集所有已注册 Layer Tag。
    2. 用每个 Layer 容器在父面板中的 ChildIndex 决定优先级(索引越大越靠前)——美术配 UI 时是按”想让谁覆盖谁”放置的,这个隐式契约直接复用,避免再写一份配置表。
    3. 给每个 Layer 注册 OnDisplayedWidgetChanged,任何 Layer 内容变化时遍历所有 Layer:找到当前最高优先级的有内容的 Layer,把比它优先级低的 Layer 设为 HitTestInvisible(保留视觉,剥夺交互),最高那一层设为 SelfHitTestInvisible
  • 理由
    • 比手动维护”谁压谁”配置更难出错——美术调整堆叠顺序时 UI 程序无需改代码。
    • 比”打开高优先级时强制 Collapse 低优先级”友好——Modal 弹出时 HUD 仍然可见(玩家能看到血量),只是无法点击。
    • 备选方案是给每个 Activatable 自己声明优先级,但要把这种横切关注点散落到 N 个业务 Widget 里,且无法处理”同一 Layer 内同时多个 Widget”的情况;放在 PrimaryGameLayout 这一处统管最干净。

2.2 输入设备热插拔与 PS/Xbox 自动识别

  • 问题:开发期用 PS5 DualSense 和 Xbox 手柄交替测试。CommonUI 默认配置下:
    1. 编辑器 GetOptions 下拉里只有 “Generic”,没有 “PlayStation” 选项,蓝图里无法手选。
    2. 手柄热插拔后 LocalPlayer 的 ControllerId 不会自动同步,导致 EnhancedInput Subsystem 取到的 PlatformUserId 与手柄真正的 InputDeviceId 错位,按键完全失灵。
    3. PS DualSense 在 Windows 上识别为 DeviceManager.WindowsDualsense,Xbox 走 XInputInterface,需要给两类 ControllerData 分别配 HardwareIdMapping 才能让 CommonInput 自动检测并切换 GamepadInputType。
  • 方案:在 USamplerUIManagerSubSystem::Initialize 中:
    • FixupControllerDataGamepadSettings:用 LoadClass 拿到项目的 BP_Input_PS / BP_Input_Xbox ControllerData 蓝图,运行时直接修改其 CDO 的 GamepadNameGamepadHardwareIdMapping,绕过编辑器的 GetOptions 限制。
    • HandleInputDeviceConnectionChange:监听 IPlatformInputDeviceMapper::Get().GetOnInputDeviceConnectionChange,手柄插入时用 GetUserIndexForPlatformUser 算出新 UserIndex,与 LocalPlayer 当前 ControllerId 不一致就 SetControllerId 重映射。
  • 理由:这两个问题是 5.x 版本 CommonUI 在 Windows 多手柄场景的实际坑,没有引擎补丁,业务层这么处理是当前最经济的方案。

2.3 GameFeature + UIExtensionPoint 解耦 HUD 布局

  • 问题:HUD 上的子模块(血条组、技能格子组、Buff 列表等)在不同关卡 / 不同战斗模式下需要不同布局;硬编码进 HUD 类会造成”加一个新 Widget 就要改 ASamplerHUD” 的耦合。
  • 方案
    • ASamplerHUD::PreInitializeComponents 中调用 AddGameFrameworkComponentReceiver 注册自身。
    • BeginPlay 调用 SendGameFrameworkComponentExtensionEvent(NAME_GameActorReady),让所有已激活的 GameFeature Action(如 GameFeatureAction_AddWidgets)知道 HUD 已就绪,可以注入 Widget 到指定 Layer/ExtensionPoint。
    • HUDLayout 中放 USamplerUIExtensionPointWidget 占位,运行时被 GameFeature 注入的 Widget 填充;编辑器中可以配置 PreviewEntryClass 显示预览。
  • 理由
    • 走 GameFeature 的标准 AddWidgets Action,UI 程序员不需要为每个新需求改 HUD 类,UX/策划侧的同事可以通过 GFP 直接配置。
    • 不同 GameMode(Town / Battle / Boss)激活不同 GFP,HUD 自动呈现不同内容,无需改 C++。

3. Q&A

3.1 你怎么处理 PS/Xbox 手柄热插拔?

  • 参考回答
  • 这是 5.x 版本 CommonUI 在 Windows 多手柄场景的实际坑,主要有三个问题。
    • 第一,编辑器里 ControllerData 蓝图的 GamepadName 字段 GetOptions 下拉只有 Generic,没有 PlayStation 选项,蓝图里手选不了。我在 UIManagerSubSystem::Initialize 时通过 LoadClass 拿到 BP_Input_PS 的 CDO,运行时直接修改它的 GamepadName 为 “PlayStation”,绕过编辑器限制。
    • 第二,PS DualSense 在 Windows 上是 DeviceManager.WindowsDualsense 设备,DualSense / DualSenseEdge / DualShock4 三种 HardwareIdentifier;Xbox 则走 XInputInterface 的 XInputController。需要给两类 ControllerData 分别配 GamepadHardwareIdMapping,CommonInputPreprocessor 才能在硬件检测时正确切换 GamepadInputType。我在同一个 Initialize 里运行时配置这两份映射。
    • 第三,热插拔时 LocalPlayer.ControllerId 不会自动同步——比如先用 PS5 接入是 ControllerId=0,然后切到 Xbox 通常会成 InputDeviceId=1,但 LocalPlayer 还认 0。导致 EnhancedInput Subsystem 取的 PlatformUserId 与手柄的 InputDeviceId 错位,按键全部失灵。
  • 解决办法是订阅 IPlatformInputDeviceMapper::GetOnInputDeviceConnectionChange,连接事件触发时用 GetUserIndexForPlatformUser 算出新 UserIndex,与 LocalPlayer 当前 ControllerId 不一致就 SetControllerId 重映射。这样玩家拔 PS 插 Xbox 时切换无感知。