UE5 UI开发入门:Common UI与Activatable Widgets实战教程
1. 项目概述为什么你需要了解Common UI与Activatable Widgets如果你刚开始接触UE5的UI开发面对UMG虚幻运动图形编辑器里琳琅满目的控件和蓝图节点可能会感到有些无从下手。特别是当你想要构建一个带有层级菜单、弹窗、或者需要在不同界面间流畅切换的复杂UI系统时传统的Widget创建和显示方式很快就会变得难以维护。这时Epic Games官方推出的Common UI插件及其核心概念——Activatable Widgets可激活控件就成了你必须要掌握的“瑞士军刀”。简单来说Common UI是一套建立在UMG之上的高级UI框架它不是为了替代UMG而是为了解决UMG在构建大型、复杂、可维护的UI系统时的一些痛点。而Activatable Widgets则是这个框架的基石。你可以把它理解为一个“有状态”的Widget。普通的Widget你创建出来添加到视口它就一直在那里。而Activatable Widgets拥有明确的“激活Activate”和“停用Deactivate”生命周期。这听起来简单但它带来的好处是革命性的自动的输入路由管理、堆栈式的界面导航、以及更清晰的UI状态逻辑。想象一下你要做一个游戏的主菜单开始游戏、设置、退出。用传统方法你可能需要手动管理哪个按钮被选中按“上/下”键时焦点如何移动打开设置菜单后如何屏蔽主菜单的输入。而使用Common UI的Activatable Widgets你可以将主菜单、设置菜单都做成可激活控件。当打开设置菜单时它会自动“激活”并成为输入焦点主菜单会自动“停用”并暂时忽略输入。关闭设置菜单后主菜单重新激活焦点完美回到之前的位置。这一切框架帮你处理了大部分繁琐的逻辑。本教程将彻底摒弃空泛的概念讲解直接带你从零开始手把手创建一个最简单的可激活UI系统。我们将完成一个经典场景一个主菜单界面上面有一个按钮点击后打开一个子菜单比如“设置”面板并能在两者间正确切换焦点和输入。通过这个最小化的实例你将透彻理解Activatable Widgets从创建、配置到绑定的完整流程以及背后“为什么”要这么做。2. 核心概念与前置准备理解Common UI的运作基石在动手之前我们需要先理清几个关键概念这能让你在后续操作中知其然更知其所以然避免盲目操作。2.1 Common UI插件与Lyra输入系统Common UI插件并非UE5默认启用。你需要手动在“编辑Edit - 插件Plugins”中搜索“Common UI”并勾选启用然后重启编辑器。启用后你会在内容浏览器的“添加Add”菜单里看到新的“Common UI”分类。更重要的是Common UI的设计与Epic的“增强型输入系统Enhanced Input System”以及“游戏功能Gameplay Features”架构深度集成。它推荐几乎是要求你使用一套结构化的输入配置。对于新手项目最快捷的方式是借鉴Lyra示例项目中的输入设置。如果你没有Lyra可以手动创建输入操作Input Action 在内容浏览器创建IA_UI_Confirm,IA_UI_Cancel,IA_UI_Navigate等分别对应确认、取消、导航。输入映射上下文Input Mapping Context 创建一个名为IMC_UI的上下文将上述输入操作映射到具体的键盘如Enter、Escape、方向键或手柄按键。在玩家控制器中设置 在你的玩家控制器蓝图中在BeginPlay事件里使用Enhanced Input相关的节点将IMC_UI添加给本地玩家并设置其优先级。注意Common UI的导航如用方向键切换按钮依赖于这些输入操作。如果你只使用鼠标点击可能暂时不会发现问题但一旦需要手柄或键盘导航缺少这些配置将导致功能失效。这是新手最常见的坑之一。2.2 Activatable Widgets的生命周期与输入栈这是最核心的部分。一个继承自CommonActivatableWidget的控件其生命周期比普通Widget多出了几个关键阶段Construct 构建和普通Widget一样。OnActivated 当Widget被激活时调用。这是你初始化界面数据、开始播放动画、请求焦点的最佳位置。OnDeactivated 当Widget被停用时调用。在这里进行清理工作如停止动画、释放占用的资源。Destruct 销毁。Common UI内部维护着一个“激活控件栈Activatable Widget Stack”。新激活的控件会被推入栈顶并自动接收输入焦点。栈顶控件下方的控件会自动进入停用状态但可以选择性地保持可见比如背景模糊效果。当栈顶控件被取消或移除时栈中下一个控件会重新被激活。2.3 视图模型ViewModel的初步认识在搜索内容中反复提到的MVVMModel-View-ViewModel模式是Common UI推荐的数据驱动UI方式。简单理解Model 你的游戏数据如玩家血量、金币数。View 就是你的UMG Widget负责显示。ViewModel 一个中间层它从Model获取数据并转换成View容易绑定的格式例如将血量的浮点数转换成进度条的百分比和显示文本同时也接收View的输入事件如按钮点击并转发给Model进行逻辑处理。对于第一个教程为了降低复杂度我们不会深入MVVM而是使用更直接的蓝图事件和变量来通信。但你需要知道对于大型项目使用ViewModel通过Common UI的“视图绑定”功能是保持UI逻辑清晰、可测试的关键。我们会在后续步骤中埋下引入ViewModel的伏笔。3. 手把手实操创建你的第一个可激活UI系统现在我们开始实战。目标创建WBP_MainMenu主菜单和WBP_SettingsMenu设置菜单实现点击主菜单的“设置”按钮打开设置菜单点击设置菜单的“返回”按钮关闭并回到主菜单。3.1 创建与配置可激活控件创建控件蓝图 在内容浏览器右键选择“用户界面 - 控件蓝图”。分别创建两个命名为WBP_MainMenu和WBP_SettingsMenu。修改父类 这是最关键的一步。打开WBP_MainMenu在蓝图编辑器的“类默认值Class Defaults”面板中找到“父类Parent Class”。点击下拉框搜索并选择CommonActivatableWidget。对WBP_SettingsMenu进行同样的操作。为什么只有继承自CommonActivatableWidget你的Widget才能被Common UI的栈系统管理享受自动的输入路由和生命周期回调。配置输入模式 仍在“类默认值”面板找到“输入配置Input Configuration”部分。输入模式Input Mode 对于游戏内的UI通常选择“游戏和UIGame And UI”这样UI可以接收输入同时游戏世界也能继续运行例如在打开背包时角色仍可移动。对于全屏菜单可以选择“仅UIUI Only”。鼠标捕获模式Mouse Capture Mode 通常选择“不捕获No Capture”或“捕获期间Capture During Mouse Down”取决于你是否需要鼠标点击UI外部的行为。隐藏光标Hide Cursor During Capture 根据游戏类型决定。设计简单界面在WBP_MainMenu中拖入一个垂直框Vertical Box里面放两个CommonButtonCommon UI提供的增强按钮自带样式和输入响应或普通按钮。将第一个按钮的文本改为“开始游戏”第二个改为“设置”。在WBP_SettingsMenu中拖入一个背景板上面放一些设置选项如一个音量滑动条CommonSlider和一个“返回”按钮。3.2 构建UI逻辑与激活/停用主菜单逻辑打开WBP_MainMenu进入图表Graph。为“设置”按钮的OnClicked事件添加节点。我们需要激活设置菜单。这里需要用到Common UI的“子管理器Subsystem”。拖出引线搜索并调用GetLocalPlayerSubsystem节点选择UCommonUISubsystem。从该子系统节点拖出引线搜索Push Widget to Layer节点。这个节点负责将控件推送到指定的“层Layer”。Layer Name 这是一个标签用于管理UI的层级。你可以在项目设置中定义这些层。我们简单起见使用内置的Game层。你也可以输入一个自定义字符串如“Menu”。Widget 这里需要传入WBP_SettingsMenu的类引用而不是实例。点击下拉框选择WBP_SettingsMenu如果没找到需要先编译一次该控件。Input Mode 通常选择“从父级继承Inherit from Parent”。这样当点击“设置”按钮时WBP_SettingsMenu的一个实例就会被创建并激活推送到栈顶。设置菜单逻辑打开WBP_SettingsMenu进入图表。为“返回”按钮的OnClicked事件添加节点。我们需要让当前控件即自己从栈中移除。这里有一个专为Activatable Widget设计的方法Deactivate Widget。调用这个节点并选择“延迟Deferred”选项通常更安全。调用Deactivate Widget后当前控件会停用并从栈中弹出栈中下一个控件即我们的主菜单会自动重新激活。3.3 初始显示与根层设置现在我们有菜单了但游戏一开始如何显示主菜单呢这需要一个“根”或“入口点”。创建HUD或玩家控制器逻辑 通常初始UI的显示逻辑放在玩家控制器Player Controller的BeginPlay事件中。获取并推送根Widget在玩家控制器蓝图中BeginPlay时获取UCommonUISubsystem同上。使用Push Widget to Layer节点将WBP_MainMenu推送到一个合适的层例如“Game”层。这将成为你UI栈的底部。配置层优先级可选但重要 在“项目设置Project Settings - Common UI”中你可以定义层的名称和优先级。例如你可以定义“HUD”层优先级0常显、“GameMenu”层优先级1、“ModalPopup”层优先级2弹窗。优先级更高的层会覆盖在低层级之上。对于我们的例子暂时使用默认层即可。4. 核心环节实现焦点管理、数据传递与进阶绑定完成了基本流程后我们会遇到更实际的问题焦点怎么控制菜单之间如何传递数据比如把音量设置的值传回游戏4.1 焦点管理与导航修复按照上述步骤你可能已经发现用键盘或手柄导航时焦点行为可能不如预期。比如在设置菜单中按“取消”键通常是Escape或B键我们期望关闭菜单。这需要配置。设置取消动作 在WBP_SettingsMenu的“类默认值”中有一个属性叫“取消操作Cancel Action”。你可以将其设置为“取消Cancel”。这样当玩家触发取消输入你在IMC_UI中为IA_UI_Cancel绑定的键时Common UI框架会自动尝试调用该Widget的Deactivate Widget。手动指定初始焦点 在WBP_MainMenu的OnActivated事件中添加一个Set Focus节点并连接到“开始游戏”按钮。这确保了菜单激活时焦点总在第一个按钮上。处理导航中断来自搜索内容的干货 这是搜索内容中讨论的核心问题。当你在一个焦点链如按钮列表中用鼠标点击了一个非焦点able的控件比如一个纯装饰用的图片或者一个没有设置Is Focusable的按钮键盘/手柄的焦点会丢失。原因 Unreal的输入系统在处理鼠标点击和游戏手柄/键盘焦点时逻辑不同。解决方案基于搜索内容提炼方案A简单 确保所有需要交互的控件都设置为Is Focusable。对于背景板如果你希望点击它也能保持焦点在某个项目上可以放一个透明的、设置为Focusable的按钮覆盖它并在其点击事件中调用Request Refresh Focus这是CommonActivatableWidget的方法强制框架重新评估焦点应落在哪里。方案B推荐使用ViewModel 像搜索内容中Irene Zanon所讲创建一个共享的“Selection ViewModel”其中有一个布尔值属性FocusRefreshRequested。当任何控件如背景按钮被点击时它设置这个属性为True。所有关心焦点的Activatable Widget都绑定监听这个属性。当属性变化时它们调用自己的RequestRefreshFocus方法。这保证了焦点逻辑的集中和一致。4.2 控件间的数据通信主菜单和设置菜单如何共享数据例如设置菜单调整了音量游戏需要知道。使用GameInstance或PlayerState简单项目 对于全局数据如音量、画质设置可以存储在GameInstance或PlayerState的变量中。设置菜单的滑动条OnValueChanged时直接写入这些变量。游戏音频系统从同一变量读取值。使用事件分发器Event Dispatcher 在设置菜单中定义一个事件分发器如OnSettingsChanged并将音量值作为参数。在玩家控制器或游戏模式中订阅这个事件。当设置菜单触发Deactivate Widget之前广播这个事件。引入ViewModel进阶数据驱动 这才是Common UI和MVVM的强项。创建一个SettingsViewModel其中有一个MasterVolume属性使用FieldNotify这样属性变化时可以通知UI。主菜单和设置菜单都绑定到同一个SettingsViewModel实例。设置菜单中的滑动条通过“视图绑定View Binding”将其值双向绑定到ViewModel的MasterVolume属性。游戏音频系统监听ViewModel的MasterVolume变化或者直接读取其值。这样数据流非常清晰View滑动条 - ViewModel - Model音频系统。菜单的打开和关闭不影响数据源。4.3 视图绑定View Binding初探视图绑定是MVVM在UMG中的实现。它允许你将Widget的属性如文本块的文本、按钮的可见性直接绑定到ViewModel的属性上无需编写重复的蓝图脚本来手动设置。创建ViewModel 在内容浏览器创建“其他 - View Model”蓝图例如VM_Settings。添加带通知的属性 在ViewModel蓝图中添加一个浮点型变量MasterVolume。在细节面板勾选FieldNotify。这会在变量值改变时生成一个通知事件。在Widget中设置ViewModel 打开WBP_SettingsMenu在“类默认值”中找到“视图模型View Model”部分将“视图模型类View Model Class”设置为VM_Settings。创建绑定 在UMG设计器中选中音量滑动条。在细节面板找到“值Value”属性点击右边的“绑定Bind”按钮选择“创建绑定Create Binding”。这会生成一个转换函数图表。编写转换函数 在生成的图表中你会有一个Get MasterVolume节点来自ViewModel。直接将其输出连接到函数的返回值。这就完成了一个单向绑定ViewModel - View。要实现双向绑定滑动滑块也能改变ViewModel需要在滑动条的OnValueChanged事件中使用Set MasterVolume节点。5. 常见问题、调试技巧与性能考量在实际操作中你肯定会遇到各种问题。这里汇总一些典型场景和排查思路。5.1 常见问题速查表问题现象可能原因排查步骤与解决方案点击按钮后新UI没出现1.Push Widget to Layer的Layer名称错误或未定义。2. 新Widget的父类不是CommonActivatableWidget。3. 输入配置冲突新UI被其他UI阻塞。1. 检查推送的Layer名并在项目设置的Common UI部分确认其存在。2. 确认控件蓝图父类已修改。3. 检查新UI的“输入模式”是否与现有UI冲突。使用Print String输出调试信息。键盘/手柄无法导航UI1. 未正确配置Enhanced Input Actions和Mapping Context。2. 按钮等控件未设置为Is Focusable。3. 初始焦点未设置。1. 确保IA_UI_Navigate,Confirm,Cancel等Action已创建并正确映射到按键且IMC已添加给玩家。2. 在UMG中选中按钮在细节面板勾选“Is Focusable”。3. 在Widget的OnActivated事件中调用Set Focus。新UI出现后旧UI的按钮仍能点击新UI的输入模式可能设置为“游戏和UI”且未正确接管焦点。旧UI可能未被停用。确保使用Push Widget to Layer来激活新UI这会自动将下层UI停用。检查下层UI是否在OnDeactivated事件中正确处理了状态如设置为不可见。关闭UIDeactivate后焦点消失关闭UI时框架会自动将焦点交还给栈中下一个激活的控件。如果下一个控件没有可聚焦的子项焦点会丢失。在即将被重新激活的控件如下层菜单的OnActivated事件中手动调用Set Focus到一个默认按钮上。视图绑定不更新1. ViewModel属性未勾选FieldNotify。2. 绑定是单向的仅ViewModel-View。3. Widget和ViewModel的关联不正确。1. 确认ViewModel中变量的FieldNotify已勾选。2. 检查是否需要双向绑定并在View的事件中更新ViewModel。3. 确认Widget的“视图模型类”设置正确并且ViewModel实例已正确获取通常通过Resolver或手动设置。5.2 调试技巧使用OnActivated和OnDeactivated事件 在这些事件中插入Print String节点打印Widget的名称和当前时间。这能清晰看到UI栈的激活/停用顺序是调试导航逻辑的利器。查看Common UI调试信息 在编辑器运行时打开“控制台命令~”输入CommonUI.Debug.ToggleWidgetStackVis可以在屏幕上显示当前所有激活的Widget及其层级关系。检查输入路由 输入CommonUI.Debug.ToggleInputRouterVis可以显示输入路由的调试信息帮助你理解输入是如何被分发的。5.3 性能与最佳实践避免频繁创建/销毁 对于频繁打开关闭的UI如物品提示考虑使用对象池Object Pooling或预先创建并隐藏而不是每次都Push Widget。ViewModel的生命周期 ViewModel不会随Widget的停用而自动销毁。你需要管理其生命周期。对于全局数据如设置ViewModel可以长期存在。对于临时数据如某个NPC的交易菜单应在Widget的OnDeactivated中清理或销毁对应的ViewModel。简化Widget树 过于复杂的UMG层级会影响性能。使用CommonLazyWidget可以延迟加载不立即显示的部分。谨慎使用Tick 尽量避免在Widget中使用Event Tick。如果需要持续更新考虑使用视图绑定到ViewModel的属性由Model层驱动更新。走到这里你已经完成了从零认识、创建到绑定一个完整可用的Common UI Activatable Widgets系统的全过程。关键在于理解其“状态化”和“栈管理”的核心思想这彻底改变了我们组织UI逻辑的方式。一开始可能会觉得比直接拖放Widget并写事件要繁琐但一旦你的UI系统变得复杂这种结构化的优势将极大地提升开发效率和代码的可维护性。记住先让最简单的流程跑通再逐步引入焦点管理、数据绑定等高级特性。当你下次需要制作一个带有暂停菜单、物品栏、技能树的复杂游戏界面时这套框架将成为你最得力的帮手。