Roblox Studio · 第 15 课

屏幕界面:制作自己的探险 HUD

用完整 LocalScript 创建任务卡、数字进度和进度条,再用 Scale、Offset、AnchorPoint 与尺寸约束完成手机适配。

10–15 岁 60 分钟 ScreenGui / UDim2 动手实践 测试与保存

Course overview

从空白 Baseplate 做出一个真正能运行的任务 HUD:显示任务、晶石进度和完成状态,并在手机与电脑上保持可读。

这一课不让你照着概念表抄属性。你会在 StarterGui 创建一个 ScreenGui,再用一段完整的 LocalScript 生成任务面板。按下 Play 后,左上角会出现“能量岛探险”、目标文字、进度条和 1 / 3 计数。

完成后你还能把进度改成 0、2、3,马上看到界面如何变化;再故意做一次固定宽度的错误版本,对比 Scale、Offset、AnchorPoint 和尺寸约束到底解决了什么问题。

01 · BEFORE

开始之前

会打开 Explorer、Properties、Output 和 Script Editor。新建 Baseplate,并另存为 Lesson15。

02 · MAKE

你的作品

一个由 LocalScript 创建的响应式任务 HUD,包含标题、任务、进度条、数字进度和完成提示。

03 · CHECK

完成标志

F5 测试时显示 1 / 3;把代码改为 3 / 3 后进度条填满并显示“任务完成”;手机竖屏中面板不超出右边界。

ScreenGui 在游戏画面中的官方截图
图 15-1:官方 ScreenGui 效果参考:界面覆盖在游戏画面上,不跟着场景里的 Part 一起移动。Roblox Creator Documentation · CC BY 4.0 · 原图未修改 · 许可说明

Learning route

先准备,再动手;看到结果后,记得保存。

每完成一小步,就测试一次。先说出你预计会看到什么,再用 Play 和 Output 检查,不用急着一次写完所有代码。

1

01 · 先明确今天要做出的结果

打开 Lesson15 Baseplate 后,先在 View(视图)菜单打开 Explorer、Properties 和 Output。今天只有一个验收目标:按 F5 后,玩家屏幕左上角出现任务卡;把最后一行的数字从 1 改成 3,界面立刻变成完成状态。

2

02 · 只创建两个对象,并把脚本放对位置

先手动创建 ScreenGui 和 LocalScript,其余 Frame、TextLabel 和进度条全部由代码生成。这样既能看懂 Explorer 层级,也不会因为漏改十几个属性而卡住。

3

03 · 粘贴完整代码,先让成品跑起来

下面的 create() 是一个小型工厂函数:创建对象、批量设置属性、最后指定 Parent。完整 HUD 都由这一个 LocalScript 建立,所以你可以直接复制运行。

4

04 · 做四次改动,亲眼看懂程序

现在不要继续抄新代码。只改一处、运行一次、记录结果。工程师调界面时会把输入压到边界值,因为 0、刚好完成、超过总数最容易暴露错误。

5

05 · 用一个失败实验理解 Scale、Offset 和 AnchorPoint

Panel.Size = UDim2.new(0.36, 0, 0, 176) 的四个数字依次是 X Scale、X Offset、Y Scale、Y Offset。宽度取父容器的 36%,高度固定 176 像素;UISizeConstraint 再把最终宽度限制在 280 到 420 像素。

6

06 · 像上线前一样测试电脑和手机

桌面预览正常只是第一关。Device Simulator 可以模拟不同屏幕尺寸、方向和触屏环境。测试时既看“有没有显示”,也检查文字是否被裁掉、任务卡是否挡住移动区、横竖屏切换后是否超出画面。

Step by step

01 · 先明确今天要做出的结果

打开 Lesson15 Baseplate 后,先在 View(视图)菜单打开 Explorer、Properties 和 Output。今天只有一个验收目标:按 F5 后,玩家屏幕左上角出现任务卡;把最后一行的数字从 1 改成 3,界面立刻变成完成状态。

