ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

动作系统设计:从状态机到输入缓冲与命中判定的实践

动作系统设计:从状态机到输入缓冲与命中判定的实践 上面这个东西当时做的时候心里其实没底。动作系统不是单纯堆一堆动画切片让角色播片真正麻烦的是逻辑。如果只是跑一个Demo用状态机硬编码也没问题做到十几个动作就要开始裂开做到角色、敌人、场景交互混在一起的时候硬编码基本就是灾难。这篇东西就围绕动作逻辑与机制讲讲我在一个3D动作场景交互实验项目里的思路、踩坑和最终落地的取舍。1. 动作逻辑的骨架为什么状态机总是死路一条1.1 大多数人印象里的状态机长什么样做动作系统新手最容易想到的就是状态机。一个角色有Idle、Walk、Run、Jump、Attack每个状态根据输入跳转画一张大流程图好像就能把动作管起来。实际做起来你会发现这套思路撑不住。原因不复杂动作状态机里的状态如果颗粒度太粗一个Attack状态内部其实还分成前摇、命中帧、后摇、收招等多个阶段每个阶段的可打断性完全不同如果颗粒度太细状态数量爆炸状态之间的连线比蜘蛛网还密。我在项目里一度把状态列表拉到了四十多个连线画完自己都看不懂。状态本身不是问题问题是状态承载的职责太多了。它既要在管当前播放哪个动画又要在管能不能被其他动作打断还要在管移动和转向是否允许甚至还要在管攻击判定什么时候生效。这些职责绑死在状态枚举里改一个需求就得动一堆条件判断。1.2 几个反直觉的坑第一状态机里的跳转条件看起来是代码其实是全局耦合。比如Jump可以打断Attack这个规则写在Attack状态里还是Jump状态里都有程序员这么干过但一旦规则多了同一个打断规则被复制到十几个状态改一次就要翻遍整个文件。第二状态这个概念天然是离散的但动作手感是连续的。比如从Run突然切到Attack动画过渡做不好角色就像瞬移一样切换姿势。为了让过渡不突兀你还需要一层动画融合逻辑可它跟状态跳转逻辑是两码事。很多人的状态机里混着动画Transition和逻辑Flag调试的时候两个维度来回切非常痛苦。第三状态机回答的是现在在干什么但动作系统真正要回答的是接下来能干什么、不能干什么。这是两套问题。前者是状态描述后者是规则约束。我在项目里把这两件事拆开了之后动作逻辑瞬间清晰很多。1.3 我最终采用的分层结构做完复盘我把动作逻辑拆成了四层动作声明层角色有哪些动作每个动作的基础属性名称、时长、动画资源、移动配置。打断权管理层每个动作对外的可打断规则不依赖具体状态。状态求解层根据当前动作、输入、物理条件算出下一个动作应该是什么。动画表现层拿到目标动作ID后去处理AnimationClip的切换、融合、速度和播放位置。这个结构里状态机从老板变成了翻译官。状态机只管状态切换打断规则交给一个独立的优先级表格动画表现完全由动画层自行处理。改一个动作的打断规则不需要动状态机代码改动画融合参数也不会影响逻辑判断。这样才算把动作逻辑和动作表现分开。2. 手感的分水岭输入缓冲与触发窗口的数值设计动作逻辑里最影响手感的不是状态机结构而是输入处理。2.1 从按下按键到角色出招中间发生了什么玩家按下一个攻击键你不能立刻让角色出招。原因很简单如果角色正在做上一个动作比如正在跑动或者正在收招后摇里直接切到新动作会显得非常生硬。我项目里的处理链是按下按键 - 输入被记录并打上按下时间戳 - 输入管理器把这个意图广播出去 - 动作系统在合适的窗口内检查这个输入 - 如果当前动作允许打断并且在输入有效窗口内则触发新动作。这里最关键的概念是输入缓冲Input Buffer和触发窗口Trigger Window。输入缓冲指按键按下去之后这个指令能保留多长时间触发窗口指在某个动作播放的哪个时间段内允许接受并响应新的输入指令。2.2 为什么这两个窗口的数值直接决定手感窗口太短玩家感觉按键不灵明明按了却没反应尤其在连招衔接、闪避接攻击这类需要卡时间点的操作里特别明显窗口太长角色会吞招玩家已经按了下一个技能结果角色把当前动作做完才执行看起来像慢半拍。通常来讲输入缓冲建议设置在100到150毫秒区间触发窗口要根据具体动作设计攻击命中的那一帧前后最容易接后续招式因为这时候玩家注意力最集中。我在实验项目里把攻击命中后继续输入下一招的触发窗口设在80到120毫秒正好覆盖玩家在命中反馈后的下意识输入区间。有一点要单独提醒游戏里如果跑在60帧一帧约16.7毫秒窗口设置低于30毫秒的话玩家在掉帧到30帧时基本就无法触发连招了。做动作系统要留出帧率容差尤其考虑低端设备和复杂场景下的性能损耗。2.3 实测中的数据参考我在项目里维护了一张输入窗口参数表每个动作独立配置核心参数如下动作预输入缓冲可取消起始帧有效输入结束帧备注轻攻击A120ms动画播放到20%命中帧之后80ms连招接续窗口重攻击B120ms动画播放到35%动画播放到80%可被闪避取消闪避100ms前摇第1帧前摇结束前全程可被攻击预输入跳跃80ms后摇前10帧落地前落地缓冲这些数值不是拍脑袋写的是我在项目里用慢动作重放逐帧调出来的。核心思路先定希望玩家在什么节奏下完成操作再把节奏换算成帧数最后配合输入缓冲做微调。3. 动作优先级与打断规则谁能管谁怎么管状态机里的跳转条件多了以后你会发现大量规则都在回答同一个问题A动作进行到一半时B动作能不能插进来这个问题实际上和当前状态无关只和A动作的硬直级别以及B动作的优先级有关。3.1 优先级表的搭建我借鉴了格斗游戏的做法把所有动作按硬直等级分成八级。等级越高越难被打断也越容易打断别人。优先级动作类别示例可被谁打断8终极技能大招动画仅死亡7处决/演出动画终结技、过场动作仅死亡6重攻击第二段蓄力重击7/8等级动作5重攻击第一段普通重击6/7/8等级动作4受击硬直被击中反应5/6/7/8等级动作3轻攻击普通连击4/5/6/7/8等级动作2移动/闪避跑动、翻滚3/4/5/6/7/8等级动作1待机/行走Idle、Walk全部可打断这张表解决了一件事打断规则从每个状态都要写条件变成查表。新加动作时只需要给它分配一个优先级系统自动就知道它能打断谁、能被谁打断。3.2 三种打断策略每个动作内部的打断点是不同的所以不能简单地说高优先级直接压过低优先级。我在项目里实现了三种打断策略无条件打断常见于闪避、防御、跳跃。无论当前动作在哪个阶段只要按下对应按键立即切换。这种策略适合生存向动作玩家需要时刻判断施放时机。帧窗口打断常见于攻击动作的取消。只有在前摇阶段或者命中帧之后的特定时间段内才允许被其他动作打断。超出窗口就要等当前动作播放完。条件打断常见于受击。只有当攻击判定命中角色且角色处于非霸体状态时才切换到受击动画。三种策略并存配合优先级表动作系统在判断能否打断时只需要三步当前动作的打断策略是否允许目标动作的优先级是否足够当前播放的进度是否在可打断帧范围内三步全过就执行切换否则维持原动作。3.3 实际调优中踩过的一个坑最初我把受击硬直的优先级设为2想着大部分动作都能打断受击。结果玩家角色被打飞之后如果在空中按一下攻击键角色居然能瞬间解除硬直在空中打出攻击看起来特别违和。后来发现优先级表只解决了能不能抢占的问题没有解决当前状态允不允许抢占的问题。空中、倒地、被击飞这几种状态应该单独有一个状态锁这些状态里不允许低优先级动作抢占。最终的规则变成了状态锁优先于优先级表只有解除锁定后优先级表才生效。这个改动之后空中受击被攻击打断的Bug就消失了。4. 命中判定的侦探游戏从判定体生成到打击感事件链动作逻辑的另一个大块是攻击判定。很多Demo里角色挥剑动画播放完敌人扣血看起来能跑通但玩家玩起来觉得砍空气没有打击感。问题多半出在判定时机和判定体的设计上。4.1 判定体生成时机比看见动画更重要新手常犯的错误是动画一播放就生成攻击判定直到动画播完才消失。这样做的直接问题是攻击判定覆盖了前摇阶段玩家还没看到刀刃挥出去伤害就已经打出来了。正确做法是在动画资源里挂判定事件帧。攻击判定不是由逻辑代码猜时机而是由动画帧明确标记第几帧开始生成判定体第几帧结束判定体用什么形状偏移多少。当角色播放攻击动画时动作系统监听这些事件帧精确地在对应时间生成和销毁判定体。我项目里的攻击判定事件结构长这样{ attackId: sword_combo_01, events: [ { frame: 14, type: hitbox_start, shape: capsule, radius: 0.55, height: 1.8, offset: [0.2, 1.2, 0.6] }, { frame: 20, type: hitbox_end }, { frame: 20, type: hit_effect, effect: slash_trail } ] }这里frame是动画帧序号。攻击判定窗口只有6帧约100毫秒但这段窗口如果落在刀刃已经挥出、看起来最应该造成伤害的时间点玩家的命中感会非常强。如果你的动画是30帧的开枪那一下的判定帧一定不要放在第1到5帧要放在枪口闪光和手臂伸直的共同帧附近。4.2 判定体形状球体、胶囊体还是长方体判定体形状不复杂但选错会很出戏。近战武器剑、刀适合用胶囊体——细长能覆盖挥砍弧线又不至于把背后也判进去。拳头攻击适合用球体拳头本身是一个点球体判定接地气。远程子弹适合用胶囊体或者长方体表现出子弹飞行方向上的贯穿感。关键点在于判定体跟着武器走而不是跟着角色中心走。如果判定体永远固定在角色中心角色转向时攻击范围会出现明显偏差让判定体和武器骨节点绑定挥砍的弧线才能和视觉表现重合。4.3 打击感事件链命中之后发生了什么一次有效命中不应只做扣血这一件事。我在项目里定义了下面的事件链判定体触发目标 - 生成命中事件命中事件通知受伤方进入受击硬直状态命中事件同时通知攻击方播放命中反应动作例如轻微停顿命中点生成特效、飘字、音效镜头轻微震动震幅受攻击等级影响这套链路里最难调的是命中停顿Hit Stop——攻击命中瞬间双方动作暂停几帧给玩家一个砍中了的放大信号。停顿太短感觉像没砍到停顿太长节奏拖沓。我实验下来的参考值是轻攻击停顿3到5帧重攻击停顿6到8帧处决动画可到12帧。停顿期间攻击方和受击方都应暂停动画播放但特效和音效照常播放。4.4 受击反馈为什么角色被打了要做后退而不是原地抖受击动作还会牵涉受击位移。如果受击方原地播放动画但没有任何位移玩家会觉得打中了但对方没反应。我这里做了两个档位轻受击只播放动画原地不动重受击在播放动画的同时沿攻击方向推出一小段位移并叠加短暂的受击倾斜。这里要注意位移不能只靠修改Transform来实现否则会跟NavMesh和物理系统打架。我在项目里通过受击位移板解决受击事件触发后往受击者身上挂一个临时脚本用曲线驱动位移位移结束后自动回收。曲线前段快速推出后段减速停滞模拟被打击后失去平衡的感觉。5. 引擎落地Animator、GAS与自研状态机的取舍到这一步动作逻辑和机制在纸面上已经完整了。真到写代码的时候你还要面对选什么载体的问题。不同引擎、不同方案实现同样的逻辑工程量天差地别。5.1 三套主流方案对比方案优点缺点适合场景Unity Animator 自写状态机动画融合方便状态机可视化生态丰富打断规则写在状态机里会乱跨层动作支持弱中小型项目原型验证UE Gameplay Ability SystemGAS内置Ability框架网络同步成熟标签系统灵活学习曲线陡概念多小项目过重大型3A动作游戏吃鸡/类魂/MMO自研动作状态机插件规则完全可控不依赖编辑器自带状态机性能可控开发成本高动画融合要自己处理动作逻辑复杂的核心向游戏我在实验项目里选择了Unity 自写逻辑层 动画层分离的方式。Animator只负责播放哪个Clip、怎么过渡真正的状态判断和打断逻辑放在C#层。这样Animator的过渡图保持简单每帧只需要被动的执行该播什么动画。5.2 动画事件与逻辑事件不同步的坑这个坑是必须拿出来说的Unity的Animation Event是在动画线程里触发的如果你的攻击判定代码直接挂在Animation Event上一旦动画因为网络延迟、物理暂停、动画融合被跳过判定就会遗漏或错位。我后来把Animation Event统一改成逻辑事件广播器动画只在特定的帧发出一个纯逻辑信号比如AttackHitFrame这个信号进入事件队列由主循环的Update统一消费。这样即便动画播放卡顿、跳过过渡逻辑侧依然能收到完整的事件序列判定也就不会丢。类似的问题也存在于动画是否播完的判断上。不要用AnimatorStateInfo.normalizedTime来驱动连招逻辑那个值在动画循环和融合时会跳动不如自己维护一套逻辑播放进度用固定时间步更新与动画播放器解耦。5.3 Web端和工具链的额外思考这次项目还涉及一个3D场景交互验证的小环境用Web渲染做了原型。核心逻辑层我用TypeScript复刻了一遍状态机和输入缓冲完全保持一致只是渲染层从Unity换成了WebGL。这里有两个经验供参考第一在Web端做动作逻辑调试慢动作回放是利器——直接把时间缩放调成0.1倍所有判定帧、输入缓冲、优先级判定都能看得一清二楚第二Web端的动画混合精度会受帧率波动影响所以输入缓冲要比在Unity里再加大约30毫秒否则低帧率设备上手感差异明显。6. 调试动作逻辑必须掌握的三板斧动作系统的调试不能靠感觉不对就改参数。如果每一步都有迹可循调起来会快很多。6.1 可视化的状态运行面板我做一个简单的调试面板实时显示当前角色状态当前动作ID、播放进度、所属优先级本帧输入缓冲队列哪些按键在等窗口当前可被打断的策略类型和剩余窗口最近5次状态切换的时间线这个面板帮我解决了很多莫名跳动作的问题。比如玩家经常发现角色在跑动中突然抽搐面板上一看原来是输入缓冲里残留了一个攻击指令跑动的某个帧窗口恰好允许打断于是角色来了一刀。这种问题如果不可视化很难从一堆日志里找出来。6.2 慢动作重放系统这是我整轮开发里性价比最高的工具。每次手感调优录一段玩家操作存按键时间戳然后以0.2倍速重放暂停在关键帧逐一检查输入缓冲是否清空、优先级判定顺序是否正确、判定体是否和动画帧对齐。实际调试中先录音再回放比现场改现场试高效得多。因为现场调试时会不断重复操作手速变化会干扰判断而录音回放能保证每一次测试的操作序列完全一致这样你改参数前后的对比才是有效的。6.3 判定体的运行时渲染命中判定体在编辑器里选中的地方能看到但运行时如果不可见你就不知道攻击判定的真实范围。美术做了一条巨大的斩击刀光实际判定却只有角色身前的一小块胶囊体这在视觉上特别露馅。我让所有判定体在Debug模式下常显用线框绘制并且用颜色区分当前处于待激活还是已激活状态。这一招基本能解决刀光砍到人但没伤害的反馈失真问题。7. 一些留给后来者的经验整个项目做下来最大的体会是动作逻辑不是代码堆出来的是约束堆出来的。优先级表、输入窗口、打断策略、判定事件帧这些东西每一项都是在定游戏允许玩家做什么、不允许做什么的边界。边界定得越清楚手感就越扎实边界模糊哪怕动画再华丽玩起来也只是在播片。如果你准备起手做一个动作类交互实验我建议别一上来就追求表现力先把这三件事做扎实一套独立于渲染的输入缓冲系统、一张足够细的动作优先级表、一个和动画帧对齐的判定事件机制。这三样稳定之后再接特效、镜头、音效手感才立得住。最后说一个小技巧在项目里给每个动作单独建一个手感配置脚本把输入缓冲、可打断窗口、优先级、命中停顿、镜头震动幅度全部集中在这个配置里。你的策划如果也想参与调手感只需要调这个脚本里的参数就行不需要碰任何状态机代码。这个做法看似笨拙但后续迭代手感时它的价值会被无限放大。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进