在您的游戏中,HUD(抬头显示)通常显示非交互式的用户界面元素,例如在 创建 HUD 计量器 中演示的健康计量器。除此之外,几乎每个游戏都需要交互式用户界面,例如响应玩家激活的按钮、在激活时动画、与其他交互控件一起淡入/淡出的菜单等。

使用 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代码解释
滑块控制器
另一个模块初始化并控制两个音量滑块。它还允许您将回调函数连接到每个滑块,以检测玩家与滑块的交互并在游戏中应用所需的更改。
在 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。每个状态都包含一个过渡,以声明(通过 TweenInfo)特定状态将如何达到在 properties 表中定义的目标属性。在这种情况下,menuOpen 声明状态更改为按钮的 Rotation 为 45°,并使用 Exponential 的半秒过渡,而 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 的半秒过渡,以及 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 将本地引用设置为 SoundGroup 中的 Effects,该组位于 SoundService 中。SoundGroup 允许您相互关联多个 Sounds,并通过组的 Volume 属性控制组中每个声音的音量。 53‑54 声明一个本地引用 effectsVolumeSlider,并使用 SliderController.hydrate() 函数进行初始化,HUDContainer 中的 EffectsVolumeSlider 框架被引用为要初始化的对象。请注意,FindFirstChild() 的第二个参数为 true,告诉该方法递归查找 HUDContainer 分支以定位 EffectsVolumeSlider。 55 滑块的初始值设置为当前 Effects 声音组的 Volume,或 0.5 作为后备。 56‑60 建立回调函数,以将滑块值更改应用于 Effects 声音组的音量。 64‑73 基本上与行 52-61 相同,但用于初始化 HUDContainer 中的 BackgroundVolumeSlider 框架,并将滑块值更改应用于 Background 声音组的音量。
有了 SettingsScript,游戏现在提供了一个完全操作的交互式 UI 示例,将一组相关的 UI 对象链接到玩家交互。























