Skip to content

09 让游戏更有反馈 ​

把参数放到配置文件,在范围内随机选择目标位置,并加入不阻断计分的音效和可选特效。

现在给每次命中加反馈:重新随机选择方块位置,并为得分者播放声音;若你在编辑器预配置了 HitEffect,还会看到短暂特效。随机位置允许与当前位置相同。分数、复位和重开不能依赖这些表现资源成功。

开始前检查 ​

第 08 课计分、HUD 和按钮都正常。基础场地要容纳方块左右移动,范围见场景搭建指南。特效可以最后做:先完成配置和随机刷新。

1. 安装带配置的检查点 ​

文件:新增 data/game_config.lua,替换 server/main.lua、client/main.lua、common/remote_events.lua。操作:下载第 09 课检查点,按同路径整文件替换或新建,全部保存同步。

预期:在 VS Code 文件树中可以看到如下目录结构:

text
你的工程目录/
├─ data/
│  └─ game_config.lua      <- [新增] 双端共用的数值配置
├─ common/
│  └─ remote_events.lua    <- 两端通信协议
├─ client/
│  └─ main.lua
└─ server/
    └─ main.lua

原有玩法仍能启动。默认每局 20 秒、每次 1 分;命中后为方块随机选择相对起点的 Z 轴偏移,可能留在原位。

2. 在一处调整参数 ​

文件:data/game_config.lua。操作:先确认完整内容,暂时不改资源编号。

lua
-- @runtime client
-- ============================================================================
-- 模块职责:全工程玩法数值与音效资产配置(双端共用配置表)
-- 说明:client 与 server 均可 require 加载;首行标记仅用于静态类型检查。
-- ============================================================================
return {
    RoundSeconds = 20,                  -- 单局对局时长(秒)
    HitCooldown = 1,                    -- 碰撞冷却时间(秒)
    HitPoints = 1,                      -- 每次有效命中得分
    CubeRandomZRange = 3,               -- 命中后方块在 Z 轴方向的随机偏移半宽
    HitEffectName = "HitEffect",        -- 场景中击中特效实体(EffectUnit)的节点名
    HitEffectSeconds = 0.6,             -- 特效单次播放持续时间(秒)
    HitSoundUri = "official://audio/10772", -- 击中音效官方资产 URI
}

data/ 只返回数据,不在加载时获取服务或绑定事件。两端通过 require("data.game_config") 读取同一份配置;真正的积分仍由服务端计算。

保持时长为正数、防抖间隔不小于 0、分值为正整数。随机范围使用不小于 0 的整数,并留在地面范围内;不要把完整资源 URI 换成裸数字。

3. 先验证随机位置,再加特效 ​

文件:server/main.lua 已随包更新,本步不改 Lua。操作:先不创建 HitEffect,连续命中三次。

预期:计分、复位和重开正常,方块的 Z 坐标在初始位置左右 3 的范围内。偏移可能抽到 0,也可能重复上一次结果,所以回到起点或留在原位都正常;验收看坐标是否在范围内,不要求每次肉眼可见地移动。

然后停止试玩,按搭建指南配置一个名为 HitEffect 的 EffectUnit,初始隐藏,资源在编辑器中选择并预览。保存地图后重新试玩,观察命中处是否出现短暂特效。不要在代码中添加隐藏的资源选择字段。

4. 理解为什么旧定时器不会关掉新特效 ​

文件:server/main.lua 的 feedback 区域。操作:阅读解释片段。

lua
-- [视觉特效播放] 在命中位置显示光效,0.6 秒后自动隐藏
local function playHitEffect(position)
    -- 表现层防护:使用 pcall 包裹,确保即使特效对象缺失也不破坏计分主流程
    local ok, err = pcall(function()
        local effect = World:FindFirstChild(Config.HitEffectName, true)
        if not effect or not effect:IsA("EffectUnit") then return end

        effectGeneration = effectGeneration + 1
        local thisGeneration = effectGeneration

        -- 将特效实体移动到命中位置并点亮
        effect:SetVisible(false)
        effect:SetPosition(position)
        effect:SetVisible(true)

        -- 延迟指定时长后自动熄灭;若中途发生了新命中则由新代次接管
        Task:Delay(Config.HitEffectSeconds, function()
            if not alive or effectGeneration ~= thisGeneration then return end
            if World:FindFirstChild(Config.HitEffectName, true) ~= effect then return end
            effect:SetVisible(false)
        end)
    end)
    if not ok then print("[推球][WARN] 特效不可用,玩法继续:", err) end
end

-- [方块随机位移] 命中后在 Z 轴范围随机移动,提供推球新目标
local function moveCube()
    local offset = math.random(-Config.CubeRandomZRange, Config.CubeRandomZRange)
    cube.Position = Vector3(cubeStartPosition.x, cubeStartPosition.y, cubeStartPosition.z + offset)
end

每次表现增加一次 effectGeneration;延时回调只负责关闭自己那一代特效。新命中开始后,旧回调看到代次不同就退出。它还重新确认场景中仍是原来的对象,避免访问已被替换的特效。

调用表现前已完成服务器计分、快照发送和球复位;可选资源找不到就跳过,表现调用失败只记 WARN。因此不能用“能听见声音”代替“积分已正确”的验证。

5. 核对声音属于谁 ​

文件:client/main.lua、data/game_config.lua。操作:先用默认配置得分一次。

预期:只有客户端收到同一局内比上一份快照更高的个人积分时,才调用 PlayLocalSound。首次同步已有分数或新局清零不会补播命中声。

示例 URI official://audio/10772 来自已有音效专题。没有声音时先检查游戏音量及目标运行包资源;可以在配置中把 HitSoundUri 设为 "" 关闭音效,计分和 HUD 仍应正确。资源实际可听效果以自己的试玩核对。

自己改一处 ​

文件:data/game_config.lua。操作:只把 HitPoints = 1 改成 HitPoints = 2,重新同步并试玩。

预测:同一玩家两次命中后 HUD 显示多少?答案:4 分;声音仍每次有效得分播放一次,方块的随机范围不变。恢复分值 1,再把 HitEffectName 临时设成不存在的名字,确认缺特效仍可玩,最后恢复 HitEffect。

保存进度 ​

下载第 09 课完整代码。下一课:整理代码并交付小游戏。

API:SoundService、EffectUnit、Task、WorldUnit。深读:声音与特效、配置与资产 URI。