【游戏功能】
一、一次对不上的攻防
封测时有段录像流传很广:战士对着怪一刀挥空,怪原地晃了一下;两秒后战士收刀站定,那头怪突然仰面倒地。没有伤害数字对齐,攻与受各播各的。排查发现两套表现各自挂了各自的定时器:攻击动画放完才发伤害消息,受击动画收到消息才开始播——总延迟一叠,视觉上就完全错位。攻防是一来一回的对手戏,两边的节奏必须在同一处对表。
二、机制原理:关键帧对齐
正解是关键帧对齐:攻击动作里指定某一帧为"命中帧"——刀刃落下的那一帧,在这一帧同步做三件事,发出伤害判定、让目标播受击、打出飘字。受击方不再有自己的等待,收到即播。受击是短动作各方向两帧,死亡是长动作各方向十帧,播完停在最后一帧留尸。这样一刀下去,刀落、怪仰、数字跳,三件事在同一帧发生,玩家感知的就是"结结实实砍中了"。
mir8-combat
三、正确写法:命中帧驱动的攻防链
function gameActor:PlayAttack(targetID)
local atkFrames = self:GetFrameCount(global.MMO.ANIM_ATTACK, self._dir)
self._hitFrame = math.floor(atkFrames * 0.55) -- 命中帧取动作的55%处
self._hitTarget = targetID
self:PlayAction(global.MMO.ANIM_ATTACK)
end
function gameActor:OnFrameAdvance(frameIndex)
if self._act ~= global.MMO.ANIM_ATTACK then return end
if frameIndex == self._hitFrame then -- 刀落帧: 三件事同帧发生
global.Facade:sendNotification(global.NoticeTable.ActorAttackHit, {
attacker = self:GetID(),
target = self._hitTarget
})
self._hitTarget = nil
end
end
-- 受击方: 收到即播, 死亡播完留尸
function gameActor:OnAttackHit(data)
if data.target ~= self:GetID() then return end
self._hp = self._hp - data.damage
if self._hp <= 0 then
self._dieKeepLast = true -- 死亡停在末帧
self:PlayAction(global.MMO.ANIM_DIE)
self:OnActorDead()
else
self:PlayAction(global.MMO.ANIM_STUCK) -- 受击: 短促两帧
end
end
四、四个必须钉死的细节
五、心法:找到每个动作的那一帧
攻防对齐的思路可以推广到所有"我与目标"的交互:施法有出手帧、捡物有弯腰帧、上马有抬腿帧。每个动作里找出真正发生交互的那一帧,把逻辑事件钉在那一帧上,前后帧都只是情绪铺垫。找不到这一帧的动作,就不该带逻辑——纯观赏动画和逻辑动画分层,是表现系统的第一刀切分。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 元方法 __close 的错误传递就这两行: to-be-closed 变量离开作用域时自动调用 __clos…
【语法算法】 goto 向后跳的控制就这一行: ::retry:: 定义标签, goto retry 跳回标签——向后跳的全…
【语法算法】 元方法 __close 的多资源清理就这两行: 两个 to-be-closed 变量——声明的逆序清理——先关…
【语法算法】 随机种子的确定性本质就这一行: 固定种子意味着随机数序列完全确定——同样的种子产生同样的序列——随机种子的全部…
【语法算法】 协程的状态机就这一行: status 返回四种字符串之一:"suspended"、"running"、"nor…
【语法算法】 xpcall 的错误对象就这两行: xpcall 和 pcall 的本质区别就是第二个参数——错误处理函数——…