Skip to content

选择运行端并理解逻辑帧 ​

理解统一世界中的双端职责、服务端权威与逻辑帧更新边界。

什么时候查这篇 ​

逻辑在错误的一端执行,或需要安排每帧和计时任务时,使用本专题。按当前需求选择小节即可,不必按专题编号顺序读完。

  • 开始前:能分辨 client/server 输出;知道事件回调是稍后执行的函数。
  • 安装与示例范围:按小节声明的运行端分别试验;帧事件片段需要保存连接并在结束时断开,不能把多个每帧试验叠加。
  • 本次要看到:能指出一次玩法结果由谁决定、哪端只显示,以及回调何时发生。

如果还没有完成可运行的小游戏,先回到主线:04 课。语法卡住时查Lua 速查,运行结果不符时查按现象排错。

你会学到什么 ​

  • 世界版"统一运转的世界"在脚本里的含义。
  • server 和 client 的职责分工。
  • 逻辑帧时序和 Heartbeat 的使用。
  • 如何用 World:GetServerTime() 做倒计时。

统一世界 ​

在世界版中,所有玩家连入同一个持续运转的世界。这意味着:

  • 服务端(server):权威状态。计分、倒计时、结算、存储、命中判定。
  • 客户端(client):本地表现。UI 显示、输入响应、相机、音效。
  • common:双端共享的常量、数据定义和纯工具函数;不要在这里保存权威的可变状态。

如果你把分数放在客户端,玩家可以修改本地数据来作弊。所以权威状态必须在服务端。

从编辑器经验看,你可以把它理解成:触发器、计时器、积分板这些可视化配置在单人调试时看起来都在“本地发生”,但一旦进入多人同场地图,就必须拆成两类问题:谁来判定事实,谁来显示结果。Lua 中的 server / client 分层,就是把这个边界写清楚。

先问三句话 ​

初学双端模型时,不要先背 API。每写一段逻辑,先问三句话:

问题通常答案例子
谁判定事实?server是否得分、是否结算、是否保存最高分
谁显示结果?clientHUD 文本、按钮状态、音效、相机反馈
失败时谁兜底?server 保留权威状态,client 做降级提示UI 节点没找到时打印 WARN,但回合仍继续

这个判断比“代码放哪个文件夹”更重要。目录只是结果:判定事实的逻辑自然进入 server/,本地表现自然进入 client/,两端都要读的常量和纯工具才进入 common/。

逻辑帧时序 ​

SE 的逻辑帧通常每秒约 30 次,按以下顺序执行。实际间隔可能波动,因此需要按时间推进的逻辑应读取事件给出的 deltaTime,不要把“一帧”硬编码成固定秒数。

text
PreSimulation → 物理模拟 → PostSimulation → Heartbeat → FrameUpdate → PostFrameUpdate

最常用的是 Heartbeat,每帧触发一次,适合周期性检查。

最小示例:服务端倒计时 ​

运行端:server
文件:server/main.lua

示例类别:独立实验;运行端:server;文件:server/main.lua。

lua
local RunService = game:GetService("RunService")
local World = game:GetService("World")

-- 设定 10 秒倒计时
local startTime = World:GetServerTime()
local duration = 10
local nextLogTime = 0
local heartbeatConnection

-- Heartbeat:每个逻辑帧触发一次
heartbeatConnection = RunService.Heartbeat:Connect(function()
    local now = World:GetServerTime()
    local remaining = duration - (now - startTime)

    if remaining <= 0 then
        print("[SE Lua Guide] 倒计时结束!")
        -- 不再需要逐帧检查时,主动断开连接。
        heartbeatConnection:Disconnect()
        heartbeatConnection = nil
        return
    end

    -- 节流:每秒只打印一次,避免每帧刷屏
    if now >= nextLogTime then
        nextLogTime = now + 1
        print("[SE Lua Guide] 剩余时间:", math.ceil(remaining), "秒")
    end
end)

进阶:用 IsServer 区分运行端 ​

在 common 代码中,可以用 RunService 判断当前运行端:

