Skip to content

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 再完成三次命中。

保存进度 ​

API:WorldUnit、BasePart、Vector3、World。深读:对象和坐标。