开始之前
理解服务器、LocalScript 与 Attributes。新建 Lesson26 Baseplate。
Course overview
一个小游戏不能永远停留在“正在进行”。回合系统把时间分成几个状态,让所有人知道现在能做什么。
本课先只做状态与倒计时,不加入得分。服务器安排阶段,客户端读取阶段和截止时间,后来加入的玩家也能看见当前状态。
理解服务器、LocalScript 与 Attributes。新建 Lesson26 Baseplate。
循环运行的 8 秒准备、20 秒游戏、5 秒结算,屏幕显示阶段和剩余时间。
两个客户端显示相同阶段;迟加入的玩家不会从 20 重新倒数。

Learning route
每完成一小步,就测试一次。先说出你预计会看到什么,再用 Play 和 Output 检查,不用急着一次写完所有代码。
我们用 Intermission、Playing、Results 三个字符串表示状态。它们像交通灯:每种状态允许的行为不同。
如果每次 task.wait(1) 后简单减 1,等待的细小误差会逐渐累积。我们改为记录“阶段在服务器时间的哪一刻结束”。
客户端每隔 0.2 秒更新一次显示,用截止时间减当前时间,再向上取整。这样不会在阶段刚开始时少显示一秒。
客户端即使显示零秒,也不能自行发奖。奖励、传送和是否接受收集,仍然由服务器当前状态决定。
两个窗口秒数可能受刷新节奏影响短暂差一格,重点是它们使用同一个截止时刻和服务器阶段,而不是各走各的倒计时。
Step by step
我们用 Intermission、Playing、Results 三个字符串表示状态。它们像交通灯:每种状态允许的行为不同。
本课只是回合时钟,Results 阶段不虚构分数或奖励。以后把收集功能接进来时,服务器应在 Playing 状态才允许计分。
Step by step
如果每次 task.wait(1) 后简单减 1,等待的细小误差会逐渐累积。我们改为记录“阶段在服务器时间的哪一刻结束”。
GetServerTimeNow 提供可用于同步显示的服务器时间。task.wait 只控制检查频率,真正决定结束的是现在是否到达截止时刻。
local function runPhase(state, seconds)
local deadline = workspace:GetServerTimeNow() + seconds
workspace:SetAttribute("RoundEndsAt", deadline)
workspace:SetAttribute("RoundState", state)
print("Phase:", state)
while workspace:GetServerTimeNow() < deadline do
task.wait(0.1)
end
end
while true do
runPhase("Intermission", 8)
runPhase("Playing", 20)
runPhase("Results", 5)
end
Step by step
客户端每隔 0.2 秒更新一次显示,用截止时间减当前时间,再向上取整。这样不会在阶段刚开始时少显示一秒。
如果属性还没传过来,就显示等待同步。它不自行启动另一个独立的 20 秒游戏计时器。
local player = game:GetService("Players").LocalPlayer
local gui = Instance.new("ScreenGui")
gui.Name = "RoundGui"
gui.ResetOnSpawn = false
local label = Instance.new("TextLabel")
label.Size = UDim2.fromOffset(300, 60)
label.Position = UDim2.new(0.5, -150, 0, 16)
label.TextSize = 22
label.Parent = gui
gui.Parent = player:WaitForChild("PlayerGui")
local names = {Intermission = "准备", Playing = "游戏进行中", Results = "本轮结束"}
while gui.Parent do
local state = workspace:GetAttribute("RoundState")
local deadline = workspace:GetAttribute("RoundEndsAt")
if state and deadline then
local remaining = math.max(0, math.ceil(deadline - workspace:GetServerTimeNow()))
label.Text = (names[state] or state) .. " · " .. remaining .. " 秒"
else
label.Text = "等待回合同步…"
end
task.wait(0.2)
end
Step by step
客户端即使显示零秒,也不能自行发奖。奖励、传送和是否接受收集,仍然由服务器当前状态决定。
接入任务脚本时,可以在处理函数开头检查 workspace:GetAttribute("RoundState") ~= "Playing" 并 return。但不要随意加到第 19 课后就称为完整回合游戏,还需要每轮重置任务记录。
Step by step
两个窗口秒数可能受刷新节奏影响短暂差一格,重点是它们使用同一个截止时刻和服务器阶段,而不是各走各的倒计时。
服务器结束 Play 后循环会随测试停止。本课循环内部始终有等待,不会用空转占满电脑。

Homework
把阶段改为 5、30、5 秒,计算一整轮的时长。
写出哪个阶段允许收集、哪个阶段展示结果、何时重置。
让 HUD 背景随阶段变化,同时保留中文文字,避免只靠颜色。
Exit questions
减少等待误差累积,也让后来加入的客户端能算出当前剩余时间。
服务器,客户端只显示状态。
不够,已领物品和完成记录等本轮数据也要一起重置。
没有,它只实现阶段与同步时钟,奖励需要额外的服务器规则。
Explore further
正文代码可以直接复制。先确认脚本类型、放置位置,以及这段代码是完整版本还是小实验;不要把同一脚本的多个版本叠加运行。界面参考图已标注来源,图中的对象名可能与本课不同,请以操作步骤为准。