一个 HUD (Heads-Up Display) 在你的体验中通常会显示非交互式的 UI 元素,例如在 创建 HUD 仪表 中演示的生命值计量器。在此基础上,几乎每个体验都需要 交互式 UI,比如在玩家激活时响应的按钮、激活时动画的按钮、与其他交互控件一起推出和收起的菜单等。

使用 UI 基础知识 - HUD 仪表 作为起始点和 UI 基础知识 - 交互性 作为完成的参考, 本教程展示了:
- 如何在屏幕顶部边缘定位设置按钮。
- 包含可交互可拖动滑块的设置菜单的设计。
- 使用 ModuleScripts 来形成“控制器”模块,以便对具有有状态声明的 UI 对象进行可扩展控制。
- 如何连接按钮与玩家激活,以切换设置菜单的推出和收起。
- 如何连接可拖动的 UI 滑块,以单独调整音效和背景氛围的音量。
启用设备模拟器
正如在 创建 HUD 仪表 中所指出的,手机和平板电脑的屏幕空间最少,因此确保 UI 元素适应更小的屏幕且对玩家清晰可见非常重要。如果你还没有这样做,请在 Studio 中启用 设备模拟器:
在 Studio 中打开 UI 基础知识 - HUD 仪表 模板。
在 Studio 的 测试 菜单中,切换打开 设备模拟器。
在主视口上方的工具栏中,选择手机模拟,例如 iPhone X 或 Samsung Galaxy A51。然后,将视图大小设置为 适应窗口 以最大化利用 Studio 的空间。

创建设置按钮
GuiButtons 是具有内置功能的交互用户界面元素,例如当按钮被点击或轻触时触发的多平台 Activated 事件。GuiButton 类扩展为两个变体,TextButton 和 ImageButton,本教程使用一个“齿轮”形状的 ImageButton 来切换设置菜单的打开或关闭。

要构建设置按钮:
在 资源管理器 窗口中,找到 StarterGui 中的 HUDContainer。

选择新按钮,设置以下属性:
- AnchorPoint = 0.5, 0.25 (水平居中和垂直上四分之一)
- BackgroundTransparency = 1 (完全透明)
- Position = 0.5, 0, 0, 0 (顶端居中定位)
- Size = 0.1, 0, 0.1, 0 (在纵横比受限时占屏幕高度的 10%)
- Image = rbxassetid://104919049969988 (“齿轮”符号的资产 ID)
在按钮中插入一个 UIAspectRatioConstraint,使用默认属性(宽高比 1:1)。

创建设置菜单
新的 设置按钮 将被编程用于切换设置菜单的打开和关闭,给玩家按需访问设置或其他信息的机会。在本教程中,菜单将包含交互式滑块,以独立调整背景音频和音效的音量水平。

创建父框架
正如在 创建 HUD 仪表 中所指出的,Frame 作为其他 UI 对象的容器。整个设置菜单将构建在一个父框架中,使其易于管理,作为一个有状态对象,根据当前状态以不同方式响应输入。
在 HUDContainer 中插入一个 Frame 并将其重命名为 SettingsMenu。

选择 SettingsMenu,设置以下属性:
- AnchorPoint = 0.5 (中心锚点)
- BackgroundColor3 = 30, 30, 60 (海军蓝)
- BackgroundTransparency = 0.25 (75% 不透明)
- Position = 0.5, 0, 0.5, 0 (屏幕中心)
- Size = 0.75, 0, 0.75, 0 (占内嵌屏幕区域的 75% 宽度/高度)

在 SettingsMenu 中插入一个 UIAspectRatioConstraint,并将其 AspectRatio 属性设置为 2.5 (2.5:1 的宽高比)。


在 SettingsMenu 中插入 UICorner 修饰符,并将其 CornerRadius 设置为 0.1, 0。


构建滑块
为了允许玩家调整音量水平,设置菜单将包含两个可拖动的 滑块小部件,由 UIDragDetector 提供支持,这是一个方便的对象,方便与 2D 用户界面元素进行交互。
要为第一个滑块创建一个父容器:
在 SettingsMenu 容器中插入一个新的 Frame,并将其重命名为 EffectsVolumeSlider。

选择 EffectsVolumeSlider 框架,设置以下属性:
- AnchorPoint = 0.5 (中心锚点)
- BackgroundTransparency = 1 (完全透明)
- Position = 0.5, 0, 0.35, 0 (水平居中,距离容器的顶部 35%)
- Size = 0.8, 0, 0.1, 0 (容器宽度的 80% 和高度的 10%)