任务卡属于每位玩家自己的屏幕,因此放在 StarterGui。不要在 Workspace 创建一块 Part 来冒充 HUD;场景 Part 会受相机距离和角度影响,ScreenGui 中的界面才固定在屏幕上。

  1. File → Save to File,把项目保存成 Lesson15.rbxl;后面每完成一节就按 Ctrl+S。
  2. View → Explorer、Properties、Output,保证三个窗口都看得见。
  3. 确认 Explorer 中有 StarterGui。今天不需要导入模型或图片。
预计结果第一次运行显示“目标:找到 3 颗晶石”、蓝色进度条和“1 / 3”。代码最后一行改成 renderQuest(3, 3) 后显示“任务完成,可以前往出口”。

Step by step

02 · 只创建两个对象,并把脚本放对位置

先手动创建 ScreenGui 和 LocalScript,其余 Frame、TextLabel 和进度条全部由代码生成。这样既能看懂 Explorer 层级,也不会因为漏改十几个属性而卡住。

LocalScript 必须在玩家客户端能运行的位置。本课把它放进 StarterGui 下的 AdventureGui。按下 Play 时,Roblox 会把 AdventureGui 复制到当前玩家的 PlayerGui,然后运行 HudController。

  1. 在 Explorer 把鼠标移到 StarterGui,点右侧的 +,搜索 ScreenGui,插入后按 F2 改名为 AdventureGui。
  2. 选中 AdventureGui,在 Properties 把 ResetOnSpawn 取消勾选;这样角色重生时不会重新创建整套 HUD。
  3. 再次点 AdventureGui 右侧的 +,搜索 LocalScript,改名为 HudController。注意图标必须是 LocalScript,不是普通 Script。
  4. 双击 HudController,删除默认内容,准备粘贴下一节的完整代码。
Explorer 结构 · 不是 Luau 代码Explorer
StarterGui
└─ AdventureGui (ScreenGui)
   └─ HudController (LocalScript)

运行 F5 后,代码会在 PlayerGui 的副本中加入:
AdventureGui
├─ HudController
└─ Panel (Frame)
   ├─ Title (TextLabel)
   ├─ Objective (TextLabel)
   ├─ ProgressTrack (Frame)
   │  └─ ProgressFill (Frame)
   ├─ Count (TextLabel)
   └─ Status (TextLabel)

Step by step

03 · 粘贴完整代码,先让成品跑起来

下面的 create() 是一个小型工厂函数:创建对象、批量设置属性、最后指定 Parent。完整 HUD 都由这一个 LocalScript 建立,所以你可以直接复制运行。

代码最后的 renderQuest(1, 3) 表示“已经找到 1 颗,总共需要 3 颗”。renderQuest() 同时更新任务文字、数字、进度条宽度和状态颜色,避免四个地方显示互相矛盾。

  1. 把完整代码粘贴到 HudController,按 Ctrl+S。
  2. 按 F5(Play),不要只按 F8(Run);本课需要创建一个玩家客户端来运行 LocalScript。
  3. 看到 HUD 后,展开 Players → 你的名字 → PlayerGui → AdventureGui,对照上一节的运行层级。
  4. 查看 Output:应该出现 [HUD] 1/3,且没有红色错误。按 Shift+F5 停止测试。
StarterGui > AdventureGui > HudController(LocalScript)Luau
local gui = script.Parent

-- 方便重复测试:如果旧面板还在,先删除再重建。
local oldPanel = gui:FindFirstChild("Panel")
if oldPanel then
    oldPanel:Destroy()
end

local function create(className, properties, parent)
    local object = Instance.new(className)
    for property, value in pairs(properties) do
        object[property] = value
    end
    object.Parent = parent
    return object
end

local panel = create("Frame", {
    Name = "Panel",
    AnchorPoint = Vector2.new(0, 0),
    Position = UDim2.fromOffset(18, 18),
    Size = UDim2.new(0.36, 0, 0, 176),
    BackgroundColor3 = Color3.fromRGB(16, 24, 40),
    BackgroundTransparency = 0.08,
    BorderSizePixel = 0,
}, gui)

create("UICorner", {
    CornerRadius = UDim.new(0, 14),
}, panel)

create("UIStroke", {
    Color = Color3.fromRGB(93, 126, 180),
    Transparency = 0.35,
    Thickness = 1,
}, panel)

