Skip to content

05 把分数记给正确玩家 ​

在服务端记录最后触碰球的玩家,每次命中消费一次归属,并处理离开和新局。

从这一课起,玩法改为个人积分:球最后被谁接触,下一次命中就记给谁。人物直接撞方块不加分,没有有效触球者的球撞方块也不加分。

开始前检查 ​

从第 04 课继续,自动开局和结算都正常。场景保持不变。单人试玩可以验证自己的得分、无归属碰撞和新局清零;两名玩家之间的归属必须在真正的双客户端联机环境验证,单人日志不能证明这一点。

1. 换上个人积分版本 ​

文件:server/main.lua、client/main.lua。操作:下载第 05 课检查点,停止试玩,整文件替换两端入口并保存同步。

预期:开局时不再使用第 03 课的“全场命中数”。让人物接触球,服务端应出现 [推球][触球] 最后触碰者:,再推球命中方块,会出现 [推球][得分]、玩家名字和个人分数。

2. 区分账号与场景角色 ​

文件:server/main.lua 的 owner 区域。操作:只阅读下面的解释片段,不在文件末尾追加。

lua
-- [触球监听] 记录是谁最后碰到了球体
local function onBallTouched(otherUnit)
    -- 仅在对局进行中记录归属
    if phase ~= "running" or World:GetServerTime() >= endTime then return end
    -- 确认撞击球体的是否为玩家角色实体(EggyUnit)
    if not otherUnit or not otherUnit:IsA("EggyUnit") then return end

    -- 通过角色实体安全反查关联的玩家对象(Player)
    local player = Players:GetPlayerFromCharacter(otherUnit)
    if player then
        lastOwnerUID = player.UserId
        print("[推球][触球] 最后触碰者:", player:GetName())
    end
end

玩家身份与角色的权威反查 ​

在服务端架构中,必须严格区分“玩家身份”与“场景实体”:

text
物理碰撞事件触发 (otherUnit)
  │
  ├─ 1. 类型判定:otherUnit:IsA("EggyUnit")
  │     (确认碰球的是真实蛋仔角色,而不是地面、方块或其他障碍)
  │
  └─ 2. 身份反查:Players:GetPlayerFromCharacter(otherUnit)
        │
        └─ 取得 Player 对象 ──> 提取 player.UserId
                                记入 lastOwnerUID
  • Player:代表玩家的网络会话与账号身份(拥有 UserId 和 GetName())。
  • EggyUnit:代表场景中实际走动的物理角色模型。
  • Players:GetPlayerFromCharacter(otherUnit):引擎公开的权威反查接口,参数契约要求传入 EggyUnit,因此必须先用 IsA 判定再调用。代码不猜“碰到某个角色子对象就一定算触碰了玩家”;不要删除类型检查,也不要用内部编号绕过归属反查。

3. 让一次归属只使用一次 ​

文件:server/main.lua 的 hit 区域。操作:阅读本课对上一课碰撞函数的修改。

lua
-- [命中方块监听] 将命中得分归属给最后触球者
local function onCubeTouched(otherUnit)
    if otherUnit ~= ball or phase ~= "running" then return end
    local now = World:GetServerTime()
    if now >= endTime or now < nextHitTime then return end
    nextHitTime = now + hitCooldown

    -- 关键设计:先消费归属权并置空,防止一次触碰被后续微小反弹反复刷分
    local uid = lastOwnerUID
    lastOwnerUID = nil

    -- 检查触碰者是否仍然在线有效
    local player = uid and Players:GetPlayerByUserId(uid)
    if not player then
        print("[推球][未计分] 没有有效触球者,请重新推球")
        resetBall()
        return
    end

    -- 为该玩家累加 1 分
    scores[uid] = (scores[uid] or 0) + 1
    print("[推球][得分]", player:GetName(), scores[uid])
    resetBall()
end

scores 是 table,在本例中像一张“玩家编号→积分”的表。scores[uid] or 0 表示这位玩家还没有记录时先按 0 处理。

命中时先取出 lastOwnerUID,再立刻设为 nil;这次触球就被消费掉了。玩家已经离开或归属不存在时,球仍复位,日志说明“未计分”,让你可以重新推球恢复玩法。

运行并核对 ​

文件:无需修改。操作:正常推球命中两次,等待结算和下一局;如果有两名联机玩家,再让 A 触球后 B 触球,最后命中方块。

预期:自己的分数依次增加;新局分数从 0 开始。双人场景下,最后接触者 B 获得这次积分。让最后触碰者离开后,再发生的无归属碰撞不能给旧玩家加分。

脚本会在命中、结算、新局和该玩家离开时清除归属。下一局不能靠上一局留下的编号得分。

自己改一处 ​

文件:server/main.lua。操作:只把 scores[uid] = (scores[uid] or 0) + 1 末尾的 1 改成 2。

预测:新局正常命中两次后会显示多少分?提示:每个人有自己的表项。答案:同一玩家两次为 4 分,其他玩家的分数不变。随后改回 1,重新试玩。

保存进度 ​

第 05 课完整代码是个人积分版本的恢复点。下一课:把服务端结果告诉客户端。

API:Players、Player、EggyUnit、WorldUnit。想继续处理角色重生、阵营和输入,阅读玩家与输入专题。