在 EffectsVolumeSlider 中插入一个新的 UIListLayout。这个布局修饰符是将兄弟 GuiObjects 自动排列到其父容器中的水平行或垂直列内的强大方法,包括应用 flex 概念的能力。

为新的 UIListLayout 设置以下属性:
- Padding = 0.06, 0 (列表元素之间 6% 的填充)
- FillDirection = Horizontal (元素的从左到右排列)
- HorizontalFlex = Fill (水平调整元素的大小以填充整个父容器,覆盖其定义的宽度)
- VerticalAlignment = Center (在垂直中心对齐元素)
滑块图标
一个简单的图标,包括音频音符和 “爆炸” 符号,帮助玩家识别滑块的目的,当他们打开设置菜单时。

选择 Icon,设置以下属性:
- BackgroundTransparency = 1 (完全透明)
- Size = 2.5, 0, 2.5, 0 (最后宽高比例约束后是父框架高度的 250%)
- Image = rbxassetid://90019827067389 (音频符号和 “爆炸” 符号的资产 ID)
范围框架
在 图标 的右侧,滑块的交互部分必须包含在另一个 Frame 中。

在 EffectsVolumeSlider 框架中插入一个新的 Frame,并将其重命名为 SliderFrame。请注意,它必须是 UIListLayout 布局修饰符的直接 兄弟。

选择 SliderFrame,设置以下属性:
- BackgroundColor3 = 0 (黑色)
- BackgroundTransparency = 0.75 (25% 不透明)
- LayoutOrder = 1
- Size = 1, 0, 1, 0 (父框架的 100% 宽度/高度)
在 SliderFrame 中插入以下修饰符:

交互手柄
构建好 滑块容器 后,现在可以为玩家创建一个 可拖动的手柄,以便在游戏中进行交互。

在 SliderFrame 容器中插入一个新的 Frame,并将其重命名为 Handle。

选择 Handle,设置以下属性:
- AnchorPoint = 0.5 (中心锚点)
- Position = 0.5, 0, 0.5, 0 (父框架的水平和垂直中心)
- Size = 1.2, 0, 1.2, 0 (最后宽高比约束后为父框架高度的 120%)
- ZIndex = 3 (在其他滑块元素的视觉层前显示手柄)
在 Handle 中插入以下修饰符:
- UIAspectRatioConstraint,保持其默认属性(1:1 宽高比)
在 Handle 中插入一个 UIDragDetector 对象。这个方便的对象促进并鼓励与 2D 用户界面元素的交互。

为新的 UIDragDetector 设置以下属性:
- ResponseStyle = Scale (通过检测器的父对象的位置的缩放值进行移动)
为了确保手柄的线性拖动范围限制在其容器内,将其 BoundingUI 属性链接到 SliderFrame 容器:
- 选择 UIDragDetector。
- 在 属性 窗口中单击其 BoundingUI 属性。
- 在 资源管理器 窗口中,单击手柄的父 SliderFrame。

现在 BoundingUI 属性链接会反映 SliderFrame 容器:

如果你现在进行游戏测试,就会在其父容器中左右拖动手柄:
内部填充
为了更清楚地表明滑块控制的是 0% 到 100% 的范围,可以在容器的左侧添加一个 内部填充,这将与手柄的可变位置同步。

在 SliderFrame 容器中插入一个新的 Frame,并将其重命名为 InnerFill。

为 InnerFill 框架设置以下属性:
- AnchorPoint = 0, 0.5 (左边缘和垂直中心)
- BackgroundColor3 = [0, 150, 255] (与滑块的手柄匹配的蓝色)
- BackgroundTransparency = 0.35 (65% 不透明)
- Position = 0, 0, 0.5, 0
- Size = 0.5, 0, 1, 0 (父框架的 50% 宽度和 100% 高度)
- ZIndex = 2 (在父框架的填充/轮廓前视觉层)
复制滑块
构建好 第一个滑块 后,可以轻松复制它并修改一些视觉方面以表示另一个目的,这里是背景音频音量由音乐音符图标表示。

选择完整的 EffectsVolumeSlider 对象。

复制它 (CtrlD 或 ⌘D) 并将副本重命名为 BackgroundVolumeSlider。