-- 0.36 让宽度跟屏幕变化;约束保证它不会小得难读或大得挡住画面。
local sizeConstraint = create("UISizeConstraint", {
    MinSize = Vector2.new(280, 176),
    MaxSize = Vector2.new(420, 176),
}, panel)

-- 把 false 改成 true,可以故意制造一次“小屏溢出”并观察原因。
local RUN_OVERFLOW_TEST = false
if RUN_OVERFLOW_TEST then
    sizeConstraint.MinSize = Vector2.new(0, 176)
    sizeConstraint.MaxSize = Vector2.new(2000, 176)
    panel.Size = UDim2.fromOffset(700, 176)
end

create("TextLabel", {
    Name = "Title",
    Position = UDim2.fromOffset(18, 12),
    Size = UDim2.new(1, -36, 0, 28),
    BackgroundTransparency = 1,
    Font = Enum.Font.GothamBold,
    Text = "能量岛探险",
    TextColor3 = Color3.fromRGB(255, 255, 255),
    TextSize = 22,
    TextXAlignment = Enum.TextXAlignment.Left,
}, panel)

local objective = create("TextLabel", {
    Name = "Objective",
    Position = UDim2.fromOffset(18, 44),
    Size = UDim2.new(1, -36, 0, 42),
    BackgroundTransparency = 1,
    Font = Enum.Font.Gotham,
    Text = "",
    TextColor3 = Color3.fromRGB(211, 220, 235),
    TextSize = 17,
    TextWrapped = true,
    TextXAlignment = Enum.TextXAlignment.Left,
    TextYAlignment = Enum.TextYAlignment.Top,
}, panel)

local track = create("Frame", {
    Name = "ProgressTrack",
    Position = UDim2.fromOffset(18, 100),
    Size = UDim2.new(1, -92, 0, 14),
    BackgroundColor3 = Color3.fromRGB(48, 61, 82),
    BorderSizePixel = 0,
    ClipsDescendants = true,
}, panel)
create("UICorner", {CornerRadius = UDim.new(1, 0)}, track)

local fill = create("Frame", {
    Name = "ProgressFill",
    Size = UDim2.fromScale(0, 1),
    BackgroundColor3 = Color3.fromRGB(72, 167, 255),
    BorderSizePixel = 0,
}, track)
create("UICorner", {CornerRadius = UDim.new(1, 0)}, fill)

local countLabel = create("TextLabel", {
    Name = "Count",
    AnchorPoint = Vector2.new(1, 0),
    Position = UDim2.new(1, -18, 0, 93),
    Size = UDim2.fromOffset(56, 28),
    BackgroundTransparency = 1,
    Font = Enum.Font.GothamBold,
    Text = "",
    TextColor3 = Color3.fromRGB(255, 255, 255),
    TextSize = 17,
    TextXAlignment = Enum.TextXAlignment.Right,
}, panel)

local statusLabel = create("TextLabel", {
    Name = "Status",
    Position = UDim2.fromOffset(18, 132),
    Size = UDim2.new(1, -36, 0, 26),
    BackgroundTransparency = 1,
    Font = Enum.Font.GothamMedium,
    Text = "",
    TextColor3 = Color3.fromRGB(150, 190, 230),
    TextSize = 15,
    TextXAlignment = Enum.TextXAlignment.Left,
}, panel)

local function renderQuest(current, total)
    total = math.max(total, 1)
    current = math.clamp(current, 0, total)
    local ratio = current / total

    objective.Text = string.format("目标:找到 %d 颗晶石", total)
    countLabel.Text = string.format("%d / %d", current, total)
    fill.Size = UDim2.fromScale(ratio, 1)

    if current == total then
        statusLabel.Text = "任务完成,可以前往出口"
        statusLabel.TextColor3 = Color3.fromRGB(109, 232, 160)
        fill.BackgroundColor3 = Color3.fromRGB(75, 220, 130)
    else
        statusLabel.Text = "继续探索,留意发光的平台"
        statusLabel.TextColor3 = Color3.fromRGB(150, 190, 230)
        fill.BackgroundColor3 = Color3.fromRGB(72, 167, 255)
    end

    print(string.format("[HUD] %d/%d", current, total))
