Skip to content

04 进度条与场景 UI ​

给作物头顶挂一条进度条:讲清场景 UI 和屏幕 UI 的区别,以及它为什么只做给自己看。

你将做出什么 ​

在作物的位置上方出现一条进度条,从 0% 走到 100% 之后消失——而且只有你自己看得到。

作物头顶的进度条走动

等待是最容易让人走神的时候。种下之后要等 3 秒,光看模型不知道"还要多久"——这一课就补上这个反馈。

1. 打开界面编辑器,开始做界面 ​

界面要在界面编辑器里做。先把编辑器打开,我们边做边认:入口在哪、面板长什么样、每个属性管什么。

UI 编辑器的入口与面板

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

打开 UI 编辑器、看节点树与属性面板

场景 UI屏幕 UI
挂在哪某一个物件身上UI 根节点下(EuiManager:GetRootNode())
属于谁那个物件玩家自己
跟着谁动物件挪到哪,它跟到哪固定在屏幕上
例子作物头顶的进度条、血条分数、状态文字、计分板

判断标准一句话:这条信息属于谁? 属于某个物件的,挂它身上;属于玩家自己的,挂屏幕上。

UI 体系(EUI / CoreGUI、布局与属性):专题「UI · EUI」

进度条属于"这一株作物",所以这一课用场景 UI。

这条进度条怎么做出来 ​

我们要的是一个场景 UI(挂在作物身上的那种)。做法和原点编辑器里做界面一样:新建一个界面 → 摆放控件 → 调属性。

它不用复杂,里面就两样东西:

text
进度条界面
├── progress_bar_bg        底板
│   └── 成长进度           进度条本体(改它的 Percent 就是"走了多少")
└── 进度值                 文字,显示 "45%"

进度条界面的层级与效果

做好之后存成预设——待会儿代码要按它的 id 把它创建出来。这个 id 就在预设列表里的「界面」分类下能找到。

text
预设
└── 界面
    └── 你的进度条        ← 复制它的资源 id, 代码里要用

预设 → 界面 分类里找到进度条预制体,复制资源 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 秒" → 客户端在作物头顶挂一条进度条 → 自己按同样节拍走 → 走完销毁。

为什么进度条自己做,不让服务端每秒同步? 三个好处:

  1. 省——不用每秒发消息
  2. 稳——不会因为网络卡顿而一格一格跳
  3. 对——"还剩几秒"本来就是每个人自己看的东西:场景物件(比如天气的天空)不用通知,而"你种的这株要长多久"只有种的人关心

运行并核对 ​

  1. 点一下地块 → 种子升起,头顶出现进度条,走完正好 3 秒后消失,地里冒出果实。
  2. 走到远处回头看 → 进度条跟着作物留在原地(不会像屏幕 UI 那样贴在视野里)。
  3. 换个地块种 → 新的一株头顶也会出现自己的进度条。

现象 → 可能原因:

  • 完全没有进度条 → 服务端那行 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。

API:EuiManager、TimerService、RemoteEvent。