将副本的 Position 更改为 0.5, 0, 0.7, 0,以便将其移动到第一个滑块下方。

展开 BackgroundVolumeSlider 的顶层分支,选择 Icon 图像标签,并将其 Image 属性更改为 rbxassetid://101125859760167 (音乐音符符号的资产 ID)。



创建关闭按钮
设置菜单的最后一个元素是一个 关闭按钮,为玩家提供一个额外的输入来关闭菜单(顶部中间的 SettingsButton 将执行相同的目的)。

选择 CloseButton,设置以下属性:
- AnchorPoint = 1, 0 (右上角)
- BackgroundTransparency = 1 (完全透明)
- Position = 1, -10, 0, 10 (距离右上角 10 像素以内)
- Size = 0.15, 0, 0.15, 0 (在宽高比受到限制后占框架高度的 15%)
- Image = rbxassetid://5577404210 (关闭按钮符号的资产 ID)
- ImageTransparency = 0.25 (75% 不透明)
创建控制模块
可扩展的 控制模块 设置使互动 UI 管理比放置在每个对象中的单个脚本更加简化。 ModuleScripts 通过让你在客户端-服务器边界的不同侧或同一侧之间重用代码来促进这种可扩展功能。
有状态对象控制器
以下有状态对象控制器模块让你可以将行为附加到 UI 对象(如 SettingsButton 和 SettingsMenu),并轻松切换/动画到不同的状态。要创建该模块:
在 ReplicatedStorage 中插入一个 ModuleScript 并将其重命名为 StatefulObjectController。

将以下代码粘贴到该模块中:
StatefulObjectControllerlocal TweenService = game:GetService("TweenService")local StatefulObjectController = {}StatefulObjectController.__index = StatefulObjectControllerexport type StateName = stringexport type State = {transition: TweenInfo,properties: { [string]: any },}function StatefulObjectController.hydrate(props: {object: Instance,states: { [StateName]: State },initialStateName: StateName})local object, states, initialStateName = props.object, props.states, props.initialStateNamelocal self = setmetatable({states = states,currentStateName = initialStateName,tweens = {},}, StatefulObjectController)-- Create tweens for reuse to avoid making new tweens every time state is changedfor stateName, state in states doself.tweens[stateName] = TweenService:Create(object, state.transition, state.properties)endself:setState(self.currentStateName)return selfendfunction StatefulObjectController:setState(stateName: StateName)local stateTween: Tween = self.tweens[stateName]if not stateTween thenwarn(string.format("Attempted to set %s to unknown state '%s'", self.object:GetFullName(), stateName))returnendself.currentStateName = stateName-- Make sure other tweens aren't conflictingfor _, tween in self.tweens dotween:Cancel()endstateTween:Play()endreturn StatefulObjectController代码说明行数 目的 6‑10 定义并导出 Luau 类型,以提高在使用此模块时的自动完成和代码检查。 12‑33 用于初始化和附加行为到一个有状态的 UI 对象的函数,接受一个 GuiObject 作为 object,一个状态及其各自 TweenInfo 数据的表,以及一个初始状态名称。在此函数内部,行 26‑28 为每个状态构建 Tweens,行 30 调用模块的 setState() 函数以将对象设置为初始状态。 35‑50 用于设置 / 更改有状态对象状态的函数。行 42 将对象的 currentStateName 设置为正在设置的状态,行 45‑47 取消任何正在运行的动画以防止冲突,行 49 执行状态的动画。
滑块控制器
另一个模块初始化并控制两个音量滑块。它还允许你将回调函数连接到每个滑块,以便检测玩家与滑块的交互并在体验中应用所需的更改。
在 ReplicatedStorage 容器中插入一个 ModuleScript 并将其重命名为 SliderController。

