主题
04 进度条与场景 UI
给作物头顶挂一条进度条:讲清场景 UI 和屏幕 UI 的区别,以及它为什么只做给自己看。
你将做出什么
在作物的位置上方出现一条进度条,从 0% 走到 100% 之后消失——而且只有你自己看得到。

等待是最容易让人走神的时候。种下之后要等 3 秒,光看模型不知道"还要多久"——这一课就补上这个反馈。
1. 打开界面编辑器,开始做界面
界面要在界面编辑器里做。先把编辑器打开,我们边做边认:入口在哪、面板长什么样、每个属性管什么。

打开之后你会发现,界面分两种挂法:

| 场景 UI | 屏幕 UI | |
|---|---|---|
| 挂在哪 | 某一个物件身上 | UI 根节点下(EuiManager:GetRootNode()) |
| 属于谁 | 那个物件 | 玩家自己 |
| 跟着谁动 | 物件挪到哪,它跟到哪 | 固定在屏幕上 |
| 例子 | 作物头顶的进度条、血条 | 分数、状态文字、计分板 |
判断标准一句话:这条信息属于谁? 属于某个物件的,挂它身上;属于玩家自己的,挂屏幕上。
UI 体系(EUI / CoreGUI、布局与属性):专题「UI · EUI」
进度条属于"这一株作物",所以这一课用场景 UI。
这条进度条怎么做出来
我们要的是一个场景 UI(挂在作物身上的那种)。做法和原点编辑器里做界面一样:新建一个界面 → 摆放控件 → 调属性。
它不用复杂,里面就两样东西:
text
进度条界面
├── progress_bar_bg 底板
│ └── 成长进度 进度条本体(改它的 Percent 就是"走了多少")
└── 进度值 文字,显示 "45%"
做好之后存成预设——待会儿代码要按它的 id 把它创建出来。这个 id 就在预设列表里的「界面」分类下能找到。
text
预设
└── 界面
└── 你的进度条 ← 复制它的资源 id, 代码里要用
2. 界面准备好了,怎么在生长时显示出来
先想一个事:谁需要看到这条进度条?
不是所有人——只有种下这颗种子的人才需要。别人既不知道你种了什么,也不关心你在等多久。
所以这件事是客户端逻辑(每个人自己的画面),而不是"世界里的事"。但它有个矛盾:"这株开始长了、要长几秒"这件事发生在服务端(生长是服务端算的),客户端怎么知道?
答案就是跨端通信——用的东西叫 RemoteEvent(远端事件)。它是这么一套东西:
text
RemoteEvent = 一条两端共用的"对讲机频道"
注册: 起个名字(两端同名, 存在 common/ 里)
发送: 服务端 FireClient(玩家, 数据) -- 只发给某个人
监听: 客户端 OnClientEvent:Connect(...) -- 接到就处理先把频道建起来:
双端通信的完整讲法:专题「双端与 RemoteEvent」
- 文件:
common/remote_event.lua(新建) 操作:写入并保存
lua
-- common/remote_event.lua —— 两端共用的自定义事件
return {
grow = RemoteEvent.New("Farm.Grow"), -- 服务端 → 客户端: 这株开始长了, 要长几秒
sell = RemoteEvent.New("Farm.Sell"), -- 服务端 → 客户端: 卖了一颗果实(分数飘字用)
}💡 Tips · 什么东西适合放进
common/
common/是两端都能require的共享目录,所以它只该放"两边都要用同一份"的东西:
- 事件名(
remote_event.lua):服务端发、客户端收,名字必须一模一样,写两处迟早对不上- 数值配置(
config.lua):服务端算、客户端显示,两边读同一份才不会打架- 纯函数小工具:两边都要用同一套算法的(比如换算、格式化)
不适合放:只有一端用的东西(比如服务端专用的表、客户端专用的界面节点查找)——放进去反而让人以为两边都在用。
3. 服务端:发一条消息给种它的人
这一课不动生长逻辑——第 03 课的分步放大照旧跑,我们只是在同一处多告诉客户端一句"要长几秒":
- 文件:
server/main.lua操作:加两处(顶部 require +tween.Completed里一行)
lua
local RemoteEvent = require("common.remote_event") -- ① 顶部加
tween.Completed:Once(function()
PlantGrow(seed, plot) -- 上一课写的, 不动
-- ② 只加这一行: 告诉种它的人"要长几秒"
RemoteEvent.grow:FireClient(player, { plant = seed, time = GROW_TIME })
end)FireClient(player, 数据) = 把数据发给这一个人。传过去两样东西,都很有用:
| 传什么 | 客户端拿它干什么 |
|---|---|
plant | 哪一株——进度条就挂在它头顶 |
time | 要等多久——客户端自己按这个时长走进度 |
4. 客户端:接住消息,把进度条挂到作物身上
客户端要做三件事:接住 grow 消息 → 在作物身上创建出进度条界面 → 按时长把进度走完。
第一步:先把界面创建出来
界面管理器是 EuiManager(挂在 PlayerGui 上):开界面、找界面、挂界面,都问它。
要在物件身上创建一个界面,用它的这个方法:
lua
local localPlayer = Players.LocalPlayer
if localPlayer == nil then return end
local euiManager = localPlayer.PlayerGui.EuiManager
-- 用预制体创建一个"挂在作物身上"的界面, 返回的就是这个界面实例
local sceneNode = euiManager:CreateSceneUIByPrefabKeyAttachUnit(
"map://preset/uc29aefa4af8448d9ab658a324843e45", -- 进度条预制体的 id
plant, -- 挂在谁身上
"Head", -- 挂在哪个插槽(头顶)
Vector3.New(0, 5, 0), -- 再抬高 5 格
false)拿到 sceneNode,就是我们那张进度条界面了。
第二步:怎么在里面找到要改的控件
界面和场景物件一样,都是一棵按层级排的树:从根节点往下,按名字找。而且界面节点继承自 Unit——场景物件有的方法它都有,所以第 01 课那套找法在这儿照样管用:
lua
-- 从这张界面里往下找: 底板 → 进度条本体
local bar = sceneNode:FindFirstChild("progress_bar_bg"):FindFirstChild("成长进度")
local txt = sceneNode:FindFirstChild("进度值")
bar.Percent = 50 -- 进度条走到 50%
txt.Text = "50%" -- 文字跟着写
FindFirstChild(name, true)里的true是递归往下找:控件可能被包在布局节点里,不在第一层。代价是重名时会拿错,所以名字别重复。
第三步:把两件事接起来
创建完界面、知道怎么找控件之后,剩下的就是按时长不断改进度——照旧用计时器。
- 文件:
client/main.lua操作:写入(本课只加这一段)
lua
-- @runtime client
local Players = game:GetService("Players")
local TimerService = game:GetService("TimerService")
local RemoteEvent = require("common.remote_event")
local localPlayer = Players.LocalPlayer
if localPlayer == nil then return end
local euiManager = localPlayer.PlayerGui.EuiManager
RemoteEvent.grow.OnClientEvent:Connect(function(data)
local plant = data.plant -- 哪一株
local time = data.time -- 要等几秒
if not plant then return end
-- ① 在作物身上挂出进度条界面(用预制体, 挂在 Head 插槽, 再抬高 5 格)
local sceneNode = euiManager:CreateSceneUIByPrefabKeyAttachUnit(
"map://preset/uc29aefa4af8448d9ab658a324843e45", plant, "Head", Vector3.New(0, 5, 0), false
)
-- ② 用计时器把进度走完(和第 03 课同一个写法, 只是这里动的是界面)
local n = 0
local count = math.ceil(time / 0.06)
TimerService:CreateTimer(count, 0.06, false, function()
n = n + 1
local percent = n / count * 100
-- ③ 从这张界面里按层级取出控件, 改进度和文字
sceneNode:FindFirstChild("progress_bar_bg"):FindFirstChild("成长进度").Percent = percent
sceneNode:FindFirstChild("进度值").Text = tostring(math.ceil(percent)) .. "%"
-- ④ 走完就销毁, 别把节点留在场景里
if n == count then sceneNode:Destroy() end
end)
end)进度的变化,用计时器去写就行——和第 03 课同一套写法,只是这次改的不再是作物大小,而是进度条的百分比和那行文字。
四步连起来:服务端说"开始长了、要 3 秒" → 客户端在作物头顶挂一条进度条 → 自己按同样节拍走 → 走完销毁。
为什么进度条自己做,不让服务端每秒同步? 三个好处:
- 省——不用每秒发消息
- 稳——不会因为网络卡顿而一格一格跳
- 对——"还剩几秒"本来就是每个人自己看的东西:场景物件(比如天气的天空)不用通知,而"你种的这株要长多久"只有种的人关心
运行并核对
- 点一下地块 → 种子升起,头顶出现进度条,走完正好 3 秒后消失,地里冒出果实。
- 走到远处回头看 → 进度条跟着作物留在原地(不会像屏幕 UI 那样贴在视野里)。
- 换个地块种 → 新的一株头顶也会出现自己的进度条。
现象 → 可能原因:
- 完全没有进度条 → 服务端那行
RemoteEvent.grow:FireClient加了吗;common/remote_event.lua建了吗 - 报错找不到预制体 → 那个
map://preset/...路径是不是进度条界面的预制体 - 进度条不显示内容 → 节点名对不上:
progress_bar_bg→成长进度的Percent、进度值的Text - 进度条走完不消失 →
n == count那行判断;Destroy有没有执行
拓展点 / 自己改一处
- 把进度条换成你自己做的预制体(换个颜色、加个作物图标)
- 让进度条在最后 20% 变个颜色(提示:在回调里按
percent判断) - 想想:如果要给作物加一条"成熟了"的感叹号提示,它该是场景 UI 还是屏幕 UI?(提示:这条信息属于谁?)
保存进度
- 下一步:05 放置区与计分——把果实搬进区域,换成分数。
- 通过标准:
grow通道发得出、收得到;场景 UI 挂在Head上;进度走完销毁。 - 恢复兜底:界面乱了就把预制体路径恢复成课文里的;代码乱了用代码包覆盖
client/main.lua。