end

renderQuest(1, 3)
先运行,后拆解如果 Output 报错,双击红色消息会跳到对应代码行。先核对是不是 LocalScript、代码是否完整,再开始改数值。

Step by step

04 · 做四次改动,亲眼看懂程序

现在不要继续抄新代码。只改一处、运行一次、记录结果。工程师调界面时会把输入压到边界值,因为 0、刚好完成、超过总数最容易暴露错误。

math.clamp(current, 0, total) 把异常进度限制在合法范围内。因此 renderQuest(9, 3) 不会画出三倍宽的进度条,而会按 3 / 3 处理。

  1. 把最后一行改成 renderQuest(0, 3):应显示 0 / 3,进度条为空。
  2. 改成 renderQuest(2, 3):应显示 2 / 3,蓝条约占三分之二。
  3. 改成 renderQuest(3, 3):绿条填满,状态改为“任务完成,可以前往出口”。
  4. 改成 renderQuest(9, 3):界面仍只显示 3 / 3。确认后恢复为 renderQuest(1, 3)。
四个测试值 · 实际使用时只保留一行Luau
-- 只改这一行,然后每次按 F5 检查:
renderQuest(0, 3) -- 空进度
renderQuest(2, 3) -- 三分之二
renderQuest(3, 3) -- 完成状态
renderQuest(9, 3) -- 越界输入会被 math.clamp 修正
04 · 做四次改动,亲眼看懂程序
输入应该看见它验证了什么
0, 30 / 3,空进度条起始状态
2, 32 / 3,蓝条约 67%Scale 可以表示比例
3, 33 / 3,绿色完成提示完成分支
9, 3仍为 3 / 3越界保护

Step by step

05 · 用一个失败实验理解 Scale、Offset 和 AnchorPoint

Panel.Size = UDim2.new(0.36, 0, 0, 176) 的四个数字依次是 X Scale、X Offset、Y Scale、Y Offset。宽度取父容器的 36%,高度固定 176 像素;UISizeConstraint 再把最终宽度限制在 280 到 420 像素。

进度条最能说明 Scale 的用途。fill.Size = UDim2.fromScale(ratio, 1) 中,ratio=2/3 时宽度就是父进度槽的三分之二。它不需要先知道手机到底有多少像素。Count 使用 AnchorPoint=(1,0),所以 Position 的 X 坐标固定的是它的右边缘;面板变宽时,数字仍贴在右侧。

  1. 把完整代码里的 RUN_OVERFLOW_TEST 从 false 改为 true。用手机竖屏模拟,700 像素宽的面板会超出右边界。
  2. 把 RUN_OVERFLOW_TEST 恢复为 false,面板会重新使用 36% 宽度和 280–420 像素约束。
  3. 把 renderQuest 改成 2, 3,确认 ProgressFill 宽度随比例变化,而不是写死像素。
  4. 把 Count 的 AnchorPoint 临时改为 Vector2.new(0, 0),观察数字为什么跑到右边界外;测试后恢复 Vector2.new(1, 0)。
溢出实验 · 修改完整程序中的同名开关Luau
-- 完整程序中已有这个实验开关:
local RUN_OVERFLOW_TEST = true
if RUN_OVERFLOW_TEST then
    sizeConstraint.MinSize = Vector2.new(0, 176)
    sizeConstraint.MaxSize = Vector2.new(2000, 176)
    panel.Size = UDim2.fromOffset(700, 176)
end

-- 测完改回 false,再测试正常的响应式方案。
05 · 用一个失败实验理解 Scale、Offset 和 AnchorPoint
写法含义本课位置
UDim2.fromOffset(18, 18)距左、上各 18 像素Panel.Position
UDim2.new(0.36, 0, 0, 176)宽度 36%,高度 176 像素Panel.Size
UDim2.fromScale(ratio, 1)宽度按进度比例,高度 100%ProgressFill.Size
AnchorPoint = Vector2.new(1, 0)用对象右上角对齐坐标Count
界面 Size 的 Scale 与 Offset 属性截图
图 15-2:官方 Size 属性截图:展开 X、Y,分别找到 Scale 与 Offset,对照本课数值输入。Roblox Creator Documentation · CC BY 4.0 · 原图未修改 · 许可说明
测试完要关掉开关RUN_OVERFLOW_TEST=true 是故意制造故障。看见手机溢出后改回 false,保存的最终版本才会恢复响应式尺寸。