将以下代码粘贴到该模块中:
SliderControllerlocal SliderController = {}SliderController.__index = SliderControllerexport type Value = numberexport type OnChanged = (Value) -> ()function SliderController.hydrate(props: {object: Instance,onChanged: OnChanged,initialValue: Value?})local object, onChanged, initialValue = props.object, props.onChanged, props.initialValuelocal handle = object:FindFirstChild("Handle", true)if not handle thenwarn(string.format("Attempted to hydrate slider %s but couldn't find Handle", object:GetFullName()))endlocal innerFill = object:FindFirstChild("InnerFill", true)if not innerFill thenwarn(string.format("Attempted to hydrate slider %s but couldn't find InnerFill", object:GetFullName()))endlocal dragDetector = handle:FindFirstChildWhichIsA("UIDragDetector")if not dragDetector thenwarn(string.format("Attempted to hydrate slider %s but couldn't find UIDragDetector", object:GetFullName()))endlocal self = setmetatable({handle = handle,innerFill = innerFill,dragDetector = dragDetector,value = initialValue or 0.5,onChanged = onChanged,}, SliderController)-- Set initial valueself:setValue(self.value)-- Connect detector to player manipulationself.dragConnection = dragDetector.DragContinue:Connect(function()self:setValue(handle.Position.X.Scale)end)return selfendfunction SliderController:setValue(value: Value)local clampedValue = math.clamp(value, 0, 1)self.value = clampedValue-- Update the handle position and inner frame size to matchself.handle.Position = UDim2.fromScale(clampedValue, 0.5)self.innerFill.Size = UDim2.fromScale(clampedValue, 1)-- Run the user's callback with the latest valuelocal changeSuccess, changeResult = pcall(self.onChanged, clampedValue)if not changeSuccess thenwarn("Error in slider callback:", changeResult)endendreturn SliderController代码说明行数 目的 7‑46 用于初始化和附加行为到滑块元素的函数,接受滑块的父 Frame 作为 object,一个 onChanged 回调函数来处理滑块更改,以及滑块手柄位置的初始值。在函数内部,行 14‑27 确认滑块的 手柄、内部填充 和 UIDragDetector 元素的存在。行 38 调用模块的 setValue() 函数,行 41‑43 将拖动检测器的 DragContinue 事件连接到模块的 setValue() 函数(见下一行的详细信息)。 48‑61 用于设置滑块值的函数。在函数内部,行 53‑54 确保手柄的位置和内部填充大小同步,行 57 将更改的值传递给回调函数,以便你可以根据需要使用该值。
创建设置脚本
随着 设置按钮 和 设置菜单 的完成,你可以使用一个脚本将一切连接在一起,该脚本利用 控制模块。
在 HUDContainer 中插入一个新的 LocalScript 并将其重命名为 SettingsScript 以描述其用途。请注意,此脚本应位于与 SettingsMenu 和 SettingsButton 同一级别(兄弟),这两个是将要管理的顶层 UI 对象。

