主题
03 每次命中只算一次
加入全场命中数、防抖和复位,让推球原型能够连续玩三次。
完成这课,每次有效命中后,日志中的全场命中数加 1,球回到起点并停下。此时还没有个人积分:所有玩家共同增加同一个数。
开始前检查
沿用第 02 课的球与方块,确认已能出现命中日志。保持搭建指南中的初始距离,球回到起点时不能与方块重叠。这课不新增对象或 UI。
1. 加入计数和复位
文件:server/main.lua。操作:整文件替换为下列完成状态。client/main.lua 不增加玩法逻辑,可继续用前一课文件。
lua
-- @runtime server
-- ============================================================================
-- 模块职责:负责推球命中计数、防抖冷却保护与球体复位
-- 核心目标:
-- 1. 记录全场总命中次数(hits),在多次碰撞间持久累加
-- 2. 通过 GetServerTime 时间戳实现 1 秒命中冷却防抖,避免刚体连续抖动刷分
-- 3. 命中后同时重置球体的 CFrame 位置与线速度/角速度,防止残留旧动量
-- ============================================================================
-- 1. 获取场景根节点服务 World
local World = game:GetService("World")
-- 2. 检索场景中的关键物体
local ball = World:FindFirstChild("球体")
local cube = World:FindFirstChild("方块")
if not ball or not ball:IsA("WorldUnit") then
print("[推球][ERROR] World 下需要名为「球体」的 WorldUnit")
return
end
if not cube or not cube:IsA("WorldUnit") then
print("[推球][ERROR] World 下需要名为「方块」的 WorldUnit")
return
end
-- 3. 玩法核心状态变量
local hits = 0 -- 全场累计命中数
local hitCooldown = 1 -- 碰撞防抖冷却时间(秒)
local nextHitTime = 0 -- 下一次允许判定的最早服务端时间戳
local startCFrame = ball.CFrame -- 记住球体启动时的初始位置与朝向
-- 4. 球体复位函数:将球体挪回起点,并清空所有物理速度
local function resetBall()
ball.CFrame = startCFrame
ball.LinearVelocity = Vector3(0, 0, 0) -- 清空线性移动速度
ball.AngularVelocity = Vector3(0, 0, 0) -- 清空自转角速度
end
-- 5. 监听方块碰撞事件
local collisionConnection = cube.OnCollisionEnter:Connect(function(otherUnit)
-- 过滤 1:只有球体撞击方块才处理
if otherUnit ~= ball then return end
-- 过滤 2:防抖冷却检查,未到允许判定时间直接忽略
local now = World:GetServerTime()
if now < nextHitTime then return end
-- 命中生效:推进下一次冷却时间戳
nextHitTime = now + hitCooldown
hits = hits + 1
print("[推球][命中] 全场命中数:", hits)
-- 每次命中后将球体重置回起点,准备下一次推球
resetBall()
end)
print("[推球][就绪] 全场命中数: 0")
-- 6. 优雅退出:注销连接
game:BindToClose(function()
collisionConnection:Disconnect()
end)预期:重新试玩后先显示 全场命中数: 0。把球推向方块,应显示 1,球回到起点;再次推球,应该显示 2。第三次是 3。
2. 分开观察“位置”与“速度”
文件:无需修改。操作:完成一次较用力的推球,观察命中后球的位置,以及它是否还带着原来的滚动速度。
预期:ball.CFrame = startCFrame 恢复起始位置和朝向;两个 Vector3(0, 0, 0) 分别清掉移动速度和旋转速度。只搬位置而不清速度,球可能刚回去又滚走。若复位后再次被蛋仔推到,属于新的物理接触,先让角色离开起点再观察。
刚才增加了什么
1. 命中与防抖判定时序
每次物理碰撞触发时,服务端的处理逻辑如下:
text
碰撞发生 (otherUnit)
│
├─ 是不是那颗球? (otherUnit == ball) ──[否]──> 忽略 (可能是人物碰方块)
│
├─ 冷却过了吗? (now >= nextHitTime) ──[否]──> 忽略 (物理抖动防刷分)
│
└─ [是] 记录当前时间 + 冷却时间 (nextHitTime = now + 1)
│
├─ 累加命中数 (hits = hits + 1)
└─ 复位球体位置与动量 (resetBall)2. 核心变量的作用
| 变量 / 函数 | 为什么这样设计 | 常见错误 |
|---|---|---|
| hits | 声明在回调外部(模块作用域),碰撞之间持续保留 | 若写在回调内部,每次碰撞都会重新变成 0 |
| hitCooldown | 设置 1 秒冷却,过滤刚体连续抖动产生的多次微碰撞 | 缺少防抖会导致球贴着方块滚一次就瞬间加几十分 |
| startCFrame | 试玩启动时记住球的初始位置与朝向 | 不要写死硬编码坐标,读取启动位置更便于在编辑器随时调整球位 |
| resetBall() | 同时重置 CFrame、LinearVelocity 和 AngularVelocity | 只重置位置不清空速度,球体会瞬间带着旧动量飞出去 |
自己改一处
文件:server/main.lua。操作:只把 hitCooldown = 1 改成 hitCooldown = 3,预测两秒内再次推球命中是否计数。
提示:比较碰撞时间和 nextHitTime。答案:不到 3 秒的碰撞会被忽略,包括计数和复位;等超过 3 秒后,让球与方块先分开再碰到,才产生新的进入事件。不要让球持续贴着方块等日志,因为 OnCollisionEnter 监听的是开始接触。
恢复:把间隔改回 1,停止后重新试玩,从 0 再完成三次命中。
保存进度
- 下载第 03 课完整代码。
- 通过标准:命中数依次为 1、2、3;每次复位并清速度;场景空闲时不会自己刷分。
- 下一课:做出完整的一局。
