开始之前
会打开 Explorer、Properties、Output 和 Script Editor。新建 Baseplate,并另存为 Lesson15。
Roblox Studio · 第 15 课
用完整 LocalScript 创建任务卡、数字进度和进度条,再用 Scale、Offset、AnchorPoint 与尺寸约束完成手机适配。
Course overview
这一课不让你照着概念表抄属性。你会在 StarterGui 创建一个 ScreenGui,再用一段完整的 LocalScript 生成任务面板。按下 Play 后,左上角会出现“能量岛探险”、目标文字、进度条和 1 / 3 计数。
完成后你还能把进度改成 0、2、3,马上看到界面如何变化;再故意做一次固定宽度的错误版本,对比 Scale、Offset、AnchorPoint 和尺寸约束到底解决了什么问题。
会打开 Explorer、Properties、Output 和 Script Editor。新建 Baseplate,并另存为 Lesson15。
一个由 LocalScript 创建的响应式任务 HUD,包含标题、任务、进度条、数字进度和完成提示。
F5 测试时显示 1 / 3;把代码改为 3 / 3 后进度条填满并显示“任务完成”;手机竖屏中面板不超出右边界。

Learning route
每完成一小步,就测试一次。先说出你预计会看到什么,再用 Play 和 Output 检查,不用急着一次写完所有代码。
打开 Lesson15 Baseplate 后,先在 View(视图)菜单打开 Explorer、Properties 和 Output。今天只有一个验收目标:按 F5 后,玩家屏幕左上角出现任务卡;把最后一行的数字从 1 改成 3,界面立刻变成完成状态。
先手动创建 ScreenGui 和 LocalScript,其余 Frame、TextLabel 和进度条全部由代码生成。这样既能看懂 Explorer 层级,也不会因为漏改十几个属性而卡住。
下面的 create() 是一个小型工厂函数:创建对象、批量设置属性、最后指定 Parent。完整 HUD 都由这一个 LocalScript 建立,所以你可以直接复制运行。
现在不要继续抄新代码。只改一处、运行一次、记录结果。工程师调界面时会把输入压到边界值,因为 0、刚好完成、超过总数最容易暴露错误。
Panel.Size = UDim2.new(0.36, 0, 0, 176) 的四个数字依次是 X Scale、X Offset、Y Scale、Y Offset。宽度取父容器的 36%,高度固定 176 像素;UISizeConstraint 再把最终宽度限制在 280 到 420 像素。
桌面预览正常只是第一关。Device Simulator 可以模拟不同屏幕尺寸、方向和触屏环境。测试时既看“有没有显示”,也检查文字是否被裁掉、任务卡是否挡住移动区、横竖屏切换后是否超出画面。
Step by step
打开 Lesson15 Baseplate 后,先在 View(视图)菜单打开 Explorer、Properties 和 Output。今天只有一个验收目标:按 F5 后,玩家屏幕左上角出现任务卡;把最后一行的数字从 1 改成 3,界面立刻变成完成状态。
任务卡属于每位玩家自己的屏幕,因此放在 StarterGui。不要在 Workspace 创建一块 Part 来冒充 HUD;场景 Part 会受相机距离和角度影响,ScreenGui 中的界面才固定在屏幕上。
Step by step
先手动创建 ScreenGui 和 LocalScript,其余 Frame、TextLabel 和进度条全部由代码生成。这样既能看懂 Explorer 层级,也不会因为漏改十几个属性而卡住。
LocalScript 必须在玩家客户端能运行的位置。本课把它放进 StarterGui 下的 AdventureGui。按下 Play 时,Roblox 会把 AdventureGui 复制到当前玩家的 PlayerGui,然后运行 HudController。
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
下面的 create() 是一个小型工厂函数:创建对象、批量设置属性、最后指定 Parent。完整 HUD 都由这一个 LocalScript 建立,所以你可以直接复制运行。
代码最后的 renderQuest(1, 3) 表示“已经找到 1 颗,总共需要 3 颗”。renderQuest() 同时更新任务文字、数字、进度条宽度和状态颜色,避免四个地方显示互相矛盾。
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)
Step by step
现在不要继续抄新代码。只改一处、运行一次、记录结果。工程师调界面时会把输入压到边界值,因为 0、刚好完成、超过总数最容易暴露错误。
math.clamp(current, 0, total) 把异常进度限制在合法范围内。因此 renderQuest(9, 3) 不会画出三倍宽的进度条,而会按 3 / 3 处理。
-- 只改这一行,然后每次按 F5 检查:
renderQuest(0, 3) -- 空进度
renderQuest(2, 3) -- 三分之二
renderQuest(3, 3) -- 完成状态
renderQuest(9, 3) -- 越界输入会被 math.clamp 修正
| 输入 | 应该看见 | 它验证了什么 |
|---|---|---|
| 0, 3 | 0 / 3,空进度条 | 起始状态 |
| 2, 3 | 2 / 3,蓝条约 67% | Scale 可以表示比例 |
| 3, 3 | 3 / 3,绿色完成提示 | 完成分支 |
| 9, 3 | 仍为 3 / 3 | 越界保护 |
Step by step
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 坐标固定的是它的右边缘;面板变宽时,数字仍贴在右侧。
-- 完整程序中已有这个实验开关:
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,再测试正常的响应式方案。
| 写法 | 含义 | 本课位置 |
|---|---|---|
| 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 |

Step by step
桌面预览正常只是第一关。Device Simulator 可以模拟不同屏幕尺寸、方向和触屏环境。测试时既看“有没有显示”,也检查文字是否被裁掉、任务卡是否挡住移动区、横竖屏切换后是否超出画面。
如果你的 Studio 里暂时看不到 Device Simulator,在 File → Beta Features 中寻找 New Device Simulator,启用后按提示重启 Studio。Roblox 会更新 Studio 界面,所以按钮位置可能变化,按功能名称找比死记图标位置更可靠。
| 现象 | 最可能原因 | 立即检查 |
|---|---|---|
| 完全没有 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
把标题改成自己的游戏名,把“晶石”换成钥匙、金币或机器人零件,并把总数改为 5。
保存 Desktop 和 Phone 竖屏各一张截图,圈出面板边界、进度条和右侧计数。
当 current 为 0 时,把 Status 改成“还没有找到晶石”,颜色改为橙色;1 到 total-1 仍显示探索提示。
Exit questions
HUD 属于当前玩家的屏幕。LocalScript 会在玩家客户端运行并操作这位玩家的 PlayerGui。
0.36 是父容器宽度的 36%;176 是固定的高度像素偏移。
Position 的坐标会对齐 Count 的右上角,因此面板宽度变化时,计数仍贴在右边。
math.clamp 把 current 限制在 0 到 total 之间,最终比例最多为 1。
不是。它是 LocalScript 在 PlayerGui 副本中创建的;下次 F5 会重新生成。要长期修改,应修改 StarterGui 中的 HudController。
Explore further
正文代码可以直接复制。先确认脚本类型、放置位置,以及这段代码是完整版本还是小实验;不要把同一脚本的多个版本叠加运行。界面参考图已标注来源,图中的对象名可能与本课不同,请以操作步骤为准。
下载本课代码与放置说明