将以下代码粘贴到该脚本中:
SettingsScriptlocal ReplicatedStorage = game:GetService("ReplicatedStorage")local SoundService = game:GetService("SoundService")local SliderController = require(ReplicatedStorage.SliderController)local StatefulObjectController = require(ReplicatedStorage.StatefulObjectController)local HUDContainer = script.Parent-- Initialize settings buttonlocal settingsButton = StatefulObjectController.hydrate({object = HUDContainer:FindFirstChild("SettingsButton"),states = {menuOpen = {transition = TweenInfo.new(0.5, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out),properties = {Rotation = 45,},},menuClosed = {transition = TweenInfo.new(0.5, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out),properties = {Rotation = 0,},},},initialStateName = "menuClosed"})-- Initialize settings menu framelocal settingsMenu = StatefulObjectController.hydrate({object = HUDContainer:FindFirstChild("SettingsMenu"),states = {menuOpen = {transition = TweenInfo.new(0.5, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out),properties = {Position = UDim2.fromScale(0.5, 0.5),Visible = true,},},menuClosed = {transition = TweenInfo.new(0),properties = {Position = UDim2.fromScale(0.5, 0.4),Visible = false,},},},initialStateName = "menuClosed"})-- Initialize effects volume sliderlocal effectsAudio = SoundService:FindFirstChild("Effects")local effectsVolumeSlider = SliderController.hydrate({object = HUDContainer:FindFirstChild("EffectsVolumeSlider", true),initialValue = effectsAudio and effectsAudio.Volume or 0.5,onChanged = function(value: SliderController.Value)if effectsAudio theneffectsAudio.Volume = valueendend,})-- Initialize background volume sliderlocal backgroundAudio = SoundService:FindFirstChild("Background")local backgroundVolumeSlider = SliderController.hydrate({object = HUDContainer:FindFirstChild("BackgroundVolumeSlider", true),initialValue = backgroundAudio and backgroundAudio.Volume or 0.5,onChanged = function(value: SliderController.Value)if backgroundAudio thenbackgroundAudio.Volume = valueendend,})-- Connect buttons to player interactionHUDContainer:FindFirstChild("SettingsButton").Activated:Connect(function()local targetState = if settingsButton.currentStateName == "menuClosed"then "menuOpen"else "menuClosed"settingsButton:setState(targetState)settingsMenu:setState(targetState)end)HUDContainer:FindFirstChild("CloseButton", true).Activated:Connect(function()settingsButton:setState("menuClosed")settingsMenu:setState("menuClosed")end)参考以下部分来探索该脚本如何利用 控制模块。
设置按钮行 10-27 初始化并附加行为到 设置按钮,如下:
行数 目的 10‑11 声明一个本地引用 settingsButton,并使用 StatefulObjectController.hydrate() 函数对其进行初始化,引用 HUDContainer 中的 SettingsButton 按钮作为要初始化的对象。 12‑25 传递一个 states 表,其中包含两个唯一状态,menuOpen 和 menuClosed。每个状态都包含一个过渡,以声明如何通过 properties 表中定义的属性达到特定状态。在这种情况下,menuOpen 声明状态变化到按钮的 Rotation 为 45°,使用 Exponential 的 0.5 秒的动画,而 menuClosed 有效地将按钮恢复到其默认旋转 0。 26 按钮的 初始 状态设置为 menuClosed (可以设置为 states 表中的任何状态名称)。 在脚本的后面(行 76-86),按钮连接到玩家交互以触发 UI 状态更改:
行数 目的 76‑82 将 SettingsButton 连接到 Activated 事件,调用一个匿名函数。在匿名函数内部,行 77‑79 在 menuOpen 和 menuClosed 之间切换 targetState 变量。行 80 然后调用 setState() 将按钮的状态设置为 targetState,行 81 对 SettingsMenu 执行相同的操作,确保其在激活 SettingsButton 时切换打开或关闭。 83‑86 将 CloseButton 连接到 Activated 事件,以调用匿名函数,该函数将 SettingsButton 和 SettingsMenu 的状态设置为 menuClosed。 设置菜单行 30-49 初始化并附加行为到 设置菜单,如下:
行数 目的 30‑31 声明一个本地引用 settingsMenu,并使用 StatefulObjectController.hydrate() 函数对其进行初始化,引用 HUDContainer 中的 SettingsMenu 框架作为要初始化的对象。 32‑46 与设置按钮类似,传递一个 states 表,其中包含两个唯一状态,menuOpen 和 menuClosed。在这里,menuOpen 声明一个状态变化到 UDim2.fromScale(0.5, 0.5),用于框架的 Position,以及 Bounce 0.5 秒的动画,设置 Visible 状态为 true。相反,menuClosed 声明位置状态变化到 UDim2.fromScale(0.5, 0.4) 并将 Visible 设置为 false,但行 41 中的 TweenInfo 设置为持续时间 0,有效地使状态更改立即发生。 48 框架的初始状态设置为 menuClosed。 在脚本的后面(行 76-86),设置菜单连接到玩家交互:
行数 目的 81 为 SettingsMenu 设置相同的 targetState,以与 SettingsButton 的激活保持同步。 85 当激活 CloseButton 时,将 SettingsMenu 的状态设置为 menuClosed。 音量滑块行 51-73 初始化并附加行为到 EffectsVolumeSlider 和 BackgroundVolumeSlider,如下:
行数 目的 52 设置对 Effects 的 SoundGroup 在 SoundService 中的本地引用。SoundGroup 使你能够将多个 Sounds 关联在一起,并通过该组的 Volume 属性控制组中每个音效的音量。 53‑54 声明一个本地引用 effectsVolumeSlider,并使用 SliderController.hydrate() 函数对其进行初始化,引用 HUDContainer 中的 EffectsVolumeSlider 框架。请注意,true 的 FindFirstChild() 第二个参数告诉该方法递归地向下查找 HUDContainer 分支以查找 EffectsVolumeSlider。 55 设置滑块的初始值为当前 Effects 声音组的 Volume,或者作为后备 0.5。 56‑60 建立回调函数,以将滑块值更改应用于 Effects 声音组的音量。 64‑73 基本上与行 52-61 相同,但用于初始化位于 HUDContainer 中的 BackgroundVolumeSlider 框架,并将滑块值更改应用于 Background 声音组的音量。
随着 SettingsScript 的到位,体验现在提供了一个完整的操作互动 UI 示例,将一组相关的 UI 对象与玩家交互链接在一起。























