Skip to content

04 做出完整的一局 ​

用服务器时间决定开局和结算,结束时停止计分,等待三秒自动开始下一局。

现在给原型加一条规则:每局 20 秒,结束后停 3 秒,再自动开始。连续两局都能运行,才算完成这课。

开始前检查 ​

从第 03 课继续,球能够重复命中、复位;场景与搭建指南相同。这课仍显示全场命中数,只看服务端日志,不需要 UI。

1. 安装本课完整代码 ​

文件:server/main.lua、client/main.lua。操作:下载第 04 课检查点,停止试玩,把包中的两个文件放到工程同路径,整文件替换并保存同步。

下面的代码块是本课完整工程中的解释片段,不单独粘贴。文件中还保留了上一课的查找、计数、碰撞和清理;下载包帮你把它们放在正确的位置。

2. 先找到三个可调数字 ​

文件:server/main.lua 的 settings 区域。操作:打开文件,确认这三个值,第一次先不修改。

lua
-- [对局时间与规则参数]
local roundSeconds = 20                 -- 单局游玩时长(秒)
local hitCooldown = 1                   -- 碰撞防抖冷却(秒)
local restartDelay = 3                  -- 结算后自动进入下一局的缓冲时长(秒)

预期:试玩后出现“第 1 局,时长: 20”,随后每秒左右输出一次剩余时间。一局的三个阶段是:

text
开始:计数清零、球复位
  ↓
running:允许计分,直到服务器时间到达 endTime
  ↓
waiting:不再计分,等待 3 秒
  ↓
下一局重新开始

3. 跟着一局读代码 ​

文件:server/main.lua 的 round 区域。操作:不用继续修改;把代码中的两个函数与上面的流程对应起来。

lua
-- [开局逻辑] 初始化一局的新状态,重置计数并设置结束时间
local function startRound()
    if not alive or phase == "running" then return end
    roundId = roundId + 1
    hits = 0
    nextHitTime = 0
    nextLogTime = 0
    endTime = World:GetServerTime() + roundSeconds
    phase = "running"
    resetBall()
    print("[推球][开局] 第", roundId, "局,时长:", roundSeconds)
end

-- [心跳驱动更新] 检查对局倒计时、触发超时结算以及下一局自动开启
local function updateRound()
    local now = World:GetServerTime()
    if phase == "running" then
        -- 判定当前局是否超时
        if now >= endTime then
            phase = "waiting"
            nextStartTime = now + restartDelay
            print("[推球][结算] 第", roundId, "局结束,3 秒后自动开局", "全场命中数:", hits)
            return
        end
        -- 约每秒输出一次倒计时日志(节流控制)
        if now >= nextLogTime then
            nextLogTime = now + 1
            print("[推球][倒计时]", math.max(0, math.ceil(endTime - now)))
        end
    elseif now >= nextStartTime then
        -- 等待缓冲结束,自动进入下一局
        startRound()
    end
end

状态机流转与权威时间 ​

对局状态机在 phase 的控制下循环运转:

text
startRound()
  │
  ▼
phase = "running"
  允许推球碰撞计分,约每秒打印一次倒计时
  │
  │ 达到 endTime
  ▼
phase = "waiting"
  停止计分,等待 3 秒,重置球体与命中数
  │
  │ 等待结束
  └──────────────────> 再次调用 startRound()(进入下一局)

为什么用 World:GetServerTime() 而不是数帧数?

  • 帧率会受机器性能波动影响(卡顿时一秒可能只有 15 帧,流畅时有 60 帧),靠“每帧累加 delta”容易产生误差。
  • 记录一个权威的时间戳 endTime = now + roundSeconds,每次只需判断 now >= endTime,无论帧率如何波动,倒计时都不会累积误差。

Heartbeat 帧循环与日志节流: RunService.Heartbeat 是每帧触发的心跳事件(一秒数十次)。如果每帧都 print 会导致日志窗口卡顿;通过 nextLogTime = now + 1,我们实现了“约每秒只打印一次倒计时”的节流。碰撞函数中同样检查了 now >= endTime,双重保护确保超时绝对不误加分。

运行并核对 ​

文件:无需修改。操作:第一局命中一次;等到结算,再在等待阶段推动球接触方块;继续观察下一局。

预期:

  • 第一局的命中数增加。
  • 出现 [推球][结算] 后,等待阶段不再加分。
  • 约 3 秒后出现第 2 局日志,球回到起点,新局重新从 0 计数。
  • 连续两局只各出现一次开局、一次结算。

如果一直重复“第 1 局”,先确认自己是否反复重启了试玩。如果等待阶段仍加分,确认覆盖的是本课整个文件,而不是在旧回调后又追加了新回调。

自己改一处 ​

文件:server/main.lua。操作:只把 roundSeconds = 20 改成 roundSeconds = 5。

预测:等待时间会不会也变成 5 秒?提示:它由另一个变量控制。答案:每局约 5 秒,局间仍等待 3 秒。连续跑两局验证,再把时长恢复为 20。

保存进度 ​

保存第 04 课包作为恢复点。通过标准是“连续两局正常,结算后不再加分,新局清零”,不要求记住整个函数。

下一课:把分数记给正确玩家。API:World、RunService、WorldUnit。深读:双端与逻辑帧。