【游戏功能】
上周有个开服的老板在群里发了一段录屏:他的战士明明刀刀都砍中了,观众却一片"这刀挥空了吧"。他原话是"判定日志全在打脸,眼睛却说没打中,我信哪个"。我看完只回了一句:你信眼睛——因为玩家也只信眼睛。这不是判定的问题,是命中表现慢了一拍。今天把命中表现这一层扒开讲透,看完你自己就能对拍。
一、先分清两件事:命中判定是逻辑层,算的是坐标和距离,服务端说了算;命中表现是表现层,管的是刀光、火星、数字什么时候出现。整条链路是:判定通过 → skillManager 分发 → skillPresentHit 接单 → 按配置延迟挂特效。
flowchart LR
A[判定通过] --> B[skillManager 分发]
B --> C[skillPresentHit 接单]
C --> D{读命中表现配置}
D --> E[hitDelay 延迟挂特效]
D --> F[hitStop 命中停顿]
D --> G[偏移表 调火星落点]
E --> H[玩家眼睛验账]
命中表现这层的代码在 skill/skillPresentHit.lua,命门的字段就两个:
-- skill/skillPresentHit.lua · 两个命门字段
local M = {}
local CONST = { HIT_DELAY_DEF = 60, HIT_STOP = 0.08 }
local _delay = CONST.HIT_DELAY_DEF
local _stopOn = false
local function ShowTip(msg) if msg and msg ~= "" then SL:ShowSystemTips(msg) end end
function M.Config(cfg)
_delay = cfg and cfg.hitDelay or CONST.HIT_DELAY_DEF
_stopOn = cfg and cfg.hitStop or false
end
function M.OnHit(x, y, dmg)
SL:ScheduleOnce(function()
if _stopOn then M.Freeze(0.08) end
ShowTip("命中表现落账:" .. dmg)
end, _delay * 0.001)
end
function M.Freeze(sec) _stopT = sec end
return M
hitDelay:命中特效延迟,毫秒,默认 60。这个数是刀光落到火星爆开之间的空拍。60 毫秒什么概念?人眨一次眼 150 毫秒——也就是四成次眨眼的功夫,火星就该到位。它存在的理由是补"接触感":刀光扫过到怪物受击之间留一线,打击才有分量。但这条线一拉长就变味:拖到 110 毫秒以上,怪在走、火星在旧位,眼睛先看到怪过了、后看到火星才炸——判定明明中了,观感就是挥空。你拖到 180 毫秒试试,那一刀简直像砍在回忆里。
hitStop:命中停顿,秒,默认 0.08。命中那一瞬把怪的动作冻住一眨眼,打击感立刻上一个档次——格斗游戏管这叫顿帧。它和 hitDelay 是两个方向的调节:延迟是"晚一点演",停顿是"演的时候掐一下"。两个都用对,一刀顶三刀;用反了,怪僵在半空火星才来,玩家以为死机了。
下面这个演示,怪在场中来回巡逻。你把延迟滑块从 0 拖到 180,每一档挥一刀,盯住火星炸开的位置和怪当时的位置——
fx-hitlag
二、实战案例:三步改造
去年带学员救"狂刀"这个技能:策划要求"打击感加重",他上来就把 hitStop 拉到 0.3 秒,结果每砍一刀怪都僵半拍,三刀下来节奏全断,玩家以为在放 PPT。三步改回来:第一步,hitStop 收回 0.08,只在暴击时给 0.12,把"重"留给关键刀;第二步,hitDelay 从他随手填的 200 收回 60,火星贴着刀光走;第三步,给第三刀单独配了偏移表,火星挂到怪的头线,三连击一刀比一刀高。改完群里试刀的反馈是:这刀有骨头了。
反面教材:有个服做"影子刺客", hitDelay 填了 500——他们想要"刀过了半秒才见血"的暗杀味。上线当天骂声一片:刺客打完都走了,火星才炸,组队的队友以为中了陷阱。表现层的延迟是调味料,一撮提鲜,一勺齁死。你想做暗杀味,去调特效的透明度渐入,别动命中表现的时序表——那张表管的是所有玩家的眼睛。
再补一句给调参人的话:改 hitDelay 和 hitStop 的时候,嘴里念着两个数——六十毫秒是火星的空拍,八十毫秒是停顿的分量。这两句念顺了,策划再说"打击感不对",你手里就有数可调,而不是对着"再爽一点"四个字干瞪眼。
三、反面教材:两个数都拧反了会怎样
满屏国战的时候,命中表现是同屏开销大户:一刀一个火星、一刀一个数字,一百人混战就是一秒几百个表现请求。skillPresentHit 的节流做法值得抄:同屏火星请求先进缓存队列,Tick 统一出队,出队的循环用数字下标 for 而不是 pairs,数组走线性遍历比哈希迭代快一截;超量的时候按"离屏幕中心近的优先"丢边缘表现——边缘那一刀的火星,玩家根本看不清,省了不心疼。
四、满屏同屏扛得住吗
调试技巧:把命中表现的时序掏出来量。在 OnHit 的回调前后各打一个时间戳,差值就是真实延迟;再跟配置里的 hitDelay 一比,对不上就是中途有人改了表没走配置。顺便看一眼 hitStop 的冻结是不是真的冻住了怪的动画状态机——有些实现只冻了计时器没冻动画,怪僵着腿还在跑,比不冻还假。
五、调试技巧:掏出来看
常见疑问三连:hitDelay 会不会影响判定?不会——判定早于表现,符纸慢了伤照掉,火星晚了血照扣,两张表两个衙门。停顿会不会冻住服务端?不会——hitStop 只冻客户端动画,服务端的怪该干嘛干嘛。这两个数该谁调?策划提需求,你来换算成毫秒——"更重一点"翻译过来就是停顿加 20、延迟减 20。
-- 调试:量命中表现的真实时序
SL:ScheduleOnce(function()
SL:BindDebugButton("量延迟", function()
local t0 = os.clock()
skillPresentHit.OnHit(0, 0, 14)
SL:ScheduleOnce(function()
local cost = (os.clock() - t0) * 1000
SL:ShowSystemTips("实际延迟 " .. string.format("%.1f", cost) .. "ms")
end, 0.3)
end)
end, 1.0)
undefined
六、常见疑问
问:命中判定和命中表现是一个东西吗?
答:不是——判定在逻辑层早就算完了,表现只是"演"给你看。
问:延迟拖到多少算过分?
答:110 毫秒是观感悬崖——怪在走火星在旧位,眼睛先喊落空。
问:停顿冻的是谁?
答:客户端的动画状态机——服务端的怪照常跑,别担心。
问:暴击要不要单独配?
答:要——停顿 0.12、火星大一圈,把"重"留给关键刀。
写完留一句给做表现层的同学:hitDelay 管空拍、hitStop 管分量,两个数对拍,玩家才信自己的眼睛。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
评审二开代码的时候,我看到最多的坏味道不是写错,是"穿墙":怪物死亡的函数里直接调 UI 刷新,UI 按钮回调里直接改怪物状…
评审二开代码的时候,我看到最多的坏味道不是写错,是"穿墙":怪物死亡的函数里直接调 UI 刷新,UI 按钮回调里直接改怪物状…
评审二开代码的时候,我看到最多的坏味道不是写错,是"穿墙":怪物死亡的函数里直接调 UI 刷新,UI 按钮回调里直接改怪物状…
评审二开代码的时候,我看到最多的坏味道不是写错,是"穿墙":怪物死亡的函数里直接调 UI 刷新,UI 按钮回调里直接改怪物状…
做排行榜、背包、邮件列表的兄弟,迟早会遇到同一张工单:"列表一打开掉帧,滑动像幻灯片"。99% 的原因是把一千行数据老老实实…
新接手 996 引擎客户端的人,打开 network/networkUtil.lua 看到满屏的 i1 、 I4 、 C32…