我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Unity动画分层实战:AvatarMask与Speed Multiplier实现边跑边攻击

Unity动画分层实战:AvatarMask与Speed Multiplier实现边跑边攻击 做动作游戏最熟悉的“吃饭手艺”之一就是让角色在跑动中出刀。标题里这串东西看着像几个零散关键词其实是把一套小系统全讲完了Animator 控制状态切换AvatarMask 决定哪些骨骼参与当前动画再单独给 run 动画配一个速度倍率。把这几个东西串起来你就能做出一个“边跑边砍而且跑得快慢还能自己调”的通用角色逻辑。这篇文章就是按这个思路把整套做法拆开适合刚接触动画层又想改点东西的开发者也适合被“为什么我上半身一动下半身就停”逼疯的老哥。1. 先把需求拆清楚1.1 这个标题到底在问什么标题里的关键字其实暴露了三个真实痛点第一角色需要在 locomotion 过程中不中断移动地去播放攻击动画第二攻击动作要出现在上半身下半身该跑还得跑第三run 这个动作的播放速度没法通过调整整个 Animator 的 speed 来解决否则会殃及攻击、待机等其它动画。很多第一次做这个功能的人会直接把攻击动画放进 Base Layer结果一攻击整条腿都停下来或者攻击动画播放时速度调到 2.0走路立刻变成鬼畜快进。问题的根源是“动画层”这个概念没吃透。这套玩法通常用在 ARPG、MOBA、俯视角射击这类需要“移动过程中释放技能”的游戏里。核心目标不是让动画“能播”而是让不同身体部位的动画互不干扰——下半身引擎是移动上半身引擎是出招最后在两个引擎之间做一个干净利落的分工。1.2 为什么非要分上半身和下半身如果所有动画都放在同一层那 Animator 在同一时刻只能选中一个 State过渡结束后播放的就是一个完整的全身动画。你想让角色“边跑边砍”可系统智商不够没法理解“奔跑的是腿攻击的是手”。你给它一个攻击 clip它就把全身所有骨骼都带过去腿自然就停了。所以必须分两层Base Layer 负责 Idle / Walk / Run攻击相关放到单独的 UpperBody 层。上层挂一个 AvatarMask把下半身挖掉这样上层动画只更新手臂、躯干和头下半身的骨骼数据由 Base Layer 继续输出。两个层的结果最后在引擎内部做骨骼混合最终显示出来的就是上攻下走。1.3 AvatarMask 在里面的角色AvatarMask 本质上是一张“哪些骨骼参与动画”的过滤表。对 Humanoid 模型来说Uniy 把骨骼归类成了头、胸、脊椎、左右臂、左右腿这些区域所以很方便。你创建一个 mask把“Legs”这一大类关掉只保留 Arm、Spine、Head 这些上半身部件攻击动画放进这个层以后即便 clip 里有腿部 pose那部分数据也会被引擎丢进垃圾桶。也可以反过来创建 lower body 的 mask看项目习惯。我通常用一个“UpperBody” mask因为移动状态本身在任何层都是全身的上层只负责局部覆盖这样容易控制权重也更符合“覆盖”的直觉。2. 动画状态机与层结构搭建2.1 搭建三个基础状态先别急着做拆分。第一步是把 Base Layer 的状态机做出来至少要包含 Idle、Run、Walk 三个状态。这三个状态之间用速度参数控制过渡这是最常见的 locomotion 做法。参数就叫 Speed从脚本里把实际移动速度归一化后灌进去。过渡条件Idle - Run 时 Speed 0.2Run - Idle 时 Speed 0.1。如果还要走和跑之间区分可以再引入一个 Sprint 参数或增加 Speed 阈值。不要在一个状态机上堆十几个状态后面调混合树和层权重会非常痛苦。2.2 创建 AvatarMask 并配置层在 Project 窗口右键 - Create - Avatar Mask重命名 UpperBodyMask。Inspector 里把 Humanoid 的 Left Foot、Right Foot、Left Leg、Right Leg 全部取消勾选其它上半身部位保持勾选。如果你用的是 Generic 动画没法走 Humanoid 面板那就得在 Transform 列表里把下半身骨骼节点剔除麻烦一些但也一样。然后打开 Animator 窗口点击左侧 Layers 标签底部的加号。新增一个 Layer命名 Attack。在最右侧的 Mask 栏里选择刚建好的 UpperBodyMaskWeight 初始给 1。它的作用是这一层里面的动画只对 UpperBodyMask 里选中的骨骼生效。这里有个经常被误解的地方Layer 的 Weight 和 Mask 不是同一件事。Mask 决定哪些骨骼参与Weight 决定这一层输出的动画在混合时占多大强度。攻击层 Weight 1 时mask 覆盖区域完全被上层动画接管Weight 0.5 时则是半个身位融入走/跑姿势。2.3 层的权重为什么不能写死如果你只是做“边走边攻击”确实可以把 Attack 层权重设成 1 后不管。但现实里的攻击动作需要过渡比如角色伸手这个前摇你不能“啪”地一下就抡圆了。动态调权重反而能做出更自然的 blend。具体做法在 Attack 层添加一个 Float 参数 LayerWeight代码里根据当前是否攻击中给目标值然后在 Update 里做 Lerp。攻击发出后设为 1攻击结束设为 0。小时候我图省事直接设 1结果每次出刀都是机械跳变后来改成平滑过渡观感立刻提升一个档次。步骤上没什么玄学代码也不多public void UpdateLayerWeight(float target) { layerWeight Mathf.Lerp(layerWeight, target, Time.deltaTime * 10f); animator.SetLayerWeight(attackLayerIndex, layerWeight); }具体数值自己试10 这个系数对应大概 0.1 秒的逼近速度手感比较顺。3. 边走边攻击从参数到过渡3.1 移动参数和攻击触发走到这一步Base Layer 已经能正常跑Attack 层也挂好了 mask。现在要做的就是把攻击动画塞进 Attack 层并让它响应触发。我在 Attack 层里只放一个状态叫 Attack它的 Motion 是攻击动画 Clip。然后在这个状态上设置过渡条过渡来源是 Any State。触发条件用触发器 AttackTrigger。animator.SetTrigger(AttackTrigger);脚本里判断玩家按下攻击键后执行这一句。注意不要每帧调用 SetTrigger最好做一个攻击冷却或者输入缓冲不然会把状态机弄得很抖。这里为什么用 Any State 而不是从 Idle 进 Attack因为攻击层本身是独立的一个层你希望“任何时候觉手上没正事按下键就开始打”。Any State 是这个需求最直接的表达。但 Any State 也有个坑如果攻击层已经处于 Attack 状态再进入 Attack 会打断上一次的后摇。所以需要加一个条件——用一个 Bool 参数“IsAttacking”攻击开始时打开动画事件或后摇结束时机位关闭过渡条件里加上 IsAttacking false。3.2 过渡条件的优先级当你同时按下移动键和攻击键两个层各干各的Base Layer 继续往 Run 走Attack 层进入 Attack。这看起来像“同时发生了两个状态切换”实际上并不冲突。关键是别让 Base Layer 的移动状态因为攻击动画里可能自带的位移而被牵连。过渡的 Exit Time 和 Transition Duration 要克制。Attack 层进 Attack 的过渡时间可以设成 0.05 秒保证出刀够快出来时可以设成 0.1-0.2 秒让手有个自然回收的过程。不要为了“干净”把过渡全部拉成 0不然动作像假肢。3.3 攻击时脚下的移动怎么处理角色边跑边攻击脚下该怎么动这个问题最常见的方式有两种一种是纯代码移动动画只负责出刀另一种是让攻击 clip 里的脚步位移也参与角色运动也就是“攻击前踢”之类的技巧。在 ARPG 里我强烈建议纯代码移动与动画移动分层合作Base Layer 提供方向、位移Attack 层只负责让上半身摆出姿势。这样无论你怎么改攻击动画都不会出现“一刀把自己瞬移出去”的怪事。如果攻击动作本身有必要的前冲比如突刺就把这个位移单独做成一个“攻击位移曲线”或处理 Apply Root Motion。不要在 Animator.speed 上想办法让位移加速。3.4 转身与朝向边走边攻击里经常会遇到角色同时转向的问题。比如你在向右跑敌人突然在左边你要向左攻击。如果只改动画角色上半身先转下半身还在跑看起来会很拧巴。我的处理是攻击方向由 Input 和敌人相对位置决定脚本把角色 transform 平滑旋转到目标朝向而不是依赖 Animator 的代码。Animator 里的参数就只管速度和攻击触发别让动画层背锅。if (targetDirection ! Vector3.zero) { Quaternion lookRot Quaternion.LookRotation(targetDirection); transform.rotation Quaternion.Slerp(transform.rotation, lookRot, Time.deltaTime * 10f); }这样 Base Layer 和 Attack 层都不用关心角色朝向动画只是播放姿势姿态不会因为转向而扭曲。4. 单独控制 run 动画速度4.1 直接调 Animator.speed 会惹什么祸新手最容易踩的坑就是角色跑得慢了直接 animator.speed 0.8。这个做法会把整个 Animator 所有层的动画播放速度全部缩放。走、跑、攻击、待机全都变成慢动作攻击手感瞬间变飘。而且动画事件触发时间也跟着变物理帧同步会被打乱。所以要想让“run 动画本身”变快或变慢就要利用 Animator 每个 State 自带的 Speed Multiplier 属性而不是全局调速。4.2 用 Speed Multiplier 单独加速 run在 Animator 窗口选中 Run 状态Inspector 里能看到 Motion 下面有一个 Speed 字段默认 1。这个就是 Speed Multiplier。你把它改成 1.4Run 动画会以 1.4 倍速度播放而其它状态的动画不受影响。在标题场景里这是最直接的“单独设置 run 动画速度”实现。如果你只是想让“跑步加速后脚步频率稍微变快”这里用到的是一个 Float 参数来驱动。比如你创建 Float 参数 RunSpeedMult然后在 Run 状态的 Speed 字段里引用它Inspector 的 Speed 框里填RunSpeedMult或者点击 Speed 旁边的小箭头选择参数。脚本里根据角色当前移动速度设这个值。这样移动越快run clip 播放得越快脚部频率匹配移动速度观感上就不会出现“跑步速度很快但脚在地上踩了一遍又一遍”的滑步感。4.3 从代码控制动画速度配合移动速度做动态变化最靠谱的方法是定义最大跑步速度然后用比例驱动。我一般在角色 Controller 里面保存 forwardSpeed 和 maxRunSpeedfloat speedRatio Mathf.Clamp01(rb.velocity.magnitude / maxRunSpeed); animator.SetFloat(RunSpeedMult, Mathf.Lerp(animator.GetFloat(RunSpeedMult), speedRatio * 1.2f, Time.deltaTime * 8f));其中乘 1.2 是给你心里那把尺留个余量。是否要加 Lerp 取决于项目风格如果你希望速度变化是瞬时的可以不加如果你做的是偏写实手感Lerp 能防止跑步动画在慢和快之间抽风。另外如果你做的是可进入奔跑状态的情况最好让动画切到 Run 状态后再去设置这个参数不然在 Walk 状态下设 RunSpeedMult 也没用。4.4 一个血泪教训Action 类游戏我把 run 的 Speed Multiplier 调成 1.5角色跑得很爽但后面发现攻击里有一个 “蓄力挥手” 的动画它的攻击前摇也依赖全局时间结果攻击动画播放到一半就切换地位了。排查半天发现是 Attack 层里的动画状态也引用了同一个 Speed 参数原则。这也是为什么我坚持run 的速度倍率那只绑定在 Run 这个状态上不要通过 Animator.SetFloat 给全状态树传同一个数值除非你确定所有引用它的状态都需要同步加速。5. 常见问题排查速查表5.1 下半身不动的奇怪姿势如果你打完攻击后角色下半身僵死第一查 Attack 层的 AvatarMask 是不是把腿也勾上了。Mask 里 Left Leg / Right Leg 没取消勾选是常见原因。其次检查层的混合模式默认是 Override如果用成 Additive骨骼叠加逻辑完全不同。5.2 普遍存在的滑步感如果走路或跑步速度看起来跟脚不匹配先别调 Speed Multiplier。先确认你的动画本身是不是 Root Motion。如果 Animator 勾了 Apply Root Motion位移由动画决定的那你再手动改速度系数等于双重叠加。一般代码控制位移时要把 Apply Root Motion 关掉或者只在动画里用 root 旋转位移交给脚本。5.3 攻击动作被转身打断攻击层在 Attack 状态时如果 Any State 到 Attack 的触发条件没做“当前不在攻击中”的过滤就会攻击到一半被新的攻击触发器切成新的 Attack。轻则动作闪切重则状态机死锁。解决办法就是给攻击层加一个 IsAttacking 的参数然后把 Any State - Attack 的条件改成AttackTrigger true IsAttacking false。5.4 动画跳转闪到管辖外如果你用 Any State 作为攻击层的入口发现角色在半空中“哐”地摆出攻击姿势大概率是攻击层的状态机里没有其它 State你想要让它在结束后回到一个“没有任何动画”的 state。可以建一个 Empty stateAttack 结束后回到 Empty避免 Any State 在攻击后还挂着“离开上一个动作”的尴尬。5.5 事件触发时机不对攻击伤害判定最好不要卡在动画开头第一帧敲 OnAnimationEvent。由于动画层叠加和 Speed Multiplier 的存在攻击 clip 的播放速度可能变化结果同一样一个事件在加速时提前触发。稳妥的做法是用 AnimatorStateInfo.normalizedTime 做判断在脚本 Update 里监控攻击状态走到 0.3-0.5 这个窗口再触发伤害判定。AnimatorStateInfo stateInfo animator.GetCurrentAnimatorStateInfo(attackLayerIndex); if (stateInfo.IsName(Attack) stateInfo.normalizedTime 0.3f stateInfo.normalizedTime 0.5f) { if (!damageSent) { ApplyDamage(); damageSent true; } }remember to reset damageSent when exiting attack state.6. 写在最后一点个人心得这套东西我做过很多遍最后总结下来就三句话第一动画层是用来分身体部位的不是用来装功能模块的第二单独控速度优先看状态自带的 Speed Multiplier尽量别碰 Animator.speed第三遇到诡异姿势先查 mask 和层的混合模式别去调动画资源里的曲线。等你做熟了会发现边跑边砍根本不是“把两个播放器拼在一起”而是让两条骨骼数据流各司其职最后在蒙皮阶段自然叠出你要的画面。像这种小系统后续还能往上面加翻滚、格挡、骑乘只要骨架搭对了一切都能平滑扩展。
返回列表