主题
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。
