主题
08 加上再来一局按钮
把自动下一局改为结算后点击重开,服务端只接受一次有效请求。
这一课明确改变规则:结算后停在等待状态,任一玩家点击“再来一局”,就为当前服务器的所有玩家开启新局。进行中不能提前重开,连点也不能多开几局。
开始前检查
第 07 课 HUD 正常。按UI 搭建指南增加 button_restart,类型为 EUIButton,文字为“再来一局”,允许触摸,放在不遮挡推球操作的位置。
1. 先看见静态按钮
文件:暂不改 Lua。操作:保存 UI,沿用第 07 课代码试玩。
预期:能看到按钮和文字;它此时还没有玩法行为。停止试玩后再进行下一步。图案显示正常不等于点击事件已经绑定。

2. 同时更新服务端和客户端
文件:server/main.lua、client/main.lua、common/remote_events.lua。操作:下载第 08 课检查点,按同路径整文件替换三个文件,保存同步后试玩。
这次两端都要更新:服务器取消自动下一局,客户端增加点击请求。只改客户端会看到旧规则继续自动开局;只改服务端则无人能发送重开请求。
预期:游戏进行中按钮隐藏;结算后出现按钮,时间标签变成“本局结束,点击再来一局”。
3. 请求不是命令
文件:server/main.lua 的 request 区域。操作:阅读解释片段,重点找到两个拒绝条件。
lua
-- [客户端请求处理] 响应 ready(补全快照)与 restart(请求重开)
local function onRequest(player, payload)
if not alive or not player or not player:IsA("Player") then return end
if type(payload) ~= "table" or payload.version ~= 1 then return end
if payload.kind ~= "ready" and payload.kind ~= "restart" then return end
-- 限频保护:同一玩家 0.5 秒内只允许一次有效请求
local now = World:GetServerTime()
local requestKey = player.UserId .. ":" .. payload.kind
if now - (lastRequestTime[requestKey] or -math.huge) < 0.5 then return end
lastRequestTime[requestKey] = now
if payload.kind == "restart" then
-- 核心防御:只有在 waiting 阶段且局号一致时才准许开局(不信任客户端分数,直接由服务端权威开局)
if phase ~= "waiting" or payload.roundId ~= roundId then return end
print("[推球][重开] 请求来自:", player:GetName())
startRound()
else
syncPlayer(player)
end
endplayer 由引擎告诉服务器,不由客户端填写。客户端请求只说“我想重开第几局”,不携带分数或要操作的玩家。
服务器只在 phase == "waiting" 且请求局号与当前 roundId 相同时接受。开始一局会立即切换成 running 并增加局号,中途不等待;同一批到达的后续请求因此不能再次开启一局。按钮显隐是体验,真正的规则检查必须保留在服务器。
客户端还限制半秒内不连续发请求;服务端独立限频,不能因为客户端做过检查就省略。
4. 核对按钮生命周期
文件:client/main.lua。操作:在完整文件中找到 bindRestart,阅读它怎样比较新旧按钮引用,暂不改动。
预期:找到同一个按钮时不重复绑定;节点被替换时先断开旧连接,再绑定新节点。每次点击只发一个请求。UI 缺失时仍会继续查找,恢复节点后无需再追加监听。
本地玩家离开时,客户端统一断开按钮、消息和 Heartbeat 连接;destroy() 可以重复调用。主动卸载脚本时也需要显式调用它,不能把文件末尾的 return 理解为自动清理。第 10 课会把这套清理收进 Hud 模块。
运行并核对
文件:无需修改。操作:等到结算后连点按钮,再完成一次新局。
预期:全场只进入下一局一次,分数清零;运行中按钮隐藏,服务器也拒绝进行中的重开请求。若有两名联机玩家,在同一结算阶段同时点击,应共同进入同一新局。
按钮一直不出现:先确认已结算,再核对名字和 EUIButton 类型。能看见却点不了:检查 TouchEnabled、遮挡控件和当前代码是否已同步。点击只会影响本次测试服务器,不应设计成由某个客户端自行清零。
自己改一处
文件:编辑器 UI 的 button_restart;不改 Lua。操作:把按钮文字改成“大家再玩一局”,保存 UI 并试玩。
预测:代码还找得到按钮吗?提示:文字与 Name 是不同属性。答案:只改显示文字不影响查找,改 Name 才会导致节点缺失。恢复文字为“再来一局”,保留 Name 不变。
保存进度
下载第 08 课完整代码。下一课:让游戏更有反馈。
API:RemoteEvent、EUIButton、EUINodeBase、Connection。深读:双端通信。