Step by step

06 · 像上线前一样测试电脑和手机

桌面预览正常只是第一关。Device Simulator 可以模拟不同屏幕尺寸、方向和触屏环境。测试时既看“有没有显示”,也检查文字是否被裁掉、任务卡是否挡住移动区、横竖屏切换后是否超出画面。

如果你的 Studio 里暂时看不到 Device Simulator,在 File → Beta Features 中寻找 New Device Simulator,启用后按提示重启 Studio。Roblox 会更新 Studio 界面,所以按钮位置可能变化,按功能名称找比死记图标位置更可靠。

  1. 按 F5 开始测试,再打开 Device Simulator;先选择一种 Phone 竖屏预设。
  2. 验收竖屏:面板左边距和上边距约 18 像素,右边仍有空隙,标题与目标没有被裁掉。
  3. 切到横屏:面板不会无限拉宽,最终宽度不超过 420 像素。
  4. 切回 Desktop:进度条、1 / 3 和状态文字仍在同一行序中,没有重叠。
  5. Output 没有红色错误后,Shift+F5 停止并 Ctrl+S 保存。
06 · 像上线前一样测试电脑和手机
现象最可能原因立即检查
完全没有 HUD脚本类型或位置错误必须是 AdventureGui 下的 LocalScript,并用 F5 测试
Output 报语法错误复制时少了 end、括号或引号双击红色错误,核对完整代码对应行
手机上超出右边仍在使用 700 像素失败示例恢复 0.36 宽度和 UISizeConstraint
数字跑到面板外Count 的 AnchorPoint 被改成 0,0恢复 Vector2.new(1, 0)
进度条一直为空没有调用 renderQuest 或 current 为 0保留最后一行 renderQuest(1, 3)
Stop 后 Panel 消失这是代码运行时创建的对象正常;HudController 会在下一次 F5 时重建

Homework

轮到你来改一改,做出自己的版本。

1 换成自己的任务

把标题改成自己的游戏名,把“晶石”换成钥匙、金币或机器人零件,并把总数改为 5。

2 做一张验收对比图

保存 Desktop 和 Phone 竖屏各一张截图,圈出面板边界、进度条和右侧计数。

3 选做:加入危险状态

当 current 为 0 时,把 Status 改成“还没有找到晶石”,颜色改为橙色;1 到 total-1 仍显示探索提示。

完成标准:先在 Playtest 中验证作品,再点击 Stop,保存为“姓名缩写_Lesson15”或对应 Homework 副本;不要覆盖上一课的可运行版本。

Exit questions

先自己回答,再展开看看。

为什么 HudController 要用 LocalScript?

HUD 属于当前玩家的屏幕。LocalScript 会在玩家客户端运行并操作这位玩家的 PlayerGui。

UDim2.new(0.36, 0, 0, 176) 的 0.36 和 176 分别是什么?

0.36 是父容器宽度的 36%;176 是固定的高度像素偏移。

为什么 Count 使用 AnchorPoint=(1,0)?

Position 的坐标会对齐 Count 的右上角,因此面板宽度变化时,计数仍贴在右边。

renderQuest(9, 3) 为什么不会让进度条冲出面板?

math.clamp 把 current 限制在 0 到 total 之间,最终比例最多为 1。

Stop 后运行时的 Panel 消失是不是 bug?

不是。它是 LocalScript 在 PlayerGui 副本中创建的;下次 F5 会重新生成。要长期修改,应修改 StarterGui 中的 HudController。

Explore further

需要查资料时,从这里出发。

正文代码可以直接复制。先确认脚本类型、放置位置,以及这段代码是完整版本还是小实验;不要把同一脚本的多个版本叠加运行。界面参考图已标注来源,图中的对象名可能与本课不同,请以操作步骤为准。

下载本课代码与放置说明