示例类别:独立实验;运行端:server;文件:server/main.lua。

lua
local RunService = game:GetService("RunService")

if RunService:IsServer() then
    print("这是服务端")
elseif RunService:IsClient() then
    print("这是客户端")
end

Heartbeat 的参数 ​

示例类别:独立实验;运行端:server;文件:server/main.lua。

lua
RunService.Heartbeat:Connect(function(deltaTime)
    -- deltaTime:本次逻辑帧的时间长度(秒)
end)

当前公开契约只要求接收 deltaTime。它表示本次逻辑帧的时间长度,可用于按时间更新玩法状态。

客户端如何显示服务端倒计时 ​

上面的倒计时完全在服务端运行,只在服务端日志中输出。但客户端的 UI 也需要显示剩余时间,怎么做?

核心思路分三步:

  1. 客户端先完成监听并向服务端报告“我已准备好”,避免服务端在客户端订阅前就把开局消息发掉。
  2. 服务端保存权威的回合状态,并把“状态快照”发给已准备好的客户端。快照至少包含当前阶段和结束时间。
  3. 客户端缓存快照,用自己的 Heartbeat 刷新显示;新加入或重连时再次请求最新快照。

这里需要用到 RemoteEvent(远程事件)。它是客户端和服务端之间通信的桥梁。当前公开契约中,每次发送只有一个可选的 Any 参数;需要传多个字段时,应把它们装进同一个 table,而不是写成多个位置参数。

用请求和快照同步双端状态会给出完整的“先监听、再握手、后同步”代码。本专题先记住数据流和权威边界,避免复制一个存在启动竞态的半成品。

数据流向 ​

text
服务端                              客户端
  |                                   |
  | ◄──────── 客户端已准备好 ──────── |
  |                                   |
  |  回合快照 { phase, endTime }      |
  |  ───────────────────────────────► |
  |                                   |  缓存快照
  |                                   |  Heartbeat: 计算 remaining 并显示
  |                                   |
  |  Heartbeat: 倒计时结束            |
  |  新的权威状态快照                 |
  |  ───────────────────────────────► |
  |                                   |  停止显示倒计时

关键理解:客户端的倒计时只是显示。游戏是否真正结束,由服务端的 Heartbeat 判定。即使客户端的显示有误差,也不会影响游戏逻辑。

常见错误 ​

错误:客户端决定游戏结算 ​

客户端 UI 可以显示倒计时,但游戏是否结束应由服务端决定。否则玩家可以伪造本地状态。

错误:Heartbeat 里每帧打印 ​

Heartbeat 每秒触发约 30 次,如果不节流,日志会被刷屏。用 nextLogTime 控制打印频率。

错误:Heartbeat 里做重活 ​

不要在 Heartbeat 里创建对象、频繁查找或做复杂计算。它适合轻量级的状态检查。

练习任务 ​

  1. 把倒计时改成 5 秒,观察日志变化。
  2. 在 client/main.lua 中打印 RunService:IsClient() 的结果。
  3. 思考:为什么不应在客户端直接写入玩家分数?

本专题验收标准 ​

  • [ ] 我理解 server 是权威方,client 负责表现。
  • [ ] 我知道 Heartbeat 每帧触发,需要节流。
  • [ ] 我能用 World:GetServerTime() 做倒计时。
  • [ ] 我能用 RunService:IsServer() 区分运行端。

本专题产物 ​

本专题产物是服务端权威倒计时。完成小游戏后选择扩展挑战 server/round_manager.lua 中的 updateRound() 用 World:GetServerTime() 判断回合结束,client 只显示服务端下发的结束时间。

本专题 API 对照 ​

把结果带回小游戏 ​

先确认本页“本次要看到”的现象,再把选中的功能接到已有模块。保留原有入口和清理逻辑,只迁入需要的部分;不要把多个试验入口拼在一起。

返回主线对应步骤,或去专题导航选择下一项能力。新的代码尚未完成目标地图实测时,记录为待验证,不把编译通过当作行为通过。